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 打断?基本上所有阻塞的系统调用都可能:read、write、accept、connect、recv、send、poll、epoll_wait 等等。你想想看,只要涉及等待的,信号一来就可能中断。
三、EINPROGRESS:非阻塞 connect 的专属信号
这个错误只出现在非阻塞 connect() 中。
当你对一个非阻塞 socket 调用 connect 时,连接不可能立即建立(毕竟要三次握手)。内核会告诉你:「连接已经开始,但还没完成,你先去干别的,等好了我通知你。」
处理流程是这样的:
- 设置 socket 为非阻塞
- 调用 connect(),返回 -1,errno 为 EINPROGRESS
- 把这个 fd 加入 epoll 或 select,监听可写事件
- 当 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 或缓冲区限制 | 分片发送 |
五、知识体系:一张图看懂
下面这张图总结了网络错误处理的整体思路。你写代码时照着这个流程走,基本不会漏掉关键点。
六、实战建议:写代码时要注意的
说了这么多理论,最后给几个实在的建议:
- 所有系统调用都要检查返回值。别偷懒,别以为不会出错。网络环境复杂得很,一个 RST 包就能让你的程序崩溃。
- EINTR 必须重试。这是最容易被忽略的。我见过太多代码只检查了 EAGAIN,没处理 EINTR,结果线上服务偶尔丢连接。
- 非阻塞模式下,EAGAIN 不是错误。别打日志,别计数,直接返回 0 表示「本次无数据」就行。打日志会拖慢性能。
- EINPROGRESS 要配合超时使用。非阻塞 connect 如果一直不成功,你得有个超时机制。我一般用 epoll 的 timeout 参数,设 3 秒。
- ECONNRESET 和 EPIPE 要区分对待。ECONNRESET 是读的时候发现的,EPIPE 是写的时候发现的。前者说明对端已经关了,后者说明你还在往已关闭的连接写数据。
最后说一句:网络编程的本质就是处理各种「意外」。你处理得越细致,程序就越健壮。别嫌麻烦,这些错误码的处理逻辑,写一次就能用一辈子。