23. UDP回射服务器:用recvfrom/sendto实现
UDP回射服务器,说白了就是“你发啥,我原样返回啥”。
它不像TCP那样需要建立连接、维护状态。UDP是无连接的,服务器只管收数据,然后原封不动地发回去。我刚开始学网络编程时,觉得这玩意儿太简单了,不就是收一下、发一下嘛。但后来在项目中踩过坑才发现,越简单的东西,越容易在细节上翻车。
核心逻辑:收包 → 回射
UDP回射服务器的核心流程,其实就两步:
- 收:用
recvfrom()接收客户端发来的数据,同时拿到客户端的地址和端口。 - 发:用
sendto()把同样的数据,发回给那个客户端。
嗯,就是这么简单。但你要注意,recvfrom 和 sendto 的调用方式,跟TCP的 read/write 不太一样。它们多了一个 struct sockaddr 参数,用来记录或指定对端地址。
关键点:UDP服务器不需要 accept(),也不需要 listen()。它只需要 socket() + bind(),然后直接循环收发包。
代码实现:一个完整的回射服务器
下面是我写的一个标准UDP回射服务器。代码不长,但每个细节我都加了注释。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.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];
// 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 = 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) {
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n < 0) {
perror("recvfrom");
continue;
}
buffer[n] = '\0'; // 确保字符串结束
printf("收到来自 %s:%d 的消息:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 原样回射
int sent = sendto(sockfd, buffer, n, 0,
(struct sockaddr*)&client_addr, addr_len);
if (sent < 0) {
perror("sendto");
}
}
close(sockfd);
return 0;
}
小提示:recvfrom 的最后一个参数 addr_len 必须初始化为 sizeof(client_addr)。我见过有人忘了初始化,结果 recvfrom 一直返回 -1,查了半天才发现是这里的问题。
流程图:UDP回射服务器的工作过程
下面这张图,把整个流程串起来了。你可以看到,UDP服务器其实就是一个“收-发”的循环,没有TCP那些握手、挥手的花哨动作。
避坑指南:我踩过的几个坑
UDP回射服务器虽然代码短,但坑不少。我把自己曾经犯过的错列出来,你写代码时留个心。
- 缓冲区大小不够:UDP数据报有长度限制(通常65535字节),但实际网络环境可能更小。我建议缓冲区设成
65536,或者至少4096。我曾经设成1024,结果客户端发了个2K的包,直接被截断了,回射回去的数据就不完整了。 - 忽略
recvfrom的返回值:返回值是实际收到的字节数。如果你用sizeof(buffer)作为长度发送,可能会把未初始化的内存也发出去。一定要用n作为sendto的长度。 - 端口绑定失败:如果端口被占用,
bind会返回 -1。我习惯在bind之前加个setsockopt设置SO_REUSEADDR,这样服务器重启时不会报“Address already in use”。 - 客户端地址结构体未清零:
recvfrom返回的client_addr里有些字段可能没被填充。虽然sendto只需要sin_addr和sin_port,但保险起见,我每次都会memset一下。
警告:UDP数据报可能丢失、重复、乱序。回射服务器本身不处理这些问题。如果你的应用场景要求可靠传输,请考虑TCP,或者在UDP之上自己实现确认重传机制。
测试方法:用nc或自己写客户端
你可以用 nc -u 127.0.0.1 8888 来测试。输入一行文字,服务器就会原样返回。我自己写了个简单的客户端来验证:
// 客户端示例(片段)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr;
// ... 设置服务器地址 ...
sendto(sockfd, "hello", 5, 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
char buf[1024];
int n = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
buf[n] = '\0';
printf("收到回射:%s\n", buf);
嗯,这里要注意:客户端的 recvfrom 最后两个参数可以传 NULL,因为我们不需要知道服务器地址(反正就是回射回来的)。但服务器端必须填,否则你没法把数据发回去。
性能与扩展
UDP回射服务器是单线程的,一次只能处理一个数据报。如果并发量高,数据报可能会排队。我在项目中遇到过,高峰期丢包率飙升。后来改成了多线程版本:主线程只负责 recvfrom,收到数据后丢给工作线程去 sendto。但要注意,UDP的并发模型跟TCP不太一样,不能简单套用 accept + 线程池。
| 特性 | UDP回射服务器 | TCP回射服务器 |
|---|---|---|
| 连接管理 | 无连接 | 需要accept/close |
| 数据边界 | 保留消息边界 | 流式,无边界 |
| 可靠性 | 不可靠,可能丢包 | 可靠,保证顺序 |
| 并发处理 | 简单,但需注意丢包 | 多线程/多进程模型成熟 |
说白了,UDP回射服务器适合做测试、调试,或者对实时性要求高但能容忍偶尔丢包的场景。比如音视频通话的探针、网络延迟测量工具等。
个人习惯:我写UDP服务器时,喜欢在 recvfrom 之后立即打印客户端的IP和端口,方便调试。生产环境里,我会加一个计数器,统计每秒处理的包数,一旦低于阈值就报警。
好了,UDP回射服务器就讲到这里。代码不长,但细节不少。你写的时候,多注意缓冲区大小和返回值处理,基本就不会出大问题。