前言:
承接上一篇 Reactor 事件驱动架构,至此我们已经完整掌握了 TCP 协议的编程模型、并发架构与高性能优化。传输层两大核心协议中,TCP 负责可靠有序传输,UDP 则主打低延迟、高效率,在音视频直播、实时游戏、DNS 解析、物联网等场景下有着不可替代的地位。本篇完整讲解 UDP Socket 的编程模型、核心 API 与实战实现,拓展广播、组播两种一对多通信方式,并分析 UDP 可靠性增强的工程实现思路,补全传输层编程的最后一块核心拼图。
一、UDP 协议核心特性
1. UDP 的本质
UDP(用户数据报协议)是传输层的无连接协议,它不提供可靠传输保证,只负责将数据报尽可能发送出去。和 TCP 面向字节流的设计完全不同,UDP 是面向报文的协议,每个数据报都是独立的,有明确的消息边界。
2. 五大核心特性
- 无连接:不需要三次握手建立连接,创建 Socket 后可以直接向任意目标发送数据,启动速度快
- 不可靠传输:不保证数据一定送达,不保证顺序,不做重传,出错直接丢弃
- 面向报文:一次发送对应一个数据报,接收方一次完整接收一个报文,不存在粘包问题
- 无拥塞控制:不会根据网络状况调整发送速率,始终以恒定速率发送,适合实时场景
- 头部开销极小:UDP 头部只有 8 字节,远小于 TCP 的 20 字节以上头部,传输效率高
3. 为什么需要「不可靠」的 UDP
可靠性和实时性是一对矛盾体:TCP 为了保证可靠,引入了握手、重传、拥塞控制等机制,带来了延迟和开销;而 UDP 舍弃了可靠性,换来了极低的延迟和极高的传输效率。
在直播、语音通话、在线游戏等场景中,宁可丢失少量数据包,也不能容忍高延迟,UDP 就是这类场景的最优选择。
二、UDP Socket 编程流程
UDP 没有连接的概念,编程流程比 TCP 简单得多,服务端和客户端的差异非常小。
1. 服务端编程流程
- 创建套接字:调用
socket()指定SOCK_DGRAM类型 - 绑定地址:调用
bind()绑定固定的 IP 和端口 - 循环收发:调用
recvfrom()接收数据,调用sendto()发送数据 - 关闭套接字:通信结束调用
close()
2. 客户端编程流程
- 创建套接字:调用
socket()创建 UDP 套接字 - 直接收发:无需绑定,直接调用
sendto()向服务端发送数据,调用recvfrom()接收回复 - 关闭套接字
核心差异:UDP 没有
listen、accept、connect这三步连接相关操作,没有监听套接字和通信套接字的区分,一个 Socket 可以和多个对端通信。
三、核心 API 函数详解
1. 创建 UDP 套接字
#include <sys/socket.h>
int socket(AF_INET, SOCK_DGRAM, 0);
- 第二个参数固定为
SOCK_DGRAM,表示数据报套接字,对应 UDP 协议 - 返回值为 Socket 文件描述符,失败返回 - 1
2. 绑定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 用法和 TCP 完全一致,服务端必须绑定固定端口才能接收数据
- 客户端通常不需要绑定,系统会自动分配临时端口
3. 接收数据:recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
buf:接收数据的缓冲区flags:接收标志,填 0 为常规阻塞接收src_addr:传出参数,保存发送方的 IP 和端口信息addrlen:值 - 结果参数,传入时为地址结构体大小,传出时为实际地址长度- 返回值:成功返回实际接收的字节数,失败返回 - 1,对端关闭返回 0(UDP 一般不会收到 0)
易错点:
addrlen必须先初始化为地址结构体的大小,否则会导致地址信息读取错误。
4. 发送数据:sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
dest_addr:目标地址结构体,指定要发送给谁addrlen:目标地址结构体的长度- 返回值:成功返回实际发送的字节数,失败返回 - 1
注意:UDP 的 sendto 成功返回只代表数据成功拷贝到内核发送缓冲区,不代表数据已经成功发出,更不代表对方已经收到。
5. UDP 的 connect 操作
UDP 虽然是无连接协议,但也可以调用connect函数,作用是绑定一个固定的对端地址:
- 绑定后可以直接用
read/write/send/recv收发数据,不需要每次都传地址参数 - 内核会自动过滤掉其他地址发来的数据包,只接收绑定地址的数据
- 这种模式称为「已连接 UDP」,比普通 UDP 效率略高,适合一对一固定通信的场景
四、实战:UDP 回显服务
实现 UDP 版的回显服务:客户端发送字符串,服务端收到后原样返回。
1. UDP 服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int main(void) {
// 创建UDP套接字
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("socket failed");
return 1;
}
// 绑定地址端口
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind failed");
return 1;
}
printf("UDP服务端启动,监听端口%d\n", PORT);
char buf[BUF_SIZE];
struct sockaddr_in cli_addr;
socklen_t cli_len;
while (1) {
cli_len = sizeof(cli_addr); // 每次接收前必须重置长度
memset(buf, 0, sizeof(buf));
// 接收数据,同时获取客户端地址
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
(struct sockaddr *)&cli_addr, &cli_len);
if (n == -1) {
perror("recvfrom failed");
break;
}
char cli_ip[16];
inet_ntop(AF_INET, &cli_addr.sin_addr, cli_ip, sizeof(cli_ip));
printf("收到 %s:%d 数据:%.*s\n", cli_ip, ntohs(cli_addr.sin_port), (int)n, buf);
// 原样回写给客户端
sendto(fd, buf, n, 0, (struct sockaddr *)&cli_addr, cli_len);
}
close(fd);
return 0;
}
2. UDP 客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int main(void) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("socket failed");
return 1;
}
// 构造服务端地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
char buf[BUF_SIZE];
socklen_t serv_len = sizeof(serv_addr);
while (1) {
printf("请输入要发送的内容:");
fgets(buf, sizeof(buf), stdin);
buf[strcspn(buf, "\n")] = 0;
if (strcmp(buf, "quit") == 0) break;
// 发送数据到服务端
sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&serv_addr, serv_len);
// 接收回显
memset(buf, 0, sizeof(buf));
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
if (n > 0) {
printf("收到回显:%.*s\n", (int)n, buf);
}
}
close(fd);
return 0;
}
五、UDP 广播通信
1. 什么是广播
广播是一对多的通信方式,发送方发送一个广播包,同一个局域网内的所有主机都能收到这个数据包。广播只能在局域网内使用,路由器不会转发广播数据包。
- 受限广播地址 :
255.255.255.255,向所有本地网络广播,不被路由器转发 - 直接广播地址 :网段的最后一个地址,如
192.168.1.255,向指定网段广播
2. 核心配置选项
发送广播前必须开启 Socket 的SO_BROADCAST选项,否则发送会失败:
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
3. 广播适用场景
- 局域网设备发现:如打印机发现、设备在线探测
- 局域网游戏、局域网文件传输
- 服务发现协议:如 DHCP 就是基于 UDP 广播实现的
4. 广播的局限性
- 只能在局域网内使用,无法跨网段
- 所有主机都会收到广播包,即使不需要处理,浪费带宽和主机资源
- 安全性差,容易被滥用造成广播风暴
六、UDP 组播(多播)通信
1. 什么是组播
组播也叫多播,是介于单播和广播之间的一对多通信方式:主机可以加入一个指定的组播组,只有加入该组的主机才能收到对应组播数据。组播可以跨网段路由,是广域网一对多传输的标准方案。
2. 组播地址
组播使用 D 类 IP 地址,范围是224.0.0.0 ~ 239.255.255.255,分为三类:
- 局部链路组播:
224.0.0.0 ~ 224.0.0.255,只在本地网段有效,路由器不转发 - 全局组播:
224.0.1.0 ~ 238.255.255.255,可在全网范围内使用 - 管理权限组播:
239.0.0.0 ~ 239.255.255.255,私有组播地址,类似内网 IP
3. 核心操作:加入 / 离开组播组
接收方必须加入指定组播组,才能收到对应组播地址的数据,通过setsockopt实现:
#include <netinet/in.h>
struct ip_mreq mreq;
// 组播地址
inet_pton(AF_INET, "239.0.0.1", &mreq.imr_multiaddr);
// 本机加入组播的网卡地址,INADDR_ANY表示任意网卡
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
// 加入组播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
// 离开组播组
setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
4. 组播的优势
- 比广播更高效,只有需要的主机会收到数据,带宽利用率高
- 支持跨网段、跨广域网传输
- 可同时支持大量接收者,适合直播、视频会议等大规模一对多场景
七、UDP 可靠性增强方案
UDP 本身不可靠,但在很多场景中,我们既需要 UDP 的低延迟,又需要一定的传输可靠性,这时就需要在应用层自己实现可靠传输机制。
1. 四大核心可靠机制
| 机制 | 作用 | 实现原理 |
|---|---|---|
| 序号机制 | 保证数据顺序、去重 | 每个数据包分配唯一序号,接收方按序号排序,重复包丢弃 |
| 确认机制 | 感知数据是否送达 | 接收方收到数据包后,回复 ACK 确认包 |
| 超时重传 | 丢失的数据包重发 | 发送方启动定时器,超时未收到 ACK 则重传对应数据包 |
| 滑动窗口 | 流量控制 + 提升吞吐 | 维护发送窗口,连续发送多个包再等待确认,提升传输效率 |
在此基础上还可以扩展拥塞控制、快速重传、选择确认等进阶机制,逐步逼近 TCP 的可靠性,同时保留 UDP 的低延迟特性。
2. 工业级可靠 UDP 实现
实际开发中不需要重复造轮子,主流的可靠 UDP 实现有:
- KCP:轻量级可靠 UDP 协议,专注于低延迟,广泛用于游戏、物联网
- QUIC:Google 推出的基于 UDP 的可靠传输协议,已成为 HTTP/3 的底层标准
- RTP/RTCP:音视频传输标准协议,RTP 传数据,RTCP 做质量监控
- ENet:游戏开发常用的可靠 UDP 网络库
3. 选型原则
如果业务对可靠性要求高、延迟不敏感,直接用 TCP;如果对延迟要求极高、可以容忍少量丢包,用原生 UDP;如果需要低延迟 + 基本可靠,选择成熟的可靠 UDP 方案。
八、TCP 与 UDP 选型指南
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接特性 | 面向连接 | 无连接 |
| 可靠性 | 可靠,保证不丢、不乱、不重复 | 不可靠,不保证送达和顺序 |
| 传输形式 | 字节流,无消息边界 | 数据报,有明确消息边界 |
| 头部开销 | 20~60 字节 | 8 字节 |
| 延迟 | 高,握手 + 重传 + 拥塞控制 | 极低,无额外开销 |
| 流量 / 拥塞控制 | 有 | 无 |
| 编程复杂度 | 高,连接管理、粘包处理 | 低,简单直接 |
| 适用场景 | 文件传输、网页、支付等可靠优先场景 | 直播、语音、游戏、DNS 等实时优先场景 |
九、面试高频考点与易错坑点
1. 经典面试问答
Q1:UDP 有没有粘包问题?为什么?
答: UDP 没有粘包问题。因为 UDP 是面向数据报的协议,每个数据报是独立的,有明确的消息边界。内核一次交付一个完整的数据报,发送方一次 sendto 对应接收方一次 recvfrom,不会出现多个包合并或者拆分的情况。
Q2:UDP 调用 connect 有什么作用?和 TCP 的 connect 有什么区别?
答: UDP 的 connect 不是建立连接,只是绑定一个固定的对端地址。绑定后可以用 read/write 收发数据,不需要每次传地址,同时内核会过滤其他地址的数据包。 和 TCP 的区别:TCP 的 connect 会发起三次握手建立真实连接;UDP 的 connect 只是在内核记录对端地址,没有任何握手过程。
Q3:广播和组播有什么核心区别?
答: 广播是向网段内所有主机发送,所有主机无论是否需要都会收到,只能在局域网使用,资源浪费大。 组播是向加入组的主机发送,只有需要的主机会收到,支持跨网段传输,带宽利用率高,适合大规模一对多场景。
Q4:怎么实现可靠的 UDP 传输?
答: 在应用层实现可靠机制:给数据包加序号实现排序去重,接收方回复 ACK 确认,发送方超时重传丢失的包,通过滑动窗口提升吞吐量。 工业界常用 KCP、QUIC 等成熟方案,不需要自行从零实现。
Q5:UDP 的 sendto 会阻塞吗?
答: 默认阻塞模式下会阻塞。当内核发送缓冲区满了的时候,sendto 会阻塞等待缓冲区有空位,不是完全不会阻塞。设置非阻塞模式后,缓冲区满会直接返回 - 1,errno 设为 EAGAIN。
2. 常见易错坑点
- 忘记初始化
recvfrom的 addrlen 参数,导致获取不到正确的对端地址 - 误以为 UDP 不会丢包,业务逻辑没有做容错处理
- 发送广播包前忘记开启
SO_BROADCAST选项,导致发送失败 - 发送的数据包超过 MTU 大小,导致 IP 分片,只要一个分片丢失整个包就作废,性能大幅下降
- 接收缓冲区设置太小,数据包超过缓冲区大小被内核直接截断丢弃
- 混淆 TCP 和 UDP 的编程模型,在 UDP 中调用 listen/accept 导致错误
- 组播接收端没有加入对应组播组,导致收不到数据
以上就是 UDP Socket 编程的全部核心内容,至此传输层两大协议的编程实践已经完整覆盖。下一篇我们将讲解 Unix 域套接字,这是本机最高效的进程间通信方式,基于 Socket 接口实现,性能远超管道、消息队列等传统 IPC,是本地服务通信的首选方案。
制作不易,如果对你有用,希望能点赞收藏支持一下。