1. 引言
在网络编程中,我们通常习惯的是「一对一」的通信模式:一台客户端连接一台服务器,数据在两者之间传递。这种模式被称为单播(Unicast)。然而,在很多实际场景下,我们需要把同一份数据同时发送给多个接收者,例如:
- 视频会议、直播推流;
- 局域网内的设备发现与自动配置;
- 股票行情、游戏房间状态同步;
- 传感器网络中批量下发指令。
如果采用单播逐台发送,不仅浪费带宽,还会增加发送端的负担。为此,TCP/IP 协议栈提供了两种高效的「一对多」通信方式:广播(Broadcast) 与 多播(Multicast)。
本文将围绕这两种方式展开,先厘清它们与单播的区别,再分别讲解 UDP 下广播与多播的编程模型,最后给出可运行的代码示例与常见坑点。
2. 单播、广播与多播
2.1 单播(Unicast)
单播是「一对一」通信。发送端把数据包发往一个明确的目的 IP 地址,只有该地址对应的主机才会处理这个包。TCP 与 UDP 都支持单播,这也是绝大多数网络应用的基础形态。
2.2 广播(Broadcast)
广播是「一对所有」通信。发送端把数据包发往广播地址,位于同一广播域(通常是同一子网)内的所有主机都会收到该数据包。
广播的特点:
- 仅限局域网内传播,路由器默认不会转发广播包;
- 使用 UDP 实现,TCP 不支持广播;
- 会对子网内所有主机造成处理开销,因此不宜频繁使用。
常见的广播地址是 255.255.255.255(受限广播),或子网定向广播地址,例如 192.168.1.255。
2.3 多播(Multicast)
多播是「一对一组」通信。发送端把数据包发往一个多播组地址,只有加入了该组的主机才会收到数据。
多播的特点:
- 可以跨越路由器,在广域网范围内传播;
- 使用 UDP 实现,TCP 不支持多播;
- 相比广播,多播只影响加入组的主机,效率更高、更可控。
IPv4 的多播地址范围是 224.0.0.0 ~ 239.255.255.255(D 类地址),其中 224.0.0.0 ~ 224.0.0.255 为本地网络控制块,通常不用于用户程序。
2.4 三者对比
| 特性 | 单播 | 广播 | 多播 |
|---|---|---|---|
| 通信对象 | 一对一 | 一对所有 | 一对一组 |
| 传输层协议 | TCP / UDP | 仅 UDP | 仅 UDP |
| 是否跨路由器 | 是 | 否 | 是 |
| 接收方开销 | 仅目标主机 | 子网内所有主机 | 仅组成员 |
| 典型场景 | Web、文件传输 | 局域网设备发现 | 直播、行情、组同步 |
3. 广播编程(UDP Broadcast)
广播基于 UDP 实现,发送端与接收端的编程模型与普通 UDP 几乎一致,区别主要在于目的地址与套接字选项。
3.1 发送端要点
发送广播数据前,必须设置套接字的 SO_BROADCAST 选项,否则发送广播包会返回错误。
c
int sock = socket(AF_INET, SOCK_DGRAM, 0);
int broadcast = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
目的地址填写广播地址即可:
c
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
inet_pton(AF_INET, "255.255.255.255", &addr.sin_addr);
3.2 接收端要点
接收端使用普通 UDP 绑定流程即可,无需额外选项:
c
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9000);
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
3.3 完整示例:广播发送端
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
int broadcast = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
inet_pton(AF_INET, "255.255.255.255", &addr.sin_addr);
const char *msg = "hello broadcast";
sendto(sock, msg, strlen(msg), 0,
(struct sockaddr *)&addr, sizeof(addr));
printf("broadcast sent: %s\n", msg);
close(sock);
return 0;
}
3.4 完整示例:广播接收端
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9000);
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sock, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n > 0) {
buf[n] = '\0';
printf("recv from %s: %s\n", inet_ntoa(peer.sin_addr), buf);
}
close(sock);
return 0;
}
4. 多播编程(UDP Multicast)
多播的编程模型比广播稍复杂:发送端需要设置多播 TTL,接收端需要加入多播组。
4.1 发送端要点
发送端把目的地址设为多播组地址,并可通过 IP_MULTICAST_TTL 设置数据包的生存时间(TTL),控制数据能跨越多少路由器:
c
int sock = socket(AF_INET, SOCK_DGRAM, 0);
unsigned char ttl = 1;
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9001);
inet_pton(AF_INET, "239.0.0.1", &addr.sin_addr);
4.2 接收端要点
接收端除了绑定端口,还必须通过 IP_ADD_MEMBERSHIP 加入多播组,否则收不到组内数据:
c
struct ip_mreq mreq;
memset(&mreq, 0, sizeof(mreq));
inet_pton(AF_INET, "239.0.0.1", &mreq.imr_multiaddr);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
4.3 完整示例:多播发送端
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
unsigned char ttl = 1;
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9001);
inet_pton(AF_INET, "239.0.0.1", &addr.sin_addr);
const char *msg = "hello multicast";
sendto(sock, msg, strlen(msg), 0,
(struct sockaddr *)&addr, sizeof(addr));
printf("multicast sent: %s\n", msg);
close(sock);
return 0;
}
4.4 完整示例:多播接收端
c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(9001);
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
struct ip_mreq mreq;
memset(&mreq, 0, sizeof(mreq));
inet_pton(AF_INET, "239.0.0.1", &mreq.imr_multiaddr);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sock, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n > 0) {
buf[n] = '\0';
printf("recv from %s: %s\n", inet_ntoa(peer.sin_addr), buf);
}
close(sock);
return 0;
}
5. 常见问题与注意事项
5.1 广播收不到数据
- 确认发送端已设置
SO_BROADCAST; - 确认发送端与接收端位于同一子网;
- 检查防火墙是否拦截了对应端口的 UDP 包。
5.2 多播收不到数据
- 确认接收端已调用
IP_ADD_MEMBERSHIP加入组; - 确认发送端与接收端使用相同的多播组地址与端口;
- 跨路由器时,确认发送端 TTL 足够大,且沿途路由器启用了多播路由协议(如 PIM)。
5.3 广播与多播都不支持 TCP
TCP 是面向连接的可靠传输,无法表达「一对多」的语义。广播与多播只能基于 UDP 实现,因此也继承了 UDP 不保证可靠、不保证有序的特点。若业务需要可靠性,应在应用层自行实现确认与重传。
5.4 多播地址的选择
用户程序应避免使用 224.0.0.0 ~ 224.0.0.255 这段本地网络控制块地址,推荐使用 239.0.0.0 ~ 239.255.255.255 范围内的地址,该段为本地管理多播地址,适合内部网络使用。
6. 总结
广播与多播是 TCP/IP 网络编程中实现「一对多」通信的两种重要手段:
- 广播实现简单,但只限于局域网,且会打扰子网内所有主机;
- 多播更高效、可控,能够跨路由器传播,但需要接收端主动加入多播组。
在实际开发中,应根据业务范围与网络环境选择合适的方案:局域网内的设备发现可以优先考虑广播,而需要跨网段、面向特定群体的数据分发则应使用多播。