3. Linux UDP 编程基础:从 socket 到非阻塞
好,咱们进入实战环节。UDP 编程,说白了就是让两个程序通过“无连接”的方式互相扔数据包。你想想看,没有三次握手,没有连接维护,效率确实高,但代价就是——丢包了你自己扛。
这一章,我会把 UDP 编程最核心的几个 API 掰开揉碎讲清楚。包括 socket()、bind()、sendto()、recvfrom(),以及非阻塞模式怎么设。每个函数我都会结合自己的踩坑经历来讲,希望能帮你少走弯路。
核心要点:UDP 编程的四个基本步骤——创建套接字、绑定地址、发送数据、接收数据。非阻塞模式是解决“卡死”问题的关键。
3.1 socket() 函数:一切通信的起点
socket() 是网络编程的入口。它的作用就是向内核申请一个套接字资源。说白了,就是拿一个文件描述符,后面所有的收发操作都靠它。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
三个参数,我一个个说:
- domain:地址族。UDP 通信几乎只用
AF_INET(IPv4)或AF_INET6(IPv6)。我个人习惯用AF_INET,兼容性最好。 - type:套接字类型。UDP 用
SOCK_DGRAM(数据报套接字)。别写成SOCK_STREAM,那是 TCP 的。 - protocol:协议。传 0 就行,内核会根据 domain 和 type 自动选择 UDP 协议。
小技巧:返回值判断要严谨。我见过不少新手直接用 if (sockfd < 0),其实应该用 if (sockfd == -1)。虽然效果一样,但后者更符合 POSIX 规范。
举个例子:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
嗯,这里要注意:socket() 返回的 fd 默认是阻塞模式。后面我们会讲怎么改成非阻塞。
3.2 bind() 函数:给套接字一个身份
bind() 的作用是把套接字和一个本地地址绑定起来。对于 UDP 服务端来说,这步是必须的——你得告诉内核:“嘿,这个端口归我管了。”
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
| 参数 | 含义 | 常见取值 |
|---|---|---|
| sockfd | socket() 返回的文件描述符 | 必须有效 |
| addr | 指向 sockaddr_in 结构体的指针 | 需要强制类型转换 |
| addrlen | 地址结构体的大小 | sizeof(struct sockaddr_in) |
我在项目中遇到过一个问题:bind()EADDRINUSE 错误。这是因为端口被占用了。解决办法有两个:一是换端口,二是设置 SO_REUSEADDR 选项。
避坑指南:我曾经在调试一个 UDP 服务时,发现 bind() 总是失败。查了半天,原来是端口号小于 1024,需要 root 权限。记住:1024 以下的端口只有 root 才能绑定。
完整的绑定代码示例:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
servaddr.sin_port = htons(8888); // 端口号
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
3.3 sendto() 与 recvfrom():UDP 的收发双雄
这两个函数是 UDP 通信的核心。和 TCP 不同,UDP 每次发送都要指定目标地址,每次接收都能拿到发送方的地址。
3.3.1 sendto():把数据扔出去
#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:套接字描述符
- buf:要发送的数据缓冲区
- len:数据长度(字节)
- flags:标志位,一般传 0
- dest_addr:目标地址结构体
- addrlen:目标地址结构体大小
返回值:成功返回发送的字节数,失败返回 -1。
个人经验:UDP 的 sendto() 不会阻塞,因为数据直接扔给内核缓冲区就返回了。但要注意:如果缓冲区满了,会返回 EAGAIN 或 EWOULDBLOCK。我在高并发场景下遇到过这个问题,解决办法是增大套接字缓冲区。
3.3.2 recvfrom():等着收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
和 sendto() 对称,但有两个关键区别:
- src_addr 是输出参数,会填上发送方的地址
- addrlen 是值-结果参数,调用前要初始化,调用后会被修改
举个例子,完整的收发流程:
// 发送端
char buffer[] = "Hello, UDP!";
struct sockaddr_in dest;
dest.sin_family = AF_INET;
dest.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.1.100", &dest.sin_addr);
ssize_t sent = sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr *)&dest, sizeof(dest));
if (sent == -1) {
perror("sendto failed");
}
// 接收端
char recv_buf[1024];
struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
ssize_t received = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&sender, &sender_len);
if (received > 0) {
recv_buf[received] = '\0';
printf("Received from %s:%d: %s\n",
inet_ntoa(sender.sin_addr),
ntohs(sender.sin_port),
recv_buf);
}
3.4 非阻塞模式设置:别让程序卡死
默认情况下,recvfrom() 是阻塞的——如果没有数据,它会一直等下去。这在生产环境里是个大问题。你想想看,如果网络断了,程序就永远卡在那了。
设置非阻塞模式有两种方法:
方法一:fcntl() 设置 O_NONBLOCK
#include <fcntl.h>
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL failed");
return -1;
}
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL failed");
return -1;
}
方法二:recvfrom() 的 MSG_DONTWAIT 标志
ssize_t n = recvfrom(sockfd, buf, len, MSG_DONTWAIT,
(struct sockaddr *)&src, &src_len);
if (n == -1 && errno == EAGAIN) {
// 没有数据,不阻塞,直接返回
printf("No data available\n");
}
避坑指南:我曾经在做一个实时音视频传输项目时,用了阻塞模式。结果有一次网络抖动,整个接收线程卡死了 30 秒,导致视频画面直接卡住。后来改成非阻塞 + 超时重试机制,问题才解决。记住:生产环境一定要用非阻塞模式。
两种方法的对比:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| fcntl() 设置 O_NONBLOCK | 全局生效,所有操作都非阻塞 | 需要额外系统调用 | 长期运行的服务端 |
| MSG_DONTWAIT 标志 | 只影响单次调用,灵活 | 每次都要传标志 | 临时需要非阻塞的场景 |
我的建议:服务端程序用 fcntl() 设置全局非阻塞,客户端按需使用 MSG_DONTWAIT。这样代码更清晰,也更容易维护。
好了,这一章的内容就到这里。UDP 编程的基础 API 其实不多,但每个细节都可能成为坑。记住:socket() 创建、bind() 绑定、sendto() 和 recvfrom() 收发、非阻塞模式保平安。把这四个点吃透,UDP 编程就算入门了。