11、UDP通信基础:UDP服务端与客户端实现,sendto()与recvfrom()
聊到UDP,很多刚入门的朋友第一反应就是“不可靠”。没错,UDP确实不保证数据一定到达,也不保证顺序。但我想说——不可靠不代表没用。
我在做音视频传输项目时,底层几乎全是UDP。为什么?因为实时性要求高,丢一两个包无所谓,但延迟绝对不能大。你想想看,视频通话如果卡一下,体验就很糟糕了。所以UDP在流媒体、游戏、DNS查询这些场景里,反而是主力。
这一章,我们就来手写一个UDP服务端和客户端。核心就两个函数:sendto() 和 recvfrom()。说白了,UDP比TCP简单太多了——没有三次握手,没有连接维护,直接发就完事了。
UDP的核心特点
先快速过一下UDP的几个关键点,后面写代码时你会更有感觉:
- 无连接:不需要像TCP那样调用
connect(),直接发数据。 - 不可靠:数据可能丢、可能乱序、可能重复到达。
- 面向报文:每次
sendto()发送一个完整报文,接收方recvfrom()一次读一个报文。不会像TCP那样粘包。 - 支持广播和多播:这是TCP做不到的。
核心区别一句话:TCP像打电话——先接通,再说话,挂断结束。UDP像寄快递——写好地址扔出去,不管对方收没收到。
UDP通信流程图
先看一张图,把整体流程刻在脑子里。我个人习惯是先画图再写代码,这样不容易漏步骤。
看到没?服务端和客户端都只需要 socket() 创建套接字,然后直接收发数据。服务端多一个 bind() 绑定端口,客户端甚至可以不绑定——让系统随机分配一个端口就行。
sendto() 和 recvfrom() 函数详解
这两个函数是UDP编程的核心。我当年第一次用的时候,被它们的参数搞得有点晕。这里拆开讲清楚。
sendto() 函数原型
#include <sys/types.h>
#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 | 套接字描述符 |
| buf | 要发送的数据缓冲区 |
| len | 数据长度(字节) |
| flags | 一般填0,特殊场景用MSG_DONTWAIT等 |
| dest_addr | 目标地址结构体(IP+端口) |
| addrlen | 地址结构体长度 |
小技巧:flags 参数平时填0就行。但如果你做高并发服务器,可以用 MSG_DONTWAIT 实现非阻塞发送,避免卡住。
recvfrom() 函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
| 参数 | 说明 |
|---|---|
| sockfd | 套接字描述符 |
| buf | 接收数据缓冲区 |
| len | 缓冲区最大长度 |
| flags | 一般填0 |
| src_addr | 发送方的地址结构体(函数返回时填充) |
| addrlen | 传入地址结构体大小,返回实际大小 |
注意:recvfrom() 的 addrlen 是指针!传入时是 sizeof(struct sockaddr_in),函数返回后变成实际地址长度。我见过有人传了 NULL,结果不知道数据是谁发的——这在多客户端场景下就尴尬了。
完整代码示例:UDP回声服务端
下面是一个完整的UDP回声服务端。客户端发什么,服务端就原样返回。这个例子我在培训新人时经常用,简单但能说明所有关键点。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
int recv_len;
// 1. 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_addr.sin_port = htons(PORT);
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(sockfd);
exit(1);
}
printf("UDP 服务端已启动,监听端口 %d...\n", PORT);
// 3. 循环接收数据并回声
while (1) {
recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &addr_len);
if (recv_len < 0) {
perror("recvfrom");
continue;
}
buffer[recv_len] = '\0';
printf("收到来自 %s:%d 的消息: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 原样发送回去
sendto(sockfd, buffer, recv_len, 0,
(struct sockaddr *)&client_addr, addr_len);
}
close(sockfd);
return 0;
}
完整代码示例:UDP客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
socklen_t addr_len = sizeof(server_addr);
char send_buf[BUFFER_SIZE];
char recv_buf[BUFFER_SIZE];
int recv_len;
// 1. 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置服务端地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
// 3. 发送数据
printf("请输入要发送的消息: ");
fgets(send_buf, BUFFER_SIZE, stdin);
send_buf[strcspn(send_buf, "\n")] = '\0'; // 去掉换行符
sendto(sockfd, send_buf, strlen(send_buf), 0,
(struct sockaddr *)&server_addr, addr_len);
printf("已发送: %s\n", send_buf);
// 4. 接收回声
recv_len = recvfrom(sockfd, recv_buf, BUFFER_SIZE, 0,
(struct sockaddr *)&server_addr, &addr_len);
if (recv_len > 0) {
recv_buf[recv_len] = '\0';
printf("收到服务端回声: %s\n", recv_buf);
}
close(sockfd);
return 0;
}
运行效果
先启动服务端,再启动客户端。客户端输入 "Hello UDP",服务端会打印收到的消息,客户端会收到同样的内容。
# 服务端输出
UDP 服务端已启动,监听端口 8888...
收到来自 127.0.0.1:54321 的消息: Hello UDP
# 客户端输出
请输入要发送的消息: Hello UDP
已发送: Hello UDP
收到服务端回声: Hello UDP
避坑指南
这里总结几个我踩过的坑,你写代码时注意一下:
- 字节序问题:IP和端口一定要用
htonl()/htons()转换。我曾经在x86机器上写死端口,换到ARM板子上就收不到数据——因为字节序反了。 - 缓冲区大小:UDP报文最大65507字节(理论值),但实际建议控制在1472字节以内(以太网MTU 1500 - IP头20 - UDP头8)。超过这个值可能会分片,分片后只要丢一片,整个报文就废了。
- recvfrom 的 addrlen 必须初始化:传入前一定要赋值为
sizeof(struct sockaddr_in),否则函数可能返回错误。 - 客户端不 bind 也可以:系统会自动分配一个临时端口。但如果你需要固定端口(比如做P2P打洞),那就手动 bind。
经验之谈:我在做游戏服务器时,UDP包大小严格控制在1400字节以内。超过这个值,某些路由器会丢弃分片包,导致玩家掉线。后来我们干脆在应用层自己做分包和重组,虽然麻烦点,但稳定多了。
UDP vs TCP 快速对比
| 特性 | UDP | TCP |
|---|---|---|
| 连接 | 无连接 | 面向连接 |
| 可靠性 | 不可靠 | 可靠 |
| 顺序 | 不保证 | 保证 |
| 速度 | 快 | 相对慢(有握手和确认) |
| 边界 | 保留消息边界 | 字节流,无边界 |
| 适用场景 | 音视频、游戏、DNS | 文件传输、网页、邮件 |
嗯,到这里UDP的基础通信就讲完了。代码不多,但核心思想都在里面。你把这个回声例子跑通了,后面做广播、多播、甚至P2P通信,都是在这个基础上加东西而已。