2. UDP通信模型:C/S架构详解、P2P架构详解、单播/广播/组播概念、端口与套接字基础
这一章,我们来聊聊UDP的通信模型。说白了,就是搞清楚数据到底是怎么从A点跑到B点的。很多新手一上来就写代码,结果部署到线上就出问题——其实根子往往是对通信模型的理解不够深。
我个人习惯把通信模型分成两类来看:一种是有中心的,一种是去中心的。前者就是C/S架构,后者就是P2P。另外,数据怎么发出去,是发给一个人、一群人、还是所有人?这就涉及单播、组播、广播的概念。最后,端口和套接字是这一切的基础设施。咱们一个一个来拆。
2.1 C/S架构:最经典的主从模式
C/S架构,就是客户端-服务器模式。服务器一直开着,等着客户端来连。客户端主动发起请求,服务器被动响应。UDP下的C/S和TCP不太一样——UDP服务器不需要accept,也不需要维护连接状态。
我在项目中遇到过好几次,有人把TCP那套思维直接套到UDP上,结果服务器写成了多线程每来一个包就fork一个进程。其实没必要。UDP服务器通常就是一个recvfrom循环,谁发来的包,我就处理完再sendto回去。
UDP C/S的基本流程
- 服务器端:创建套接字 → bind绑定端口 → 进入recvfrom循环
- 客户端:创建套接字 → sendto发送数据 → recvfrom接收响应(可选)
// 服务器端核心代码(C语言风格)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8888);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[1024];
while(1) {
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&client_addr, &addr_len);
// 处理数据...
sendto(sockfd, response, resp_len, 0,
(struct sockaddr*)&client_addr, addr_len);
}
C/S架构的优缺点
| 维度 | 说明 |
|---|---|
| 优点 | 结构简单,管理集中,安全性好控制 |
| 缺点 | 服务器是单点瓶颈,高并发时压力大 |
| 适用场景 | DNS查询、NTP时间同步、日志收集 |
2.2 P2P架构:去中心化的对等通信
P2P架构,每个节点既是客户端也是服务器。没有中心节点,大家平等通信。UDP在P2P里用得特别多,原因很简单——UDP没有连接状态,适合做NAT穿透。
你想想看,如果两个设备都在内网,它们怎么直接通信?TCP需要三次握手,中间有防火墙拦着,很难打通。UDP就不一样了,配合STUN/TURN协议,打洞成功率很高。我记得有一次做视频会议项目,TCP打洞死活不行,换成UDP+ICE框架,一下子就通了。
UDP P2P的典型流程
- 每个节点启动后,先向信令服务器注册自己的公网地址和端口
- 信令服务器交换双方的地址信息
- 双方互相发送UDP探测包,完成NAT穿透
- 建立直接通信通道,后续数据不经过服务器
2.3 单播、广播、组播:三种发送方式
数据发出去,到底谁能收到?这就涉及发送方式的选择。我习惯用一个比喻来理解:
- 单播:一对一私聊。你给一个人发消息,只有他能看到。
- 广播:在群里@所有人。局域网内所有设备都能收到。
- 组播:拉个群聊。只有加入这个群组的人能收到。
单播(Unicast)
最常用的方式。每个UDP包的目标地址是一个具体的IP:端口。一对一通信,简单直接。缺点也很明显——如果给1000个客户端发同样的数据,你得发1000次,带宽浪费严重。
广播(Broadcast)
目标地址是255.255.255.255或者子网广播地址。局域网内所有设备都会收到这个包。注意:广播包不会被路由器转发,所以只能在同一个广播域内使用。
组播(Multicast)
组播介于单播和广播之间。发送方把数据发到一个组播地址(224.0.0.0 ~ 239.255.255.255),只有加入了这个组播组的设备才能收到。IGMP协议负责管理组播组的加入和离开。
// 加入组播组的代码片段
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.1.1.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
| 特性 | 单播 | 广播 | 组播 |
|---|---|---|---|
| 通信范围 | 一对一 | 一对所有 | 一对多(指定组) |
| 带宽效率 | 低(重复发送) | 高(一次发送) | 高(一次发送) |
| 跨网段 | 支持 | 不支持 | 需要路由器支持 |
| 典型应用 | DNS、NTP | DHCP、ARP | 视频会议、直播 |
2.4 端口与套接字基础
端口和套接字,是UDP通信的基石。没有它们,数据包就不知道往哪儿送。
端口号
端口号是一个16位的整数,范围0~65535。其中:
- 0~1023:知名端口,需要root权限才能绑定。比如DNS用53,DHCP用67/68。
- 1024~49151:注册端口,一般应用程序用。
- 49152~65535:动态/私有端口,客户端临时使用。
套接字(Socket)
套接字是操作系统提供的网络编程接口。对于UDP来说,套接字就是一个文件描述符,通过它来收发数据。
UDP套接字有几个关键属性:
- 协议族:AF_INET(IPv4)或AF_INET6(IPv6)
- 类型:SOCK_DGRAM(数据报套接字)
- 本地地址:IP+端口,通过bind绑定
- 缓冲区:发送缓冲区和接收缓冲区,大小可调
// 创建UDP套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
// 设置接收缓冲区大小(防止丢包)
int recv_buf_size = 256 * 1024; // 256KB
setsockopt(sock, SOL_SOCKET, SO_RCVBUF,
&recv_buf_size, sizeof(recv_buf_size));
// 绑定端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8888);
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
2.5 本章知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个思维导图来看。
这张图把本章的知识点分成了三大块:通信架构、发送方式和基础设施。你在实际项目中做技术选型时,可以对照这张图来思考——先确定用C/S还是P2P,再决定数据怎么发,最后把端口和套接字配置好。嗯,思路就清晰了。