36、超时控制:setsockopt设置SO_RCVTIMEO/SO_SNDTIMEO、alarm信号超时
网络编程里,超时控制是个老生常谈,但又特别容易翻车的地方。
你想想看,一个socket读操作,如果对端一直不发数据,你的程序就卡在那了。要是写操作,缓冲区满了,也可能一直阻塞。怎么办?
我个人习惯,只要涉及网络通信,一定会给读写操作加上超时。这不是可选项,是保命项。
两种主流的超时控制手段
在Linux下,常用的有两种:
- setsockopt + SO_RCVTIMEO / SO_SNDTIMEO —— 基于socket选项,粒度细,推荐。
- alarm + SIGALRM 信号 —— 老派做法,但坑多,不推荐在新代码里用。
我刚开始做网络编程时,图省事用过alarm。结果呢?有一次线上服务莫名其妙被中断,查了半天才发现是alarm信号干扰了其他系统调用。嗯,从那以后我再也不敢在生产环境乱用alarm了。
SO_RCVTIMEO 和 SO_SNDTIMEO 详解
这两个选项,说白了就是给recv、send、read、write等阻塞调用设一个“最长等待时间”。
超时时间用 struct timeval 来指定,精度到微秒。
核心要点:
- 超时触发后,阻塞调用会返回 -1,errno 设置为
EAGAIN或EWOULDBLOCK。 - 只对阻塞模式的socket有效。非阻塞模式下,这个选项会被忽略。
- 设置后,所有后续的读写操作都会受这个超时影响,直到你重新修改它。
代码示例:设置接收超时
#include <sys/socket.h>
#include <sys/time.h>
#include <errno.h>
#include <stdio.h>
int set_recv_timeout(int fd, int seconds, int microseconds) {
struct timeval tv;
tv.tv_sec = seconds;
tv.tv_usec = microseconds;
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
perror("setsockopt SO_RCVTIMEO");
return -1;
}
return 0;
}
// 使用示例
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
// ... 连接对端 ...
set_recv_timeout(sock, 5, 0); // 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;
}
我的经验: 设置超时后,一定要检查 errno 是不是 EAGAIN。很多新手只判断返回值小于0,然后直接报错,结果把超时当成真的网络错误处理了。
代码示例:设置发送超时
int set_send_timeout(int fd, int seconds, int microseconds) {
struct timeval tv;
tv.tv_sec = seconds;
tv.tv_usec = microseconds;
if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
perror("setsockopt SO_SNDTIMEO");
return -1;
}
return 0;
}
发送超时一般发生在对端接收窗口太小,或者网络拥堵导致内核缓冲区满。我遇到过几次,都是因为对端处理太慢,我这边send一直阻塞,最后靠超时机制才没让整个服务卡死。
alarm 信号超时 —— 老司机也翻车
alarm 的原理很简单:设置一个定时器,时间到了内核发 SIGALRM 信号。如果进程没有处理这个信号,默认动作是终止进程。
警告: 在多线程程序里,alarm 是进程级的,一个线程设了 alarm,所有线程都会收到信号。这会导致不可预测的行为。我强烈建议:新代码不要用 alarm 做超时控制。
alarm 的典型用法(仅作了解)
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void timeout_handler(int sig) {
// 注意:信号处理函数里只能调用异步信号安全的函数
write(STDOUT_FILENO, "alarm timeout!\n", 15);
}
int main() {
signal(SIGALRM, timeout_handler);
alarm(5); // 5秒后触发 SIGALRM
// 这里放一个可能会阻塞很久的操作
// 比如 read 或 recv
char buf[1024];
int n = read(0, buf, sizeof(buf));
if (n < 0) {
perror("read");
}
alarm(0); // 取消闹钟
return 0;
}
你看,代码看起来挺简单。但实际坑在哪?
- 如果 read 被信号中断,它会返回 -1,errno 是 EINTR。你得处理这个。
- alarm 是全局的,如果你调用了 sleep、pause 等函数,它们也可能被 SIGALRM 打断。
- 多线程环境下,alarm 信号会发给主线程,但其他线程也可能被影响。
我曾经在一个遗留项目里看到过用 alarm 做超时的代码,结果每次压力测试都会随机挂掉。最后全部改成 setsockopt 方式,世界清净了。
两种方式的对比
| 特性 | setsockopt + SO_RCVTIMEO/SO_SNDTIMEO | alarm + SIGALRM |
|---|---|---|
| 作用范围 | 单个 socket 描述符 | 整个进程 |
| 精度 | 微秒级 | 秒级 |
| 多线程安全 | 是 | 否 |
| 错误处理 | 返回 EAGAIN/EWOULDBLOCK | 返回 EINTR,需额外处理 |
| 推荐程度 | 强烈推荐 | 不推荐 |
知识体系与核心逻辑
下面这张图,帮你理清超时控制的整体脉络。
避坑指南
- 不要混用两种方式。 同一个socket上同时用setsockopt和alarm,行为未定义。
- 超时时间别设太短。 我见过有人设100微秒,结果大部分正常请求都超时了。网络延迟、系统调度都有开销,建议至少留几百毫秒。
- 检查返回值时,先判断 errno。 很多系统调用在超时后返回 -1,但errno是EAGAIN。别把它当成致命错误。
- alarm 的 SIGALRM 默认会终止进程。 如果你非要用,记得先 signal(SIGALRM, handler) 注册处理函数。
一个小技巧: 如果你需要精细控制不同操作的超时时间,可以在每次读写前临时修改SO_RCVTIMEO/SO_SNDTIMEO。虽然有点开销,但胜在灵活。
好了,超时控制这块就聊到这。记住一句话:没有超时的网络程序,就像没有刹车的汽车。 你永远不知道下一秒会不会撞上什么。
公众号:蓝海资料掘金营,微信deep3321