20、超时控制:setsockopt()设置收发超时、alarm()信号超时、select()超时。

网络编程里,超时控制是个绕不开的话题。你想想看,一个 socket 连上去之后,如果对端一直不回复数据,你的程序怎么办?傻等?那肯定不行。我刚开始写网络程序那会儿,就吃过这个亏——一个客户端连上服务器后,服务器挂了,客户端就卡在 recv() 那里一动不动,像个木头人。后来我才明白,超时控制是每个网络程序员必须掌握的技能。

今天咱们就聊聊三种常见的超时控制手段:setsockopt() 设置收发超时alarm() 信号超时select() 超时。这三种方法各有各的适用场景,我一个个给你拆解。

一、三种超时控制方式对比

先看个总览,心里有个谱:

方式 原理 适用场景 优点 缺点
setsockopt() 设置 socket 选项 SO_RCVTIMEO / SO_SNDTIMEO 阻塞式 recv() / send() 超时 简单、直接、线程安全 只能用于阻塞模式,粒度较粗
alarm() 利用 SIGALRM 信号中断阻塞调用 整体操作超时(如 connect) 可以中断任何阻塞系统调用 信号处理复杂,多线程下坑多
select() 用 select 的第五个参数控制等待时间 多路复用 + 超时控制 灵活、可同时监控多个 fd 代码稍复杂,大并发下性能不如 epoll
核心观点:没有银弹。三种方式各有利弊,实际项目中我经常混合使用。比如用 select() 做整体超时框架,内部再用 setsockopt() 做单次收发的兜底。

二、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;
}
避坑指南:我曾经在项目中遇到过一个诡异的问题——设置了 SO_RCVTIMEO 后,recv() 返回 -1 但 errno 不是 EAGAIN。后来发现是内核版本差异导致的。有些老内核在超时后返回 EINTR。所以建议你判断超时时,把 EAGAIN、EWOULDBLOCK、EINTR 都考虑进去。

三、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;
}
警告:alarm() 是进程级别的。如果你在多线程程序里用 alarm(),SIGALRM 信号会随机发给某个线程。这会导致不可预知的行为。我个人建议:多线程环境下,不要用 alarm(),改用 select() 或 setsockopt()。

四、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);
}
实战经验:我在做一个物联网网关项目时,需要同时监控几十个设备连接。每个连接的超时时间还不一样——有的设备要求 3 秒超时,有的可以容忍 10 秒。我用 select() 配合一个最小堆来管理超时时间,效果非常好。select() 的灵活性在这里体现得淋漓尽致。

五、三种方式的选择策略

说了这么多,到底该用哪种?我根据自己的经验给你一个参考:

  • 简单客户端:用 setsockopt() 就够了。代码少,逻辑清晰。
  • 单线程、需要中断 connect():可以用 alarm(),但记得处理好信号。
  • 多线程服务器:用 select() 或 poll()/epoll()。别碰 alarm()。
  • 需要同时监控多个 fd:select() 是底线方案,大并发建议用 epoll。

嗯,这里还要注意一点:超时时间设置要合理。我见过有人把超时设成 1 毫秒,结果网络稍微波动一下程序就报超时。也见过有人设成 60 秒,用户等得都想砸键盘。一般来说,局域网内 3-5 秒比较合理,公网环境可以放宽到 10-15 秒。

六、知识体系总览

下面这张图帮你梳理一下三种超时控制方式的关系和适用场景:

Socket 超时控制三种方式 超时控制 setsockopt() SO_RCVTIMEO / SO_SNDTIMEO 阻塞 recv/send 超时 优点:简单、线程安全 缺点:仅限阻塞模式 alarm() 信号 SIGALRM 中断系统调用 适合 connect() 超时 优点:可中断任何调用 缺点:多线程不安全 select() 第五个参数 timeval 多路复用 + 超时 优点:灵活、可监控多 fd 缺点:大并发性能差 推荐:简单场景用 setsockopt,多线程用 select,单线程特殊场景用 alarm

好了,三种超时控制方式就聊到这儿。每种方式都有它的脾气,选对了事半功倍,选错了坑你没商量。我个人建议你从 select() 开始练手,它虽然代码量稍大,但能帮你建立更清晰的超时控制思维。等你用熟了,再根据场景灵活搭配其他方式。

最后说一句:不管用哪种方式,超时后的错误处理一定要做干净。释放资源、关闭连接、记录日志,一个都不能少。我曾经因为超时后忘了 close socket,导致文件描述符泄漏,服务器跑了三天就挂了。教训深刻啊。

公众号:蓝海资料掘金营,微信deep3321