4. 阻塞与非阻塞UDP套接字
前面我们聊的都是两台机器之间的点对点通信。但实际工作中,你总会遇到这样的场景:一台服务器要同时给局域网里所有设备发消息,或者只发给特定的一组设备。这时候,单播就有点力不从心了。
我刚开始做网络编程时,就踩过这个坑。当时要做一个局域网设备发现功能,我傻乎乎地让每台设备都去轮询所有IP地址……结果网络拥堵得一塌糊涂。后来才明白,广播和组播才是干这活的正确姿势。
4.1 广播:一呼百应的粗暴方案
广播,说白了就是一台机器往局域网里吼一嗓子,所有机器都能听到。它的目标地址很特殊——广播地址。
4.1.1 广播地址长什么样?
IPv4的广播地址分两种:
| 类型 | 格式 | 示例 | 说明 |
|---|---|---|---|
| 有限广播 | 255.255.255.255 | 255.255.255.255 | 只在当前子网内广播,路由器不转发 |
| 直接广播 | 网络号 + 全1主机位 | 192.168.1.255 | 可跨路由器发到指定子网(但多数路由器默认禁止) |
实际开发中,有限广播(255.255.255.255)用得最多。因为它简单粗暴,不需要知道自己的子网掩码,直接往这个地址发就行。
4.1.2 Python实现广播发送
实现广播发送,关键一步是设置 SO_BROADCAST 套接字选项。不设置的话,系统会直接拒绝你往广播地址发数据。
import socket
# 创建UDP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 关键:允许广播
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 广播地址和端口
broadcast_addr = ('255.255.255.255', 8888)
message = b'Hello, everyone!'
# 发送广播
sock.sendto(message, broadcast_addr)
print(f'广播已发送到 {broadcast_addr}')
sock.close()
接收端就简单了,跟普通UDP接收一样,绑定到端口就行:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 8888)) # 监听所有网卡
while True:
data, addr = sock.recvfrom(1024)
print(f'收到来自 {addr} 的广播: {data.decode()}')
4.2 组播:精准投递的优雅方案
广播的问题很明显——太吵了。你想想看,如果我只想发给视频会议的那几个同事,结果全公司的人都收到了,多尴尬?
组播(Multicast)就是来解决这个问题的。它用组播地址来标识一个"组",只有加入这个组的设备才能收到消息。
4.2.1 组播地址范围
IPv4的组播地址范围是 224.0.0.0 ~ 239.255.255.255(D类地址)。其中有些是保留的:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 224.0.0.0 ~ 224.0.0.255 | 本地链路组播 | 路由协议用,TTL固定为1,不转发 |
| 224.0.1.0 ~ 238.255.255.255 | 全球范围组播 | 互联网上可用,需要IGMP支持 |
| 239.0.0.0 ~ 239.255.255.255 | 本地管理组播 | 企业内部用,相当于私有地址 |
我个人习惯用 239.x.x.x 做内部测试,不会跟公网组播冲突。
4.2.2 IGMP协议:组的管理员
IGMP(Internet Group Management Protocol)是组播的"管家"。它负责三件事:
- 加入组:主机告诉路由器"我想听这个组"
- 离开组:主机告诉路由器"我不听了"
- 维护组:路由器定期查询,看还有谁在听
IGMP有三个版本:
| 版本 | 特点 | 我遇到的坑 |
|---|---|---|
| IGMPv1 | 基本加入/离开,路由器主动查询 | 离开时不会通知路由器,有延迟 |
| IGMPv2 | 增加离开组报文,支持特定组查询 | 目前最常用,兼容性好 |
| IGMPv3 | 支持源过滤,只收指定源的组播 | 部分老旧交换机不支持 |
4.3 Python实现组播收发
组播的代码比广播稍微复杂一点,因为需要加入组播组。这个操作通过 setsockopt 的 IP_ADD_MEMBERSHIP 选项完成。
4.3.1 组播发送端
import socket
import struct
# 组播地址和端口
MULTICAST_GROUP = '239.0.0.1'
PORT = 9999
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 设置TTL,控制组播包能跳多少跳
# TTL=1 只在本地子网,TTL>1 可以跨路由器
ttl = struct.pack('b', 2)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
message = b'Hello, multicast group!'
sock.sendto(message, (MULTICAST_GROUP, PORT))
print(f'组播消息已发送到 {MULTICAST_GROUP}:{PORT}')
sock.close()
4.3.2 组播接收端
接收端需要做两件事:绑定端口,然后加入组播组。
import socket
import struct
MULTICAST_GROUP = '239.0.0.1'
PORT = 9999
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定到端口,注意要绑定到0.0.0.0
sock.bind(('0.0.0.0', PORT))
# 加入组播组
# struct.pack 格式:4字节IP + 4字节本地IP
group = socket.inet_aton(MULTICAST_GROUP)
local_if = socket.inet_aton('0.0.0.0') # 使用默认网卡
mreq = struct.pack('4s4s', group, local_if)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
print(f'已加入组播组 {MULTICAST_GROUP}:{PORT}')
while True:
data, addr = sock.recvfrom(1024)
print(f'收到来自 {addr} 的组播: {data.decode()}')
socket.inet_aton('192.168.1.100') 指定从哪块网卡加入组播组。我在做视频监控项目时,就是靠这个把组播流量绑定到独立的监控网卡上,避免影响业务网络。
4.4 广播 vs 组播:怎么选?
嗯,这里我直接给个对比表,你一看就明白:
| 对比项 | 广播 | 组播 |
|---|---|---|
| 目标范围 | 子网内所有设备 | 加入组的设备 |
| 网络负载 | 高(所有设备都要处理) | 低(只有组成员处理) |
| 跨网段 | 基本不行(路由器默认禁止) | 可以(需IGMP和组播路由) |
| 适用场景 | 局域网设备发现、DHCP | 视频会议、直播、数据同步 |
| 实现复杂度 | 简单 | 中等 |
说白了,小范围、临时性、所有设备都要收的场景用广播;大范围、持续性、只发给特定设备的场景用组播。
4.5 本章知识体系
下面这张图帮你理清广播和组播的核心脉络:
广播和组播,说白了就是一对多通信的两种姿势。广播简单粗暴,适合小范围;组播优雅高效,适合大规模。掌握了这两种方式,你的UDP工具箱就算基本齐活了。