第24章 UDP回射客户端:实现UDP回射客户端,使用sendto/recvfrom

UDP回射客户端,说白了就是给服务器发一条消息,然后等着服务器原封不动地给你弹回来。听起来简单吧?但这里面的坑,我当年可没少踩。

这一章,我们就手写一个UDP回射客户端。核心API就两个:sendtorecvfrom。一个发,一个收,干净利落。

24.1 回射模型长什么样?

先画个图,让你心里有个底。UDP回射的流程其实特别直白:

UDP回射模型 UDP客户端 socket() sendto() → 发送数据 UDP服务器 recvfrom() sendto() → 回射数据 发送数据 回射数据 客户端 sendto → 服务器 recvfrom → 服务器 sendto → 客户端 recvfrom 整个过程无连接,无状态,一发一收

嗯,就是这么简单。客户端发一条消息,服务器收到后,原样再发回来。客户端再收回来,比对一下,就知道网络通不通、服务器活不活。

24.2 客户端核心流程

写UDP客户端,我个人习惯分四步走:

  1. 创建套接字 —— socket(AF_INET, SOCK_DGRAM, 0)
  2. 设置服务器地址 —— 填好IP和端口
  3. 发送数据 —— sendto()
  4. 接收回射 —— recvfrom()

没有 connect(),没有 bind()。UDP客户端就是这么洒脱。

小提示: 虽然UDP不需要connect,但如果你后续只用同一个服务器通信,可以调用connect()。这样sendto就能简化为send()。不过这一章我们保持最原始的方式,让你看清UDP的本质。

24.3 代码实现:UDP回射客户端

直接上代码。我尽量把注释写清楚,你跟着看就行。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.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;
    char send_buf[BUFFER_SIZE];
    char recv_buf[BUFFER_SIZE];
    socklen_t addr_len = sizeof(server_addr);

    // 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_port = htons(SERVER_PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    printf("UDP回射客户端已启动,目标:%s:%d\n", SERVER_IP, SERVER_PORT);
    printf("输入消息,按回车发送。输入 'quit' 退出。\n");

    while (1) {
        printf("> ");
        fgets(send_buf, BUFFER_SIZE, stdin);
        // 去掉换行符
        send_buf[strcspn(send_buf, "\n")] = '\0';

        if (strcmp(send_buf, "quit") == 0) {
            break;
        }

        // 3. 发送数据到服务器
        ssize_t sent_len = sendto(sockfd, send_buf, strlen(send_buf), 0,
                                  (struct sockaddr*)&server_addr, addr_len);
        if (sent_len < 0) {
            perror("sendto");
            continue;
        }

        // 4. 接收服务器回射的数据
        ssize_t recv_len = recvfrom(sockfd, recv_buf, BUFFER_SIZE - 1, 0,
                                    (struct sockaddr*)&server_addr, &addr_len);
        if (recv_len < 0) {
            perror("recvfrom");
            continue;
        }

        recv_buf[recv_len] = '\0';
        printf("收到回射:%s\n", recv_buf);
    }

    close(sockfd);
    printf("客户端已退出。\n");
    return 0;
}

24.4 关键点解析

代码不长,但有几个地方我得拎出来说说。

24.4.1 sendto 的第五、第六参数

这两个参数是目标地址和地址长度。UDP是无连接的,所以每次发数据都要告诉内核:「嘿,帮我发给这个IP这个端口」。不像TCP,connect一次后面就不用管了。

我刚开始写UDP的时候,经常忘记填地址长度,或者填错了,结果sendto一直返回-1。后来养成了习惯,每次都在调用前先 addr_len = sizeof(server_addr) 重置一下。

24.4.2 recvfrom 的第五、第六参数

这两个参数是发送方的地址。在回射场景里,服务器回射的数据就是从服务器地址发回来的,所以这里填 &server_addr 没问题。但如果你要同时跟多个服务器通信,这里就得用另一个结构体来接收来源地址。

注意: recvfrom的第六参数是值-结果参数。调用前必须初始化成 sizeof(server_addr),调用后它会被内核改写为实际收到的地址长度。如果不初始化,recvfrom可能返回-1。我曾经在这个坑里趴了半小时。

24.4.3 阻塞与非阻塞

默认情况下,recvfrom 是阻塞的。也就是说,如果服务器挂了或者网络断了,你的客户端就会卡在 recvfrom 那里,一直等下去。生产环境中,我一般会配合 setsockopt 设置超时,或者用 select / poll 做多路复用。不过这一章我们先不搞那么复杂,先把基础跑通。

24.5 编译与测试

假设你已经有UDP回射服务器在跑了(比如上一章我们写的那个),编译客户端:

gcc -o udp_echo_client udp_echo_client.c

运行:

./udp_echo_client

效果大概是这样:

UDP回射客户端已启动,目标:127.0.0.1:8888
输入消息,按回车发送。输入 'quit' 退出。
> hello
收到回射:hello
> world
收到回射:world
> quit
客户端已退出。

看到没?你发什么,它就回什么。这就是回射。

24.6 常见问题与避坑

  • sendto 返回成功但服务器没收到? 检查一下服务器IP和端口是不是写对了。UDP不保证送达,sendto成功只代表数据交给了内核,不代表对方收到了。
  • recvfrom 一直阻塞? 服务器可能没启动,或者防火墙拦了。先 ping 一下服务器,再用 netstat -uln 确认端口在监听。
  • 收到的数据乱码? 检查字节序。端口号用 htons,IP地址用 inet_pton,这些都不能省。
  • recvfrom 返回 -1 但 errno 是 EINTR? 信号中断了系统调用。简单处理就是重新调用一次 recvfrom。我在写压力测试程序时经常遇到这个。

24.7 小结

UDP回射客户端,说白了就是 sendtorecvfrom 的循环。没有连接维护,没有状态管理,简单粗暴。但正是这种简单,让UDP在很多实时场景下比TCP更受欢迎。

我个人建议你把这个例子跑通之后,试着改一改:比如加个超时机制,或者改成并发发送多条消息。动手试过,才是自己的。

核心口诀: socket创建,sendto发送,recvfrom接收,close收尾。UDP客户端,四步走天下。

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