第11章 UDP编程:通信流程、核心函数与回射服务器实现
聊到UDP编程,我脑子里第一个蹦出来的词就是“轻量级”。跟TCP那套复杂的握手挥手比起来,UDP简直像个直来直去的快递员——把包裹扔给你就走,签不签收它不管。很多初学者觉得UDP简单,不就是发个数据报嘛。但我在项目中踩过几次坑之后才发现,越简单的东西,越容易在细节上翻车。
11.1 UDP通信流程:无连接的“发完即走”
UDP的全称是User Datagram Protocol,用户数据报协议。它没有TCP那种“先打电话再说话”的流程。说白了,UDP就是直接往对端IP和端口上扔数据,对方收不收得到,它不关心。
通信流程大致如下:
- 服务端:创建socket → bind绑定端口 → 循环recvfrom接收 → sendto回复
- 客户端:创建socket → sendto发送 → recvfrom接收(可选)
你看,客户端连connect都不需要。我刚开始学的时候觉得这太爽了,少写好多代码。后来才发现,没有连接意味着没有重传、没有拥塞控制、没有保活机制。你发出去的数据报,可能半路就丢了,而你的程序还傻呵呵地以为对方收到了。
核心区别:TCP是面向连接的字节流,UDP是无连接的数据报。UDP每个sendto对应一个独立的数据报,接收方必须用足够大的缓冲区一次收完。
11.2 recvfrom()与sendto():UDP的灵魂函数
这两个函数是UDP编程的核心。我见过不少新手把参数填错,导致数据死活发不出去。咱们一个一个来看。
11.2.1 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:socket描述符,就是那个int
- buf:要发送的数据缓冲区
- len:数据长度,注意不要超过UDP数据报大小限制(65507字节)
- flags:一般填0,除非你要用MSG_DONTWAIT之类的特殊标志
- dest_addr:目标地址结构体,里面填IP和端口
- addrlen:地址结构体的大小
返回值是实际发送的字节数。如果返回-1,那就是出错了。嗯,这里要注意:sendto成功返回只代表数据成功交给了协议栈,不代表对端收到了。我曾经在调试一个UDP服务时,看到sendto返回了100字节,以为万事大吉,结果对端那边毛都没收到——因为中间路由器把包丢了。
11.2.2 recvfrom():蹲在端口上等数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数跟sendto基本对称:
- sockfd:socket描述符
- buf:接收缓冲区
- len:缓冲区大小,建议至少65536字节
- flags:一般填0
- src_addr:输出参数,返回发送方的地址信息
- addrlen:值-结果参数,调用前填sizeof,调用后变成实际地址长度
recvfrom会阻塞,直到有数据到达。如果你不想阻塞,可以用fcntl设置非阻塞模式,或者用select/poll/epoll来监控。
个人经验:我习惯在recvfrom之前把src_addr结构体清零,addrlen设为sizeof(struct sockaddr_in)。这样能避免一些奇怪的内存残留问题。别问我怎么知道的——调试了一下午才发现的。
11.3 UDP回射服务器实现:从理论到代码
回射服务器(echo server)是网络编程里的“Hello World”。客户端发什么,服务端原样返回什么。虽然简单,但能完整展示UDP通信的整个流程。
11.3.1 服务端代码
#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. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 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 failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("UDP echo server listening on port %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 failed");
continue;
}
buffer[recv_len] = '\0'; // 确保字符串结束
printf("Received from %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;
}
11.3.2 客户端代码
#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 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. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
// 3. 发送并接收回射数据
while (1) {
printf("Enter message: ");
fgets(send_buf, BUFFER_SIZE, stdin);
send_buf[strcspn(send_buf, "\n")] = '\0'; // 去掉换行符
if (strcmp(send_buf, "exit") == 0) break;
// 发送数据
sendto(sockfd, send_buf, strlen(send_buf), 0,
(struct sockaddr *)&server_addr, addr_len);
// 接收回射数据
recv_len = recvfrom(sockfd, recv_buf, BUFFER_SIZE, 0,
NULL, NULL); // 不关心发送方地址
if (recv_len < 0) {
perror("recvfrom failed");
continue;
}
recv_buf[recv_len] = '\0';
printf("Echo from server: %s\n", recv_buf);
}
close(sockfd);
return 0;
}
11.4 UDP通信流程图
下面这张图展示了UDP回射服务器的完整通信流程。我特意把服务端和客户端放在两侧,方便对比。
11.5 避坑指南:UDP编程常见问题
写UDP程序,有几个坑我几乎每次都会遇到。分享出来,希望大家少走弯路。
坑一:数据报边界问题
UDP是消息边界保护的。你sendto一次,对端必须recvfrom一次。如果对端缓冲区太小,多余的数据会被丢弃。我曾经写过一个日志收集程序,客户端每次发500字节,服务端缓冲区只设了256字节——结果日志全丢了,排查了一天才发现。
坑二:字节序问题
端口号和IP地址在网络传输中要用网络字节序(大端)。我见过有人直接赋值端口号,没调用htons,结果服务端bind到端口8888,客户端却往端口0x22B8(小端解释)发数据。嗯,这种bug最难查。
坑三:UDP丢包无感知
UDP不保证可靠传输。如果你的应用场景要求数据必须到达,那就要在应用层自己实现确认和重传机制。我在做视频流传输时,丢个几帧还能接受,但如果是金融交易数据,丢一个包就是事故。
11.6 总结
UDP编程的核心就三个东西:socket、sendto、recvfrom。流程简单,但细节不少。我个人建议初学者先把回射服务器跑通,然后试着改一改——比如改成广播模式,或者加个简单的超时重传。只有亲手写过、调试过、踩过坑,才能真正理解UDP的脾气。
记住一句话:UDP是快,但快是有代价的。选择UDP,就意味着你要自己处理丢包、乱序、重复等问题。用好了,它是利器;用不好,它就是坑。