第24章 UDP回射客户端:实现UDP回射客户端,使用sendto/recvfrom
UDP回射客户端,说白了就是给服务器发一条消息,然后等着服务器原封不动地给你弹回来。听起来简单吧?但这里面的坑,我当年可没少踩。
这一章,我们就手写一个UDP回射客户端。核心API就两个:sendto 和 recvfrom。一个发,一个收,干净利落。
24.1 回射模型长什么样?
先画个图,让你心里有个底。UDP回射的流程其实特别直白:
嗯,就是这么简单。客户端发一条消息,服务器收到后,原样再发回来。客户端再收回来,比对一下,就知道网络通不通、服务器活不活。
24.2 客户端核心流程
写UDP客户端,我个人习惯分四步走:
- 创建套接字 ——
socket(AF_INET, SOCK_DGRAM, 0) - 设置服务器地址 —— 填好IP和端口
- 发送数据 ——
sendto() - 接收回射 ——
recvfrom()
没有 connect(),没有 bind()。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 没问题。但如果你要同时跟多个服务器通信,这里就得用另一个结构体来接收来源地址。
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回射客户端,说白了就是 sendto 加 recvfrom 的循环。没有连接维护,没有状态管理,简单粗暴。但正是这种简单,让UDP在很多实时场景下比TCP更受欢迎。
我个人建议你把这个例子跑通之后,试着改一改:比如加个超时机制,或者改成并发发送多条消息。动手试过,才是自己的。
公众号:蓝海资料掘金营,微信deep3321