3. 套接字编程基础
这一章,我们直接动手写代码。
UDP 通信说白了就是两个程序互相扔数据包。一个扔,一个接,接完了再扔回去。这就是回显(Echo)服务——你发什么,我原样返回什么。别小看这个模式,我在生产环境里用它做过心跳检测、日志收集,甚至临时调试工具。简单,但实用。
3.1 回显服务器的核心逻辑
回显服务器要做什么?就三件事:
- 绑定一个端口,等着数据来
- 收到数据后,记下客户端的地址和端口
- 把数据原封不动发回去
嗯,这里要注意:UDP 是无连接的,所以服务器每次收到数据,都必须从 recvfrom 的返回值里拿到对方的地址。不像 TCP 已经建立了连接,可以直接 send。我刚开始写 UDP 时犯过这个错——直接用 send 代替 sendto,结果数据不知道发给谁了。
关键区别:UDP 服务器必须使用 sendto() 指定目标地址,TCP 服务器使用 send() 即可。
3.2 代码实现:回显服务器
我用 Python 的 socket 模块来实现。Python 的语法简洁,适合讲清楚逻辑。你换成 C 或 Java,思路完全一样。
import socket
# 创建 UDP 套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定到本机的 8888 端口
server_address = ('0.0.0.0', 8888)
server_socket.bind(server_address)
print(f"UDP 回显服务器已启动,监听 {server_address}")
while True:
# 接收数据,最大 1024 字节
data, client_address = server_socket.recvfrom(1024)
print(f"收到来自 {client_address} 的消息:{data.decode()}")
# 原样返回
server_socket.sendto(data, client_address)
print(f"已回显给 {client_address}")
这段代码里,recvfrom 返回两个值:一个是收到的字节数据,一个是客户端的地址元组(IP, 端口)。sendto 的第一个参数是要发的数据,第二个参数是目标地址。
个人习惯:我一般把缓冲区大小设成 1024 或 4096。UDP 数据报最大 65507 字节,但设太大容易浪费内存。如果消息可能超过缓冲区,recvfrom 会截断,丢掉的你就拿不到了。
3.3 客户端:发送与接收
客户端更简单。创建套接字,发数据,等回复。不需要 connect,直接 sendto 就行。
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('127.0.0.1', 8888)
message = "Hello, UDP Server!"
client_socket.sendto(message.encode(), server_address)
print(f"已发送:{message}")
# 等待回复
data, server = client_socket.recvfrom(1024)
print(f"收到回复:{data.decode()}")
client_socket.close()
你运行服务器,再运行客户端,终端上会看到:
# 服务器端输出
UDP 回显服务器已启动,监听 ('0.0.0.0', 8888)
收到来自 ('127.0.0.1', 54321) 的消息:Hello, UDP Server!
已回显给 ('127.0.0.1', 54321)
# 客户端输出
已发送:Hello, UDP Server!
收到回复:Hello, UDP Server!
看到没?数据原样回来了。这就是一次完整的 UDP 单次通信。
3.4 单次通信 vs 多次通信
上面的例子是单次通信:客户端发一条,收一条,结束。但实际场景中,客户端往往需要连续发多条消息,比如传感器每隔一秒上报一次数据。
多次通信的客户端,其实就是把发送和接收放到循环里:
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('127.0.0.1', 8888)
messages = ["第1条消息", "第2条消息", "第3条消息", "exit"]
for msg in messages:
client_socket.sendto(msg.encode(), server_address)
data, _ = client_socket.recvfrom(1024)
print(f"回复:{data.decode()}")
client_socket.close()
服务器端不需要改——它本来就一直在 while 循环里等着。你发多少条,它就回多少条。
我曾经踩过的坑:UDP 的 recvfrom 是阻塞的。如果客户端发了消息但服务器没收到(比如网络丢包),客户端会一直卡在 recvfrom 那里。生产环境一定要加超时:client_socket.settimeout(5.0),5 秒没回复就重发或报错。
3.5 通信流程可视化
下面这张图把整个流程串起来了。你看一眼就能记住:
单次通信就是走一遍流程:客户端发、服务器收、服务器回、客户端收。多次通信就是把这个流程放在循环里,客户端可以连续发多条,服务器每次收到都回。
3.6 几个重要的细节
| 概念 | 说明 | 我的建议 |
|---|---|---|
| 缓冲区大小 | recvfrom 的参数,一次最多读多少字节 | 设 1024 够用,别超过 65507 |
| 阻塞 vs 非阻塞 | 默认 recvfrom 是阻塞的,没数据就卡住 | 用 settimeout 设超时,别让程序死等 |
| 地址复用 | 服务器关闭后立即重启可能报错 Address already in use | 加 setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) |
| 数据编码 | sendto 需要 bytes,字符串要 encode/decode | 统一用 UTF-8,避免乱码 |
一个小技巧:调试 UDP 时,我经常在客户端和服务器两边都打印时间戳。这样能快速判断是网络延迟还是代码逻辑问题。比如:print(f"[{time.time():.3f}] 收到数据")。
3.7 完整的多轮通信示例
最后给一个能跑起来的完整例子。服务器不变,客户端支持从终端输入,发什么回什么,输入 quit 退出。
# echo_client_interactive.py
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.settimeout(5.0) # 5秒超时
server_addr = ('127.0.0.1', 8888)
while True:
msg = input("请输入消息(quit 退出):")
if msg == 'quit':
break
client.sendto(msg.encode(), server_addr)
try:
data, _ = client.recvfrom(1024)
print(f"服务器回复:{data.decode()}")
except socket.timeout:
print("超时了,服务器没响应")
client.close()
你运行服务器,再运行这个客户端,就能体验多轮 UDP 通信了。发一句,回一句,清清楚楚。
这一章的内容就到这里。代码不多,但这是 UDP 通信的基石。后面的所有高级话题——广播、多播、可靠性、性能调优——都建立在这个基础之上。先把回显跑通,把单次和多次通信的逻辑刻在脑子里,后面就顺了。