TCP与UDP客户端—服务器编程
TCP
Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。
Socket起源于UNIX一切皆文件的哲学思想,服务器和客户端各自维护一个Socket,在建立连接后,可以向自己Socket写入内容供对方读取或者读取对方内容,通讯结束时关闭Socket。
逐步运行TCP客户端与服务器
解锁完整互动实验、进阶解析与精选参考答案。
socket()
- IP地址类型:
AF_INET(IPv4)、AF_INET6(IPv6)。 - 数据传输方式/套接字类型:
SOCK_STREAM(流格式套接字/面向连接的套接字)、SOCK_DGRAM(数据报套接字/无连接的套接字)。
bind()
将Socket和本地地址绑定,地址包括IP和端口号。
127.0.0.1用于表示本地地址。
端口号的范围为0~65535,其中0~1023为知名端口(well known ports),用于绑定一些常用服务。
这里127.0.0.1是IPv4环回地址,适合本机实验;绑定端口0通常请求操作系统选择可用临时端口,不是指定一个固定服务端口。
listen()
监听,将接收到的客户端连接放入队列。
accept()
从队列获取请求,返回客户端Socket描述符。当已完成连接队列的下一个完成连接是空,那么accept()将被阻塞。
Python的accept()返回(connected_socket, peer_address)。监听套接字继续负责接收新连接,实际读写发生在返回的已连接套接字上。
read() / write()
双向通信。read()负责从Socket中读取内容,write()负责将内容写入Socket中。
Python套接字示例使用recv()与send()表达读写。send()可能只接受部分字节,sendall()会尝试发送全部数据或报告错误;它也不意味着对方已经处理了正文。
close()
关闭Socket,终止TCP/UDP连接。
UDP没有TCP式连接,关闭UDP套接字释放本地资源,不是执行TCP挥手。
本机TCP回显
分别启动服务器与客户端,再输入短ASCII文本;输入exit结束。两个程序都在本机运行,服务器先启动,8080端口必须可用。
服务器
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 8080
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((SERVER_HOST, SERVER_PORT))
sock.listen()
print("[Server] Server started.")
print("[Server] Waiting for connection...")
# Get client's socket and address
client, client_addr = sock.accept()
print("[Server] Client%s:%s Connected to server." % client_addr)
while True:
data = client.recv(100).decode("UTF8")
print("[Server] Received: %s" % data)
if data == "exit":
client.send("exit".encode("UTF8"))
break
else:
client.send(data.encode("UTF8"))
sock.close()
if __name__ == "__main__":
main()客户端
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 8080
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((SERVER_HOST, SERVER_PORT))
while True:
msg = input("[Client] Enter data: ")
sock.send(msg.encode("UTF8"))
reply = sock.recv(100).decode("UTF8")
if reply == "exit":
break
else:
print("[Server] " + reply)
sock.close()
if __name__ == "__main__":
main()这个简短示例展示调用关系,不是完整消息协议。TCP传输字节流,一次send()不保证对应一次recv();recv(100)最多读100字节,也不保证一次读满。网络可能拆分或合并内容,甚至拆开UTF-8字符。真正的文本消息需要长度或分隔符约定。
recv()返回b""表示对端已结束发送,不能当作新的空文本反复处理。还应明确关闭已连接套接字与监听套接字,设置适当超时并处理异常;示例为了突出顺序没有完整展示这些边界。
知识测验
UDP
UDP是尽最大能力进行传输,但是并不能保证可靠性,丢包并不会重发。TCP的可靠性是因为一系列的机制保证可靠性。两种协议并没有优劣之分,要区分不同的场景来区分。
例如进行文件传输,不能有数据丢失,TCP协议就更合适,而进行实时视频通话,UDP会根据恒定的速率进行发送,这样的情况允许部分数据的丢失去追求更好的实时性,所以UDP更合适。
发送速率由应用与协议控制,UDP本身不提供恒定速率保证。应用可以在UDP之上实现重传或其他可靠机制。实时应用也需拥塞控制与安全设计。
本机UDP回显
服务器
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 8080
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((SERVER_HOST, SERVER_PORT))
print("[Server] Server started.")
print("[Server] Waiting for connection...")
while True:
data, client_addr = sock.recvfrom(100)
print("[Server] Client%s:%s: " % client_addr, end=":")
data = data.decode("UTF8")
print(data)
if data == "exit":
sock.sendto("exit".encode("UTF8"), client_addr)
break
else:
sock.sendto(data.encode("UTF8"), client_addr)
sock.close()
if __name__ == "__main__":
main()客户端
import socket
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 8080
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input("[Client] Enter data: ")
sock.sendto(msg.encode("UTF8"), (SERVER_HOST, SERVER_PORT))
reply, addr = sock.recvfrom(100)
reply = reply.decode("UTF8")
if reply == "exit":
break
else:
print("[Server] " + reply)
sock.close()
if __name__ == "__main__":
main()recvfrom()返回数据和发送方地址,sendto()需要明确目标地址。服务器必须把包括exit在内的回复发回client_addr。代码输出的“Waiting for connection”在这里表示等待数据报,不代表UDP握手。
UDP保留数据报边界,但接收缓冲过小可能截断数据。这个示例每次最多接收100字节,因此仅测试短消息。若请求或回复丢失,阻塞接收可能一直等待,实际应用应使用超时与合适的恢复策略。
知识测验
编写TCP大写转换服务
编写一个Python脚本,在本机运行一个小型TCP服务并与它通信。与回显示例不同,服务器要把消息转成大写并附上字节数再回复,且消息以换行符结尾。
- 服务器:创建TCP套接字,绑定到127.0.0.1、端口0(让操作系统分配空闲端口),调用listen(),并在线程中运行accept/接收/回复部分。
- 客户端:连接到该端口,用sendall()发送"hello, leaflet"并以\n结尾。
- 服务器:反复调用recv()直到读到完整的一行(不要假设一次recv()就能拿到整条消息,recv()返回b''时要停止)。
- 服务器:回复"<大写消息> (<n> bytes)\n",其中n是消息的UTF-8字节数。
- 客户端:读取完整的回复行并打印,确保所有套接字都已关闭、脚本能自行结束。
预期输出:HELLO, LEAFLET (14 bytes)。再试试“你好”:回复应显示6字节,而不是2。