超时机制:alarm()信号超时、select()超时、setsockopt()超时
网络编程里,超时处理是个绕不开的话题。你想想看,一个socket连不上,总不能一直等下去吧?我在早期做项目时,就吃过这个亏——一个客户端连不上服务器,程序直接卡死了,用户那边还以为崩溃了。后来我学乖了,每个网络操作都得配上超时。
C语言里实现超时,主要有三条路:alarm()信号、select()轮询、setsockopt()选项。这三条路各有各的脾气,咱们一条一条捋清楚。
1. alarm() 信号超时
alarm()是Unix系统里最古老的超时手段。它本质上是让内核在指定秒数后,给当前进程发一个SIGALRM信号。如果你没处理这个信号,进程就挂了。
我个人的习惯是,用它来给read()、write()这类阻塞操作加个“闹钟”。代码长这样:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
void timeout_handler(int sig) {
// 啥也不干,就是为了打断阻塞调用
// 我一般在这里写个空函数
}
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// ... 连接服务器 ...
signal(SIGALRM, timeout_handler);
alarm(5); // 5秒后触发信号
char buf[1024];
int n = read(sockfd, buf, sizeof(buf));
if (n < 0 && errno == EINTR) {
printf("读取超时了\n");
}
alarm(0); // 取消闹钟
return 0;
}
另外,alarm()在多线程环境下不太安全。信号是发给进程的,不是线程。你想想看,如果两个线程都在等闹钟,到底谁收到信号?嗯,这问题挺麻烦的。所以我建议,多线程项目就别用alarm()了。
2. select() 超时
select()是我用得最多的超时方案。它不光能设超时,还能同时监控多个socket。说白了,它就是让你告诉内核:“我等着这些socket,最多等xx时间,超时了你就回来告诉我。”
核心用法是这样的:
#include <sys/select.h>
#include <sys/time.h>
struct timeval tv;
tv.tv_sec = 3; // 3秒
tv.tv_usec = 0; // 0微秒
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
int ret = select(sockfd + 1, &readfds, NULL, NULL, &tv);
if (ret == -1) {
perror("select出错");
} else if (ret == 0) {
printf("超时了,3秒内没数据\n");
} else {
if (FD_ISSET(sockfd, &readfds)) {
// 可以读了
read(sockfd, buf, sizeof(buf));
}
}
关键点:select()返回后,timeval结构体里的值会被修改。Linux下会减去已经等待的时间,但有些系统不会。所以我每次调用前都会重新设置超时值。
为什么我喜欢用select()?因为它精度高——微秒级。而且它不依赖信号,线程安全。我在做一个高并发网关时,就是用select()来管理上千个连接的读写超时。
小技巧:如果你只想检查一个socket,可以把nfds设成sockfd + 1,readfds只放这一个。这样性能更好。别傻乎乎地设成FD_SETSIZE,那是浪费。
3. setsockopt() 超时
这是最“优雅”的方式——直接通过socket选项设置超时。内核会在收发操作时自动检查时间,超时了就返回错误。
两个关键选项:
SO_RCVTIMEO:接收超时SO_SNDTIMEO:发送超时
用法示例:
#include <sys/socket.h>
#include <sys/time.h>
struct timeval timeout;
timeout.tv_sec = 5; // 5秒
timeout.tv_usec = 0;
// 设置接收超时
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO,
&timeout, sizeof(timeout)) < 0) {
perror("setsockopt失败");
}
// 之后调用read,如果5秒内没数据,返回-1,errno为EAGAIN或EWOULDBLOCK
char buf[1024];
int n = read(sockfd, buf, sizeof(buf));
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
printf("接收超时\n");
}
setsockopt()设置的超时是“每次操作”的超时,不是“整个连接”的超时。比如你循环读数据,每次read()都会重新计时。这和select()的“一次性超时”不一样。我刚开始用的时候没注意,结果发现每次读都等5秒,整个流程慢得离谱。
另外,setsockopt()的超时在有些系统上只对阻塞模式有效。非阻塞模式下,它可能不生效。嗯,这个坑我也踩过——在Linux上没问题,换到某BSD系统上就不行了。所以跨平台项目要小心。
三种方式对比
| 特性 | alarm() | select() | setsockopt() |
|---|---|---|---|
| 精度 | 秒级 | 微秒级 | 微秒级 |
| 线程安全 | 否 | 是 | 是 |
| 多socket监控 | 否 | 是 | 否 |
| 代码侵入性 | 低 | 中 | 低 |
| 可移植性 | POSIX | 几乎全平台 | 大部分平台 |
| 适用场景 | 简单阻塞操作 | 多路复用、高精度 | 单个socket的收发 |
核心逻辑图
下面这张图展示了三种超时机制在socket编程中的位置和关系:
我的建议
如果你问我选哪个,我的回答是:看场景。
- 简单脚本或小工具:用
alarm(),代码少,够用就行。 - 高并发服务器:用
select()或epoll(后面会讲),精度高,可控性强。 - 单个socket的收发:用
setsockopt(),最省事,不用改代码逻辑。
我个人在实际项目中,80%的情况用select(),15%用setsockopt(),5%用alarm()。为什么alarm()用得少?因为信号处理太容易出幺蛾子了。我曾经在一个项目里,alarm()和sleep()混用,结果闹钟信号把sleep()打断了,整个时序全乱套。从那以后,我对信号超时就格外谨慎。
避坑指南:如果你非要用alarm(),记得在信号处理函数里只设一个标志位,别做复杂操作。信号处理函数里调用printf()?千万别,那是未定义行为。我见过有人这么干,程序偶尔崩溃,查了两天才找到原因。
好了,超时机制就聊到这儿。三种方式各有千秋,选对工具,你的网络程序才能既健壮又高效。