一、为什么需要 UDP?
TCP 提供可靠的、面向连接的传输服务,但代价是:
- 需要三次握手建立连接
- 需要四次挥手断开连接
- 需要维护连接状态(每个客户端一个套接字)
- 有流量控制、拥塞控制等复杂机制
这就好比打电话:必须先拨号、等待对方接听、才能说话,说完还要挂断。
UDP 则像是寄明信片:写好地址,直接扔进邮筒,不保证对方一定能收到,也不需要提前建立连接。它简单、快速,适合对实时性要求高、对可靠性要求不高的场景(如视频直播、DNS查询)。
二、UDP 的核心特点
| 特性 | 说明 |
|---|---|
| 无连接 | 无需三次握手,直接发送数据 |
| 无消息边界 | 每个 sendto 发出的数据包是独立的消息,接收方用 recvfrom 一次收一个完整包(但可能丢包) |
| 一对多通信 | 一个 UDP 套接字可以给多个不同地址发送数据 |
| 简单 | 不需要 listen、accept,只需 socket + bind(可选) |
| 不可靠 | 可能丢包、乱序、重复,没有重传机制 |
三、UDP 编程核心函数
UDP 使用 sendto 和 recvfrom 函数发送和接收数据,每次调用都需要指定对方的地址。
3.1 sendto -- 发送数据
c
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
UDP 套接字描述符 |
buf |
要发送的数据缓冲区 |
len |
要发送的字节数 |
flags |
通常为 0 |
dest_addr |
目标主机的地址信息(IP + 端口) |
addrlen |
dest_addr 结构体的大小 |
返回值:成功返回发送的字节数,失败返回 -1。
3.2 recvfrom -- 接收数据
c
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
UDP 套接字描述符 |
buf |
接收缓冲区 |
len |
缓冲区大小 |
flags |
通常为 0 |
src_addr |
用于返回发送方的地址信息(IP + 端口) |
addrlen |
传入传出参数,指定 src_addr 的大小,并返回实际地址长度 |
返回值:成功返回接收的字节数,失败返回 -1。
四、UDP 回声服务器实现
4.1 服务器端代码(udp_echo_server.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock; // UDP 套接字
char message[BUF_SIZE]; // 数据缓冲区
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 1. 创建 UDP 套接字(注意 SOCK_DGRAM)
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
error_handling("UDP socket creation error");
// 2. 绑定地址(服务器必须绑定,否则客户端不知道往哪发)
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 本机任意 IP
serv_adr.sin_port = htons(atoi(argv[1])); // 指定端口
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
// 3. 循环接收并回传数据
while (1) {
clnt_adr_sz = sizeof(clnt_adr);
// 接收数据,同时获取发送方的地址
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
// 将收到的数据原样发回给发送方
sendto(serv_sock, message, str_len, 0,
(struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
关键点:
- 创建套接字时用
SOCK_DGRAM表示 UDP。 - 服务器必须
bind到固定端口,否则客户端无法找到它。 - 用
recvfrom接收数据,同时获得客户端的地址。 - 用
sendto将数据原路返回,目标地址就是刚才得到的客户端地址。
4.2 客户端代码(udp_echo_client.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 1. 创建 UDP 套接字
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
// 2. 初始化服务器地址
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器 IP
serv_adr.sin_port = htons(atoi(argv[2])); // 服务器端口
while (1) {
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
// 发送数据到服务器(每次都要指定服务器地址)
sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&serv_adr, sizeof(serv_adr));
// 接收回声(同时获取发送方地址,这里就是服务器地址)
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&from_adr, &adr_sz);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
关键点:
- 客户端不需要
bind,第一次调用sendto时系统会自动分配一个随机端口。 - 每次发送都需要指定服务器地址。
- 接收时也用
recvfrom,可以拿到发送方地址(用于验证是否是服务器发回的)。
五、UDP 地址分配的秘密
5.1 为什么客户端不 bind 也能工作?
TCP 客户端通过 connect 隐式绑定了本地地址。UDP 客户端调用 sendto 时,如果套接字尚未绑定地址,操作系统会自动为该套接字分配一个临时端口和本地 IP。这个地址一直保留到套接字关闭。
所以,UDP 客户端通常不需要显式调用 bind,除非你想固定使用某个端口。
5.2 什么时候需要显式 bind?
- 服务器:必须绑定知名端口,客户端才能找到。
- 客户端 :如果需要固定端口(如某些防火墙规则),可以主动
bind。 - 多网卡场景:如果希望数据从特定网卡发送,可以绑定到特定 IP。
六、UDP 的特性与注意事项
6.1 UDP 的数据包边界
UDP 是面向数据报 的协议,每个 sendto 发出的数据包,接收方用一个 recvfrom 就能完整接收(前提是缓冲区足够大)。如果接收缓冲区小于数据包大小,多余的数据会被丢弃。
因此,在 UDP 编程中,你需要保证发送的数据包大小不超过接收方的缓冲区,通常建议小于 1500 字节(以太网 MTU 限制)。
6.2 UDP 的不可靠性
UDP 不保证数据包能到达,也不保证顺序。如果网络发生拥塞,可能丢包;如果发送速度过快,接收方来不及处理,也可能丢包。应用层需要自己处理这些情况。
6.3 一对多通信
一个 UDP 套接字可以给多个不同地址发送数据,也可以接收来自任意地址的数据。这非常适合广播、多播等场景。
七、拓展:如何实现可靠的 UDP?
虽然 UDP 本身不可靠,但可以在应用层增加可靠性机制,例如:
- 确认机制:发送方等待接收方的 ACK,超时则重传。
- 序号:给每个数据包编号,接收方检测丢包和乱序。
- 滑动窗口:流量控制,避免发送过快。
著名的可靠 UDP 协议有:RUDP、UDT、QUIC(HTTP/3 使用)。