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 设置为 EAGAINEWOULDBLOCK
  • 只对阻塞模式的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,需额外处理
推荐程度 强烈推荐 不推荐

知识体系与核心逻辑

下面这张图,帮你理清超时控制的整体脉络。

超时控制核心逻辑 网络超时控制 setsockopt 方式 alarm 信号方式 SO_RCVTIMEO SO_SNDTIMEO SIGALRM 信号 alarm() 函数 返回 EAGAIN / EWOULDBLOCK 返回 EINTR,需重试 ✅ 强烈推荐 ❌ 不推荐新用

避坑指南

  • 不要混用两种方式。 同一个socket上同时用setsockopt和alarm,行为未定义。
  • 超时时间别设太短。 我见过有人设100微秒,结果大部分正常请求都超时了。网络延迟、系统调度都有开销,建议至少留几百毫秒。
  • 检查返回值时,先判断 errno。 很多系统调用在超时后返回 -1,但errno是EAGAIN。别把它当成致命错误。
  • alarm 的 SIGALRM 默认会终止进程。 如果你非要用,记得先 signal(SIGALRM, handler) 注册处理函数。

一个小技巧: 如果你需要精细控制不同操作的超时时间,可以在每次读写前临时修改SO_RCVTIMEO/SO_SNDTIMEO。虽然有点开销,但胜在灵活。

好了,超时控制这块就聊到这。记住一句话:没有超时的网络程序,就像没有刹车的汽车。 你永远不知道下一秒会不会撞上什么。


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