3、UDP单播通信:单播原理、点对点日志发送、接收端绑定与监听、单播日志示例

单播,说白了就是「一对一」的通信模式。一个发送端,一个接收端,数据包在网络上走一条明确的路径。我刚开始接触网络编程时,总觉得单播太简单,没什么好研究的。后来在线上环境排查过一个日志丢失的问题,才发现单播里那些看似不起眼的细节,才是真正要命的坑。

这一章,我们就从单播的原理讲起,然后手把手搭建一个点对点的日志发送与接收系统。你会看到完整的 Python 代码,也会明白为什么接收端必须绑定端口,以及监听时有哪些容易忽略的陷阱。

核心要点:单播是 UDP 最基础的通信模式。发送端指定目标 IP 和端口,数据包直接送达。接收端需要提前绑定端口,否则数据到了也无人接收。

3.1 单播原理:数据包怎么找到你?

UDP 单播的流程其实很直白。发送端把数据封装成报文,加上目标 IP 和端口,扔进网络。路由器根据 IP 地址把报文一步步转发,最终到达目标主机。目标主机的操作系统根据端口号,把数据交给绑定了该端口的应用程序。

这里有个关键点:UDP 是无连接的。发送端不需要提前和接收端「握手」,直接发就行。接收端也不需要告诉发送端「我准备好了」,它只需要在某个端口上等着。

我遇到过不少新手问:「如果接收端没启动,发送端会报错吗?」答案是:不会。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 的扩展性也更好。

注意:UDP 单次发送的数据包大小有限制。以太网环境下,建议控制在 1472 字节以内(1500 MTU 减去 20 字节 IP 头、8 字节 UDP 头)。超过这个值,IP 层会分片,分片丢失会导致整个包无法重组。我曾经因为这个丢过一批日志,排查了半天才发现是日志内容太长。

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 持续监听。
避坑指南:我曾经在生产环境遇到过一个问题——接收端启动后,明明程序没报错,但就是收不到数据。排查了半天,发现是防火墙把 UDP 9999 端口给拦了。所以,部署时一定要确认防火墙规则。另外,如果你在容器里跑,别忘了端口映射。

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 图把单播通信的核心流程串起来了。你可以对照着看,加深理解。

UDP 单播通信流程 发送端 socket(AF_INET, SOCK_DGRAM) UDP 数据包 目标 IP + 端口 接收端 bind() + recvfrom() 关键点: 发送端无需连接,直接 sendto();接收端必须提前 bind() 端口,否则数据到达后无人接收。 ① 创建 Socket ② 绑定端口 ③ 发送/接收 ④ 关闭 Socket 接收端步骤:① → ② → ③ → ④(发送端可跳过 ②)

3.7 几个实战建议

  • 端口别冲突:系统保留端口(0-1023)需要 root 权限。日志监控建议用 1024 以上的端口,比如 9999、8888。
  • 缓冲区要够:如果日志量很大,接收端处理不过来,数据会在内核缓冲区堆积。缓冲区满了之后,新来的包会被丢弃。你可以用 setsockopt 调大缓冲区。
  • 日志要有序列号:UDP 不保证顺序,也不保证不重复。我习惯在每条日志里加一个递增的序列号,接收端可以据此检测丢包和乱序。
再强调一次:UDP 单播没有拥塞控制。如果发送端疯狂发数据,而接收端处理不过来,丢包是必然的。在日志量大的场景下,一定要在发送端做限速,或者在接收端用异步处理。