UDP协议与数据报套接字编程
本文介绍 UDP(User Datagram Protocol,用户数据报协议)的编程方法,分析其与 TCP 协议的核心差异,并通过实例演示 UDP 数据报套接字(Datagram Socket)的使用方式。
UDP 与 TCP 协议对比
协议特性差异
UDP 是一种无连接的数据报协议,而 TCP 是一种面向连接的字节流协议。两者在传输特性上存在本质区别:
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接 | 无连接 |
| 传输单元 | 字节流(Byte Stream) | 数据报(Datagram) |
| 可靠性 | 保证数据可靠传输 | 不保证数据到达 |
| 有序性 | 保证数据顺序 | 不保证数据顺序 |
| 流量控制 | 具备滑动窗口机制 | 无流量控制 |
| 拥塞控制 | 具备拥塞控制算法 | 无拥塞控制 |
| 首部开销 | 20 字节 | 8 字节 |
| 上下文 | 维护连接状态 | 无连接状态 |
通信模型对比
TCP 通信类似于电话通话模式:双方建立连接后,在确定的上下文中进行双向数据传输,通信过程具有明确的开始、持续和结束阶段。
UDP 通信类似于明信片投递模式:发送方将数据报文投递后即可离开,每个数据报文相互独立,不保证到达顺序,也不保证可靠送达。接收方收到报文时无法确定其发送顺序,且无法请求重传丢失的报文。
UDP 的应用场景
尽管 UDP 缺乏可靠性保障,但其简洁高效的特性使其适用于以下场景:
- DNS(Domain Name System)服务:域名解析对时延敏感,单次请求-响应模式无需建立连接
- SNMP(Simple Network Management Protocol)服务:网络管理协议,允许少量丢包
- 实时音视频传输:视频会议、直播等场景,优先保证实时性而非完整性
- 多人在线游戏:游戏状态同步,允许少量状态丢失
- IoT 设备通信:资源受限设备,UDP 开销更小
UDP 套接字编程接口
核心函数
UDP 编程使用 recvfrom() 和 sendto() 函数进行数据报的接收与发送:
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen);内核版本说明:上述接口为 POSIX 标准定义,Linux 内核自 2.6 版本起完全支持,在 Linux 7.0 中保持稳定兼容。
recvfrom 函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
| sockfd | int | 本地套接字描述符 |
| buff | void* | 接收缓冲区指针 |
| nbytes | size_t | 最大接收字节数 |
| flags | int | I/O 操作标志,默认为 0 |
| from | struct sockaddr* | 输出参数,返回发送方地址 |
| addrlen | socklen_t* | 输入输出参数,地址结构长度 |
| 返回值 | ssize_t | 实际接收字节数,-1 表示错误 |
与 TCP 不同,UDP 每次调用 recvfrom() 都会获取对端地址信息,体现了数据报的无上下文特性。
sendto 函数参数
| 参数 | 类型 | 说明 |
|---|---|---|
| sockfd | int | 本地套接字描述符 |
| buff | const void* | 发送缓冲区指针 |
| nbytes | size_t | 发送字节数 |
| flags | int | I/O 操作标志,默认为 0 |
| to | const struct sockaddr* | 目标地址结构 |
| addrlen | socklen_t | 地址结构长度 |
| 返回值 | ssize_t | 实际发送字节数,-1 表示错误 |
UDP 通信流程
UDP 服务器端实现
以下代码实现一个简单的 UDP 回显服务器:
#include "lib/common.h"
static int count;
static void recvfrom_int(int signo) {
printf("\nreceived %d datagrams\n", count);
exit(0);
}
int main(int argc, char **argv) {
int socket_fd;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
socklen_t client_len;
char message[MAXLINE];
count = 0;
signal(SIGINT, recvfrom_int);
struct sockaddr_in client_addr;
client_len = sizeof(client_addr);
for (;;) {
int n = recvfrom(socket_fd, message, MAXLINE, 0,
(struct sockaddr *) &client_addr, &client_len);
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
sendto(socket_fd, send_line, strlen(send_line), 0,
(struct sockaddr *) &client_addr, client_len);
count++;
}
}代码要点说明:
- 套接字类型:使用
SOCK_DGRAM创建数据报套接字 - 地址绑定:与 TCP 类似,调用
bind()绑定本地端口 - 无监听过程:UDP 不需要
listen()和accept(),直接接收数据报 - 信号处理:注册
SIGINT处理函数,统计接收的数据报总数
UDP 客户端实现
以下代码实现对应的 UDP 客户端:
#include "lib/common.h"
#define MAXLINE 4096
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: udpclient <IPaddress>");
}
int socket_fd;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
struct sockaddr *reply_addr;
reply_addr = malloc(server_len);
char send_line[MAXLINE], recv_line[MAXLINE + 1];
socklen_t len;
int n;
while (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
printf("now sending %s\n", send_line);
size_t rt = sendto(socket_fd, send_line, strlen(send_line), 0,
(struct sockaddr *) &server_addr, server_len);
if (rt < 0) {
error(1, errno, "send failed ");
}
printf("send bytes: %zu \n", rt);
len = 0;
n = recvfrom(socket_fd, recv_line, MAXLINE, 0, reply_addr, &len);
if (n < 0)
error(1, errno, "recvfrom failed");
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
exit(0);
}代码要点说明:
- 套接字创建:使用
SOCK_DGRAM类型 - 目标地址初始化:设置服务器 IP 地址和端口
- 无连接过程:直接调用
sendto()发送数据,无需connect() - 请求-响应模式:发送后等待接收服务器响应
运行场景分析
场景一:仅启动客户端
当服务器端未启动时,客户端发送数据报后将阻塞在 recvfrom() 调用:
$ ./udpclient 127.0.0.1
1
now sending g1
send bytes: 2
<阻塞在此处>与 TCP 的差异:TCP 客户端在服务器未启动时,connect() 会立即返回 "Connection refused" 错误;而 UDP 由于无连接特性,客户端无法感知服务器状态,只能等待响应。
场景二:正常启动服务器与客户端
服务器端输出:
$ ./udpserver
received 2 bytes: g1
received 2 bytes: g2客户端输出:
$ ./udpclient 127.0.0.1
g1
now sending g1
send bytes: 2
Hi, g1
g2
now sending g2
send bytes: 2
Hi, g2场景三:多客户端并发访问
UDP 服务器可同时处理多个客户端请求,无需为每个客户端创建独立连接:
服务器端输出:
$ ./udpserver
received 2 bytes: g1
received 2 bytes: g2
received 2 bytes: g3
received 2 bytes: g4
^C
received 4 datagrams服务器重启后的行为:UDP 服务器重启后可立即接收新报文,无需重新建立连接。这体现了 UDP 的无状态特性——每个数据报独立处理,不依赖历史上下文。
关键技术要点
1. 无连接特性
UDP 不需要三次握手建立连接,服务器端无需调用 listen() 和 accept()。每个数据报独立传输,不维护连接状态。
2. 数据报边界保留
UDP 保留消息边界,一次 sendto() 对应一次 recvfrom()。这与 TCP 的字节流特性形成对比——TCP 可能出现粘包/拆包问题。
3. 阻塞与非阻塞
默认情况下,recvfrom() 为阻塞模式。可通过以下方式设置非阻塞:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);内核版本说明:
O_NONBLOCK标志在 Linux 2.6 及以上版本稳定支持,Linux 7.0 中行为一致。
4. 数据报大小限制
UDP 数据报最大大小受 MTU(Maximum Transmission Unit)限制。以太网 MTU 通常为 1500 字节,扣除 IP 头部(20 字节)和 UDP 头部(8 字节),有效载荷最大为 1472 字节。超过 MTU 的数据报将被 IP 层分片,增加丢包风险。
总结
本文介绍了 UDP 协议的核心特性与编程方法,要点如下:
- UDP 是无连接的数据报协议,不保证可靠性、有序性和流量控制
- UDP 编程使用
recvfrom()和sendto()函数,无需建立连接 - UDP 适用于对实时性要求高、可容忍少量丢包的场景
- UDP 服务器重启后可立即接收新报文,体现了无状态特性
思考题
- 在场景一中,
recvfrom()长期阻塞不合理,应如何优化处理? - UDP 请求-响应模式中,数据报的最大有效载荷是多少?如何确定最优大小?
版本信息
- 更新日期:2026-06-09
- 目标内核:Linux 7.0
- API 兼容性:POSIX.1-2008, SUSv4