23. UDP回射服务器:用recvfrom/sendto实现

UDP回射服务器,说白了就是“你发啥,我原样返回啥”。

它不像TCP那样需要建立连接、维护状态。UDP是无连接的,服务器只管收数据,然后原封不动地发回去。我刚开始学网络编程时,觉得这玩意儿太简单了,不就是收一下、发一下嘛。但后来在项目中踩过坑才发现,越简单的东西,越容易在细节上翻车。

核心逻辑:收包 → 回射

UDP回射服务器的核心流程,其实就两步:

  1. :用 recvfrom() 接收客户端发来的数据,同时拿到客户端的地址和端口。
  2. :用 sendto() 把同样的数据,发回给那个客户端。

嗯,就是这么简单。但你要注意,recvfromsendto 的调用方式,跟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回射服务器流程图 创建socket bind绑定端口 循环:recvfrom → sendto close(退出时) UDP无连接 不需要accept/listen 直接收/发数据 recvfrom同时获取 客户端地址和端口

避坑指南:我踩过的几个坑

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_addrsin_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回射服务器就讲到这里。代码不长,但细节不少。你写的时候,多注意缓冲区大小和返回值处理,基本就不会出大问题。

公众号:蓝海资料掘金营,微信deep3321