3、UDP单播通信:单播原理、点对点日志发送、接收端绑定与监听、单播日志示例
单播,说白了就是「一对一」的通信模式。一个发送端,一个接收端,数据包在网络上走一条明确的路径。我刚开始接触网络编程时,总觉得单播太简单,没什么好研究的。后来在线上环境排查过一个日志丢失的问题,才发现单播里那些看似不起眼的细节,才是真正要命的坑。
这一章,我们就从单播的原理讲起,然后手把手搭建一个点对点的日志发送与接收系统。你会看到完整的 Python 代码,也会明白为什么接收端必须绑定端口,以及监听时有哪些容易忽略的陷阱。
3.1 单播原理:数据包怎么找到你?
UDP 单播的流程其实很直白。发送端把数据封装成报文,加上目标 IP 和端口,扔进网络。路由器根据 IP 地址把报文一步步转发,最终到达目标主机。目标主机的操作系统根据端口号,把数据交给绑定了该端口的应用程序。
这里有个关键点:UDP 是无连接的。发送端不需要提前和接收端「握手」,直接发就行。接收端也不需要告诉发送端「我准备好了」,它只需要在某个端口上等着。
我遇到过不少新手问:「如果接收端没启动,发送端会报错吗?」答案是:不会。UDP 没有连接的概念,数据发出去就完了。至于有没有人收,它不管。这也是 UDP 日志系统需要额外做可靠性设计的原因。
3.2 点对点日志发送:Python 实现
先看发送端的代码。我用 Python 的 socket 模块,这是标准库,不需要额外安装。
import socket
import json
import time
# 目标接收端的 IP 和端口
TARGET_IP = "192.168.1.100"
TARGET_PORT = 9999
# 创建 UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 模拟日志数据
log_entry = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"level": "INFO",
"module": "user_service",
"message": "用户登录成功,user_id=1024"
}
# 序列化为 JSON 字符串
log_json = json.dumps(log_entry)
# 发送
sock.sendto(log_json.encode('utf-8'), (TARGET_IP, TARGET_PORT))
print(f"已发送日志: {log_json}")
sock.close()
这段代码做了几件事:
- 创建了一个 UDP socket,类型是
SOCK_DGRAM。 - 把日志字典转成 JSON 字符串,再编码为字节流。
- 用
sendto方法发送到目标地址。
你可能会问:「为什么用 JSON?直接用字符串不行吗?」当然可以。但我个人习惯用 JSON,因为结构清晰,接收端解析方便。如果以后要加字段,JSON 的扩展性也更好。
3.3 接收端绑定与监听:别让数据白来一趟
接收端要做的事情更关键。它必须绑定一个端口,然后在这个端口上持续监听。
import socket
import json
# 本机监听端口
LISTEN_PORT = 9999
# 创建 UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定到本机所有网络接口的指定端口
sock.bind(("0.0.0.0", LISTEN_PORT))
print(f"UDP 接收端已启动,监听端口: {LISTEN_PORT}")
while True:
# 接收数据,缓冲区大小设为 4096 字节
data, addr = sock.recvfrom(4096)
# 解析
log_entry = json.loads(data.decode('utf-8'))
print(f"收到来自 {addr} 的日志:")
print(f" 时间: {log_entry['timestamp']}")
print(f" 级别: {log_entry['level']}")
print(f" 模块: {log_entry['module']}")
print(f" 消息: {log_entry['message']}")
print("-" * 40)
这里有几个细节值得注意:
bind(("0.0.0.0", LISTEN_PORT)):0.0.0.0表示监听本机所有网络接口。如果你只想监听某个特定 IP,可以换成具体的地址。recvfrom(4096):参数是缓冲区大小。我一般设 4096,够用。如果日志内容可能更大,可以调高到 65535。- 死循环:接收端通常是一个长期运行的服务,所以用
while True持续监听。
3.4 单播日志示例:跑起来看看
我们把发送端和接收端放在一起测试。假设接收端在 192.168.1.100 上运行,发送端在另一台机器上。
接收端输出:
UDP 接收端已启动,监听端口: 9999
收到来自 ('192.168.1.101', 54321) 的日志:
时间: 2025-03-15 14:30:22
级别: INFO
模块: user_service
消息: 用户登录成功,user_id=1024
----------------------------------------
注意看 addr 里的端口号 54321。这是发送端操作系统随机分配的临时端口。每次发送,端口都可能不同。接收端回复数据时,就需要用这个地址。
但说实话,在日志监控场景里,接收端一般不需要回复。日志是单向流动的——从业务系统流向监控中心。这也是 UDP 适合日志传输的原因之一:单向、轻量、低延迟。
3.5 单播 vs 其他模式:一张表说清楚
| 通信模式 | 发送端 | 接收端 | 典型场景 |
|---|---|---|---|
| 单播 | 1 个 | 1 个 | 点对点日志、设备控制 |
| 广播 | 1 个 | 同一网段所有主机 | ARP 请求、DHCP 发现 |
| 组播 | 1 个 | 加入组播组的主机 | 视频会议、数据分发 |
单播的优势是简单、可控。你明确知道数据发给谁,不会像广播那样「吵」到整个网络。缺点也很明显——如果有多台接收端,你得发多份。下一章我们会讲广播,它能解决「一对多」的问题,但又有自己的麻烦。
3.6 本章知识结构图
下面这张 SVG 图把单播通信的核心流程串起来了。你可以对照着看,加深理解。
3.7 几个实战建议
- 端口别冲突:系统保留端口(0-1023)需要 root 权限。日志监控建议用 1024 以上的端口,比如 9999、8888。
- 缓冲区要够:如果日志量很大,接收端处理不过来,数据会在内核缓冲区堆积。缓冲区满了之后,新来的包会被丢弃。你可以用
setsockopt调大缓冲区。 - 日志要有序列号:UDP 不保证顺序,也不保证不重复。我习惯在每条日志里加一个递增的序列号,接收端可以据此检测丢包和乱序。