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)用得最多。因为它简单粗暴,不需要知道自己的子网掩码,直接往这个地址发就行。

我的经验:在虚拟机或Docker环境里测试广播时,经常发现收不到包。原因往往是虚拟网卡没开启广播支持,或者防火墙把广播包过滤了。排查时先抓包看看,别一上来就怀疑代码。

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()}')
注意:广播会消耗大量网络资源。所有设备收到广播包后,都要上送到应用层去判断是否是自己需要的。我曾经在一个有500台设备的局域网里做广播测试,结果CPU飙升,网络延迟暴涨。所以广播只适合小规模网络,或者偶尔发一下。

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 支持源过滤,只收指定源的组播 部分老旧交换机不支持
避坑指南:我曾经在一个跨网段组播项目里折腾了两天,发现组播包就是过不去。最后查出来是中间交换机没开启IGMP Snooping。记住:组播要跨网段,必须保证沿途所有路由器都支持IGMP,并且开启了组播路由协议(如PIM)。

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广播与组播知识体系 一对多通信 广播 (Broadcast) 组播 (Multicast) 有限广播 直接广播 SO_BROADCAST 组播地址 IGMP协议 加入组 广播:子网内所有设备,简单但浪费资源 组播:仅组成员接收,高效且可跨网段

广播和组播,说白了就是一对多通信的两种姿势。广播简单粗暴,适合小范围;组播优雅高效,适合大规模。掌握了这两种方式,你的UDP工具箱就算基本齐活了。