20、超时控制:setsockopt()设置收发超时、alarm()信号超时、select()超时。
网络编程里,超时控制是个绕不开的话题。你想想看,一个 socket 连上去之后,如果对端一直不回复数据,你的程序怎么办?傻等?那肯定不行。我刚开始写网络程序那会儿,就吃过这个亏——一个客户端连上服务器后,服务器挂了,客户端就卡在 recv() 那里一动不动,像个木头人。后来我才明白,超时控制是每个网络程序员必须掌握的技能。
今天咱们就聊聊三种常见的超时控制手段:setsockopt() 设置收发超时、alarm() 信号超时、select() 超时。这三种方法各有各的适用场景,我一个个给你拆解。
一、三种超时控制方式对比
先看个总览,心里有个谱:
| 方式 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| setsockopt() | 设置 socket 选项 SO_RCVTIMEO / SO_SNDTIMEO | 阻塞式 recv() / send() 超时 | 简单、直接、线程安全 | 只能用于阻塞模式,粒度较粗 |
| alarm() | 利用 SIGALRM 信号中断阻塞调用 | 整体操作超时(如 connect) | 可以中断任何阻塞系统调用 | 信号处理复杂,多线程下坑多 |
| select() | 用 select 的第五个参数控制等待时间 | 多路复用 + 超时控制 | 灵活、可同时监控多个 fd | 代码稍复杂,大并发下性能不如 epoll |
二、setsockopt() 设置收发超时
这是最直接的方式。你只需要在创建 socket 之后,调用 setsockopt() 设置 SO_RCVTIMEO 或 SO_SNDTIMEO 选项。说白了就是告诉内核:如果 recv() 或 send() 在指定时间内没完成,直接返回错误。
我个人习惯用这种方式处理简单的客户端超时,因为它真的省事。来看代码:
#include <sys/socket.h>
#include <sys/time.h>
#include <errno.h>
int set_recv_timeout(int fd, int seconds) {
struct timeval tv;
tv.tv_sec = seconds;
tv.tv_usec = 0;
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO,
&tv, sizeof(tv)) < 0) {
return -1;
}
return 0;
}
// 使用示例
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 设置接收超时 5 秒
set_recv_timeout(sock, 5);
char buf[1024];
int n = recv(sock, buf, sizeof(buf), 0);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("接收超时了\n");
} else {
perror("recv error");
}
}
return 0;
}
三、alarm() 信号超时
alarm() 这种方式比较「暴力」。它利用 SIGALRM 信号来中断当前进程的阻塞系统调用。你想想看,如果 connect() 一直连不上,默认可能要等几十秒甚至更久。这时候用 alarm() 就很合适。
不过我得提醒你:信号处理在多线程环境下是个大坑。我早期在一个多线程服务器里用过 alarm(),结果信号不知道发给哪个线程,程序行为变得不可预测。从那以后,我只有在单线程的简单场景下才敢用 alarm()。
#include <unistd.h>
#include <signal.h>
#include <errno.h>
int connect_timeout = 0;
void alarm_handler(int sig) {
connect_timeout = 1;
}
int connect_with_timeout(int fd, struct sockaddr *addr,
socklen_t len, int seconds) {
// 注册信号处理函数
signal(SIGALRM, alarm_handler);
// 设置闹钟
alarm(seconds);
int ret = connect(fd, addr, len);
// 取消闹钟
alarm(0);
if (ret < 0) {
if (errno == EINTR && connect_timeout) {
errno = ETIMEDOUT;
return -1;
}
}
return ret;
}
四、select() 超时
select() 是我最推荐的方式。它不仅能做超时控制,还能同时监控多个文件描述符。你想想看,一个服务器要同时处理几百个客户端连接,每个连接都要有超时检测,用 select() 就非常自然。
select() 的第五个参数是一个 struct timeval 指针,如果传 NULL 表示无限等待,传一个具体值就表示超时时间。当 select() 返回 0 时,就说明超时了。
#include <sys/select.h>
#include <sys/time.h>
int recv_with_select(int fd, char *buf, int len, int seconds) {
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
tv.tv_sec = seconds;
tv.tv_usec = 0;
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret == 0) {
// 超时
errno = ETIMEDOUT;
return -1;
}
if (ret < 0) {
return -1;
}
// 有数据可读
return recv(fd, buf, len, 0);
}
五、三种方式的选择策略
说了这么多,到底该用哪种?我根据自己的经验给你一个参考:
- 简单客户端:用 setsockopt() 就够了。代码少,逻辑清晰。
- 单线程、需要中断 connect():可以用 alarm(),但记得处理好信号。
- 多线程服务器:用 select() 或 poll()/epoll()。别碰 alarm()。
- 需要同时监控多个 fd:select() 是底线方案,大并发建议用 epoll。
嗯,这里还要注意一点:超时时间设置要合理。我见过有人把超时设成 1 毫秒,结果网络稍微波动一下程序就报超时。也见过有人设成 60 秒,用户等得都想砸键盘。一般来说,局域网内 3-5 秒比较合理,公网环境可以放宽到 10-15 秒。
六、知识体系总览
下面这张图帮你梳理一下三种超时控制方式的关系和适用场景:
好了,三种超时控制方式就聊到这儿。每种方式都有它的脾气,选对了事半功倍,选错了坑你没商量。我个人建议你从 select() 开始练手,它虽然代码量稍大,但能帮你建立更清晰的超时控制思维。等你用熟了,再根据场景灵活搭配其他方式。