98、常见网络错误:EAGAIN/EINTR/EINPROGRESS处理、错误码大全

网络编程里,错误处理是门手艺活。

我见过不少新手,写了个 socket 程序,一跑就崩。查了半天,发现是没处理 EAGAIN。还有更惨的,线上服务莫名其妙挂掉,最后定位到是 EINTR 信号中断了阻塞调用。嗯,这些坑我都踩过。

今天咱们就把这些常见的网络错误码掰扯清楚。说白了,就是让你知道:哪些错误可以忽略重试,哪些必须立即止损

核心原则:网络编程中,大部分错误不是「挂了」,而是「还没准备好」。学会区分「暂时不可用」和「永久失败」,是写出健壮网络程序的第一步。

一、EAGAIN / EWOULDBLOCK:不是错误,是提醒

这两个错误码其实是一个东西(在 Linux 上值相同)。

什么意思呢?你调用 read()write() 时,内核告诉你:「哥们,现在没数据可读,或者缓冲区满了写不进去,你等会儿再来。」

我刚开始写非阻塞 socket 时,一看到 EAGAIN 就以为出大事了,直接打印错误日志然后 close。后来发现,这其实是正常流程的一部分。

我的习惯:在非阻塞模式下,EAGAIN 出现时,我会把它当作「无事发生」,继续下一轮 epoll 等待。只有在连续出现 N 次(比如 10 次)EAGAIN 后,我才会考虑是不是对端已经挂了。

处理方式很简单:

// 非阻塞 socket 读取示例
ssize_t n = read(fd, buf, sizeof(buf));
if (n == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 没数据可读,下次再来
        return 0;  // 表示本次没读到数据,不是错误
    }
    // 真正的错误处理
    perror("read error");
    close(fd);
    return -1;
}

注意一点:阻塞模式下几乎不会出现 EAGAIN。除非你设置了超时(比如 SO_RCVTIMEO),超时后 read 返回 -1,errno 可能是 EAGAIN。

二、EINTR:被信号打断了,重试就行

这个错误我印象特别深。有一次线上服务莫名其妙丢请求,查了半天,发现是某个阻塞的 accept() 被 SIGCHLD 信号打断了,返回了 EINTR。而代码里直接把它当错误处理了,导致连接丢失。

EINTR 的意思是:系统调用在执行过程中被信号中断了。这不是网络问题,也不是对端问题,纯粹是内核告诉你:「刚才有个信号来了,我暂停了一下你的系统调用,现在你可以重新调一次。」

处理方式:

// 正确的 EINTR 处理模式
ssize_t robust_read(int fd, void *buf, size_t count) {
    ssize_t n;
    do {
        n = read(fd, buf, count);
    } while (n == -1 && errno == EINTR);
    return n;
}

// 或者用更简洁的写法
while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR)
    ;  // 空循环,继续重试

我曾经踩过的坑:在信号处理函数里调用了非异步信号安全的函数(比如 printf),导致死锁。记住:信号处理函数里能做的事非常有限,最好只设置一个 volatile sig_atomic_t 标志位,然后在主循环里检查。

哪些系统调用会被 EINTR 打断?基本上所有阻塞的系统调用都可能:readwriteacceptconnectrecvsendpollepoll_wait 等等。你想想看,只要涉及等待的,信号一来就可能中断。

三、EINPROGRESS:非阻塞 connect 的专属信号

这个错误只出现在非阻塞 connect() 中。

当你对一个非阻塞 socket 调用 connect 时,连接不可能立即建立(毕竟要三次握手)。内核会告诉你:「连接已经开始,但还没完成,你先去干别的,等好了我通知你。」

处理流程是这样的:

  1. 设置 socket 为非阻塞
  2. 调用 connect(),返回 -1,errno 为 EINPROGRESS
  3. 把这个 fd 加入 epoll 或 select,监听可写事件
  4. 当 epoll 返回可写时,用 getsockopt 检查连接是否成功
// 非阻塞 connect 的标准写法
int do_nonblock_connect(int sockfd, struct sockaddr *addr, socklen_t addrlen) {
    // 设置非阻塞
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    int ret = connect(sockfd, addr, addrlen);
    if (ret == 0) {
        // 运气好,连接立即建立(比如本机回环)
        return 0;
    }
    if (errno != EINPROGRESS) {
        // 真正的连接失败
        return -1;
    }

    // 连接正在进行中,等待 epoll 通知
    // ... 这里用 epoll_wait 等待可写事件 ...
    
    // 检查连接结果
    int error = 0;
    socklen_t len = sizeof(error);
    if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) == -1) {
        return -1;
    }
    if (error != 0) {
        errno = error;
        return -1;
    }
    return 0;
}

我个人习惯:非阻塞 connect 的等待时间不要设太长。一般 3-5 秒就够了。如果对端没响应,直接超时处理,别傻等。我曾经遇到过对端防火墙直接丢弃 SYN 包的情况,等了 30 秒才超时,用户体验极差。

四、错误码大全:一张表搞定

下面是我整理的网络编程中最常见的错误码。你把它存下来,遇到问题直接查表。

错误码 含义 常见场景 处理方式
EAGAIN / EWOULDBLOCK 资源暂时不可用 非阻塞 read/write 无数据或缓冲区满 忽略,下次重试
EINTR 系统调用被信号中断 任何阻塞调用被信号打断 重新调用
EINPROGRESS 操作正在进行中 非阻塞 connect 等待可写事件后检查结果
ECONNRESET 对端重置连接 对端崩溃或主动关闭后继续读写 关闭 socket,重新连接
EPIPE 管道破裂 向已关闭的 socket 写数据 关闭 socket,停止写入
ETIMEDOUT 连接超时 connect 或 send 超时 重试或放弃
ECONNREFUSED 连接被拒绝 目标端口未监听 检查服务端是否启动
EHOSTUNREACH 主机不可达 网络不通或路由问题 检查网络配置
ENETUNREACH 网络不可达 网关或路由问题 检查网络连接
EADDRINUSE 地址已被使用 bind 时端口被占用 换端口或设置 SO_REUSEADDR
EADDRNOTAVAIL 地址不可用 绑定的 IP 不是本机地址 检查 IP 配置
EMSGSIZE 消息太长 send 数据超过 MTU 或缓冲区限制 分片发送

五、知识体系:一张图看懂

下面这张图总结了网络错误处理的整体思路。你写代码时照着这个流程走,基本不会漏掉关键点。

网络错误处理决策树 系统调用返回 -1 检查 errno 值 EAGAIN / EWOULDBLOCK EINTR EINPROGRESS 非阻塞:忽略,继续等待 阻塞:检查是否设了超时 立即重试系统调用 用 while 循环包裹 加入 epoll 等待可写 用 getsockopt 检查结果 其他错误:关闭 socket,记录日志

六、实战建议:写代码时要注意的

说了这么多理论,最后给几个实在的建议:

  • 所有系统调用都要检查返回值。别偷懒,别以为不会出错。网络环境复杂得很,一个 RST 包就能让你的程序崩溃。
  • EINTR 必须重试。这是最容易被忽略的。我见过太多代码只检查了 EAGAIN,没处理 EINTR,结果线上服务偶尔丢连接。
  • 非阻塞模式下,EAGAIN 不是错误。别打日志,别计数,直接返回 0 表示「本次无数据」就行。打日志会拖慢性能。
  • EINPROGRESS 要配合超时使用。非阻塞 connect 如果一直不成功,你得有个超时机制。我一般用 epoll 的 timeout 参数,设 3 秒。
  • ECONNRESET 和 EPIPE 要区分对待。ECONNRESET 是读的时候发现的,EPIPE 是写的时候发现的。前者说明对端已经关了,后者说明你还在往已关闭的连接写数据。

最后说一句:网络编程的本质就是处理各种「意外」。你处理得越细致,程序就越健壮。别嫌麻烦,这些错误码的处理逻辑,写一次就能用一辈子。

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