4、UDP数据发送与接收:sendto()与recvfrom()系统调用
好,咱们今天聊聊UDP最核心的两个操作——发数据和收数据。说白了,就是 sendto() 和 recvfrom() 这两个系统调用。你想想看,TCP那边有 connect()、listen()、accept() 一套流程,UDP这边就简单多了,两个函数搞定一切。
我个人习惯把UDP的收发比作「寄明信片」。你写好内容,贴上地址,扔进邮筒就行。不需要提前跟对方打招呼,也不需要确认对方收到了没有。这就是UDP的哲学——简单、直接、不拖泥带水。
4.1 sendto():把数据扔出去
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:你创建好的UDP套接字描述符
- buf:要发送的数据缓冲区
- len:数据长度(字节)
- flags:标志位,一般填0就行。想玩点花的可以填
MSG_DONTWAIT或MSG_NOSIGNAL - dest_addr:目标地址结构体(IP + 端口)
- addrlen:地址结构体长度
返回值是实际发送的字节数,出错返回 -1。
sendto() 一定会把 len 个字节全发出去。其实不一定!虽然UDP数据报理论上不会像TCP那样分多次发送,但如果发送缓冲区满了,或者遇到信号中断,返回值可能小于 len。我建议你写个循环检查返回值,别偷懒。
4.2 recvfrom():等着收明信片
有发就有收。recvfrom() 是接收端的主力:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
注意最后两个参数是指针。调用前 addrlen 要初始化为 sizeof(struct sockaddr_in),调用后里面会填上发送端的地址信息。这样你就知道是谁发来的数据。
返回值是接收到的字节数。如果返回0,说明对端关闭了连接——等等,UDP哪有连接?嗯,这里返回0其实不太常见,一般表示收到了一个空数据报。
recvfrom() 一直返回 -1,errno 是 EAGAIN。查了半天才发现,我把套接字设成了非阻塞模式,但没检查返回值就硬等。非阻塞模式下没数据就直接返回,不会阻塞。这个后面会细说。
4.3 无连接特性:UDP的灵魂
UDP最鲜明的特点就是「无连接」。什么意思?
- 发送前不需要握手,直接扔数据
- 接收端不需要维护连接状态表
- 一个套接字可以跟任意多个对端通信
- 内核不保存任何关于「对方是谁」的信息
我刚开始学网络编程时,总觉得这有点「不靠谱」。后来做视频流传输项目才明白,无连接恰恰是UDP的优势。你想啊,视频通话里每一帧都是独立的,丢了就丢了,下一帧补上。如果每发一帧都要先建立连接,那延迟得有多大?
下面这张图能帮你理解UDP无连接的工作方式:
4.4 数据报边界保持:UDP的「信封」
这一点特别重要,也是UDP和TCP最大的区别之一。
TCP是流式协议,没有边界。你发两次 send(),对方可能一次 recv() 就全收走了,也可能分三次收。你根本不知道哪段数据对应哪次发送。
UDP不一样。它严格保持数据报边界。你发一次 sendto() 送出去的数据,对方必须用一次 recvfrom() 完整接收。不多不少,刚刚好。
举个例子:
| 操作 | 发送端 | 接收端 |
|---|---|---|
| 第一次 | sendto("Hello", 5) | recvfrom() → 收到 "Hello" (5字节) |
| 第二次 | sendto("World", 5) | recvfrom() → 收到 "World" (5字节) |
| 第三次 | sendto("!", 1) | recvfrom() → 收到 "!" (1字节) |
看到没?三次发送,三次接收,每次的数据长度完全对应。不会出现TCP那种「粘包」问题。
recvfrom() 会返回 -1,并设置 errno 为 EMSGSIZE。数据报剩余部分会被丢弃。所以接收缓冲区一定要够大!我一般设 65535 字节,这是UDP数据报的最大理论值。
4.5 阻塞与非阻塞模式:等还是不等?
默认情况下,套接字是阻塞模式。什么意思?
- 阻塞 recvfrom():如果没有数据可读,调用会一直等,直到有数据到达
- 阻塞 sendto():如果发送缓冲区满了,调用会阻塞直到有空间
但有些场景下,你不想让程序卡在那里。比如一个游戏服务器,既要处理网络数据,又要更新游戏逻辑。这时候就需要非阻塞模式。
设置非阻塞有两种方式:
// 方法一:创建时指定
int sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
// 方法二:运行时设置
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
非阻塞模式下,recvfrom() 如果没有数据,立即返回 -1,errno 设为 EAGAIN 或 EWOULDBLOCK。你的程序可以去做别的事,过一会儿再来检查。
我做过一个实时监控系统,用非阻塞UDP收数据。主循环每10毫秒轮询一次套接字,有数据就处理,没数据就继续跑其他逻辑。这样既保证了实时性,又不浪费CPU。
select()、poll() 或 epoll() 使用,让内核告诉你什么时候有数据。
4.6 实战:一个简单的UDP收发示例
光说不练假把式。咱们写个最简单的UDP回射服务器和客户端:
发送端(客户端):
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
const char *msg = "Hello, UDP!";
int ret = sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
if (ret < 0) {
perror("sendto");
} else {
printf("发送了 %d 字节\n", ret);
}
close(sockfd);
return 0;
}
接收端(服务器):
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
return 1;
}
char buf[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int ret = recvfrom(sockfd, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&client_addr, &addr_len);
if (ret < 0) {
perror("recvfrom");
} else {
buf[ret] = '\0';
printf("收到来自 %s:%d 的消息: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buf);
}
close(sockfd);
return 0;
}
这个例子虽然简单,但涵盖了UDP收发的全部核心流程。你编译运行一下,先开服务器,再开客户端,就能看到效果。
recvfrom() 在没有数据时返回什么。再试试发一个超过接收缓冲区大小的数据报,看看 EMSGSIZE 错误。自己动手踩一遍坑,比看十遍书都管用。
好了,UDP的收发机制就聊到这儿。记住四个关键词:sendto、recvfrom、无连接、边界保持。理解了这些,UDP编程你就掌握了七八成。剩下的就是多写多练,把坑都踩一遍,自然就熟练了。