3. UDP客户端与服务端:简单回显服务器实现、客户端发送与接收、单次通信与多次通信

这一章,我们直接动手写代码。

UDP 通信说白了就是两个程序互相扔数据包。一个扔,一个接,接完了再扔回去。这就是回显(Echo)服务——你发什么,我原样返回什么。别小看这个模式,我在生产环境里用它做过心跳检测、日志收集,甚至临时调试工具。简单,但实用。

3.1 回显服务器的核心逻辑

回显服务器要做什么?就三件事:

  1. 绑定一个端口,等着数据来
  2. 收到数据后,记下客户端的地址和端口
  3. 把数据原封不动发回去

嗯,这里要注意: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 通信流程可视化

下面这张图把整个流程串起来了。你看一眼就能记住:

UDP 回显通信流程 客户端 服务器 1. 创建 socket 1. 创建 socket + bind sendto(数据) 2. recvfrom(等待) 2. recvfrom(收到) sendto(原数据) 3. 打印回复 3. 继续等待 单次通信:1→2→3 结束;多次通信:循环 1→2→3→1→2→3...

单次通信就是走一遍流程:客户端发、服务器收、服务器回、客户端收。多次通信就是把这个流程放在循环里,客户端可以连续发多条,服务器每次收到都回。

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 通信的基石。后面的所有高级话题——广播、多播、可靠性、性能调优——都建立在这个基础之上。先把回显跑通,把单次和多次通信的逻辑刻在脑子里,后面就顺了。