通过socket系统调用错误码分析网络异常
2020-07-16
在进行网络编程中,经常会遇到一些奇怪的网络异常或错误,这些错误其实通过分析socket 的send和recv函数的返回值和错误码就可以得到具体的错误原因,进而可以对症下药解决网络问题。
send的返回值分析
返回值 | 错误码 | 含义 | 解决措施 |
---|---|---|---|
>0 | 无 | 返回值表示成功拷贝到发送缓冲区的字节数 | 无 |
=0 | 无 | 发送的数据长度为0 | 检查发送的数据是否为空 |
-1 | EACCES | SO_BROADCAST没被设置却尝试向一个广播地址发送数据 | 检查发送地址是否正确,或设置SO_BROADCAST |
-1 | EAGAIN | 非阻塞模式读操作被阻塞或者读超时 | 正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次 |
-1 | EWOULDBLOCK | 非阻塞模式下无数据可读或接收操作被阻塞或者接收超时 | 正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次 |
-1 | EBADF | 使用的sockfd是无效 | 检查socket的建立是否成功 |
-1 | ECONNRESET | 本连接收到了rst包,对方异常关闭了双方连接,本连接已经关闭了 | Connection reset by peer,在收到RST后的socket继续发送数据,会生成SIGPIPE信号, 导致进程退出(默认的系统处理SIGPIPE信号的方式) 。如果对 SIGPIPE 进行忽略处理, 二次调用write方法时, 会返回-1, 同时errno置为SIGPIPE。这类情况的处理方法是遇到ECONNRESET的错误码就调用close关闭连接。 |
-1 | EFAULT | 访问了无效的用户地址空间,即指向缓冲区的指针有误 | 检查缓冲区指针是否有分配空间,空间是否异常回收了 |
-1 | EHOSTUNREACH | 对方地址不可达 | 检查一下对方的网络状态,如ping探测一下 |
-1 | EINTR | 由于信号中断,没写成功任何数据。 | 正常,本次调用没有写入任何数据到发生缓冲区,下次调用需要再次写入 |
-1 | EMSGSIZE | 发送的数据过大,无法发送 | 出现在UDP发送数据时,发送的数据大于MTU 1500字节时发生,可以 调整发送数据的大小,分批发送;或者调大MTU,但不推荐 |
-1 | ENETDOWN | 本地网络接口无法正常工作 | 检查网卡,检查网络是否正常 |
-1 | ENETUNREACH | 无网络路由 | 检查路由 |
-1 | ENOBUFS | 网卡的发送队列已满,一般表示网卡无法发送数据了 | UDP头域有一个16bit的字段用于标识UDP的包大小,所以一个UDP包数据长度最大为65K |
-1 | EPIPE | 尝试往一个已经断开的socket发送数据,第一次会产生ECONNRESET,如果后续继续这样做,系统会产生SIGPIPE信号通知线程,错误码变为EPIPE | 会产生broken pipe的错误,正确处理方式是close掉这个连接。建议应用根据需要处理SIGPIPE信号,至少不要用系统缺省的处理方式处理这个信号,系统缺省的处理方式是退出进程,这样你的应用就很难查处处理进程为什么退出。 |
-1 | EADDRNOTAVAIL | 指定的本地地址已经不再可用了 | ifconif查看自己的IP是否变化了,是否与socket使用的IP不同 |
-1 | EINVAL | 函数传参错误 | 检查传参 |
recv的返回值分析
返回值 | 错误码 | 含义 | 解决措施 |
---|---|---|---|
>0 | 无 | 成功接收到的数据字节数 | 无 |
0 | 无 | 对方已正常关闭连接 | 应用进程调用close关闭本方连接 |
-1 | EAGAIN | 非阻塞模式下无数据可读或接收操作被阻塞或者接收超时 | 正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次 |
-1 | EWOULDBLOCK | 非阻塞模式下无数据可读或接收操作被阻塞或者接收超时 | 正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次 |
-1 | EBADF | 使用的sockfd是无效 | 检查socket的建立是否成功 |
-1 | ECONNRESET | 本连接收到了rst包,对方异常关闭了双方连接,本连接已经关闭了 | Connection reset by peer,在收到RST后的socket继续读数据,会生成SIGPIPE信号, 导致进程退出(默认的系统处理SIGPIPE信号的方式) 。如果对 SIGPIPE 进行忽略处理, 二次调用recv方法时, 会返回-1, 同时errno置为SIGPIPE。这类情况的处理方法是遇到ECONNRESET的错误码就调用close关闭连接。 |
-1 | EFAULT | 访问了无效的用户地址空间,即指向缓冲区的指针有误 | 检查缓冲区指针是否有分配空间,空间是否异常回收了 |
-1 | EINTR | 由于信号中断返回,没有任何数据可用。 | 正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次 |
-1 | ENOBUFS | 系统无法再分配内部缓冲区,内存不足 | 系统层面的问题,检查系统内存使用情况 |
-1 | ENOTCONN | 面向连接的socket尚未连接 | 检查是否判断了connet的状态,可能是connet失败了但自己还是继续调用recv读数据 |
-1 | ETIMEDOUT | 连接超时,TCP keepalive 超时触发 | 对方已经可能连接关闭了,我们调用close关闭自己的连接 |
-1 | EINVAL | 函数传参错误 | 检查传参 |
-1 | ECONNREFUSED | 对方拒绝网络连接 | 调用close关连接,检查接收方的网络访问策略 |
-1 | EPIPE | 尝试往一个已经断开的socket读取数据,第一次会产生ECONNRESET,如果后续继续这样做,系统会产生SIGPIPE信号通知线程,错误码变为EPIPE | 会产生broken pipe的错误,正确处理方式是close掉这个连接。建议应用根据需要处理SIGPIPE信号,至少不要用系统缺省的处理方式处理这个信号,系统缺省的处理方式是退出进程,这样你的应用就很难查处处理进程为什么退出。 |
特别:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。
总结常见的网络异常情况
1. bind()时的address already used
bind()时失败,错误码为EADDRINUSE。
原因:
- 有线程或进程占用着该IP和端口,导致bind失败。
- 进程运行,然后重启了,因为有time_wait状态的存在,需要等待2msl的时间才能释放端口,在释放端口前进行bind,也会失败。
- 有进程使用了0.0.0.0绑定了相同的端口(0.0.0.0表示所有本地网络地址)。在默认设置下,没有socket能够绑定到同一地址的同一端口。比如在Socket A已经绑定了0.0.0.0:8000以后,Socket B若是想要绑定192.168.0.100:8000,那就会报EADDRINUSE。因为Socket A已经绑定了所有ip地址的8000端口,包括192.168.0.100:8000。
地址冲突情况列举:
SO_REUSEADDR | socketA | socketB | Result |
---|---|---|---|
ON/OFF | 192.168.0.1:21 | 192.168.0.1:21 | Error (EADDRINUSE) |
ON/OFF | 192.168.0.1:21 | 10.0.0.1:21 | OK |
ON/OFF | 10.0.0.1:21 | 192.168.0.1:21 | OK |
OFF | 0.0.0.0:21 | 192.168.1.0:21 | Error (EADDRINUSE) |
OFF | 192.168.1.0:21 | 0.0.0.0:21 | Error (EADDRINUSE) |
ON | 0.0.0.0:21 | 192.168.1.0:21 | OK |
ON | 192.168.1.0:21 | 0.0.0.0:21 | OK |
ON/OFF | 0.0.0.0:21 | 0.0.0.0:21 | Error (EADDRINUSE) |
解决:
setsockopt里设置SO_REUSEADDR。设置了SO_REUSEADDR以后,判断冲突的方式就变了。只要地址不是正好(exactly)相同,那么多个Socket就能绑定到同一ip上。比如0.0.0.0和192.168.0.100,虽然逻辑意义上前者包含了后者,但是0.0.0.0泛指所有本地ip,而192.168.0.100特指某一ip,两者并不是完全相同,所以Socket B尝试绑定的时候,不会再报EADDRINUSE,而是绑定成功。另外,SO_REUSEADDR的另一个作用是,可以绑定TIME_WAIT状态的地址。
2. connect()时的address already used
Connect()失败,错误码为EADDRINUSE。
在默认情况下,一般在bind()时可能会出现EADDRINUSE问题,bind()时因为src ip和src port已经不同,不可能报EADDRINUSE。但是在SO_REUSEADDR和SO_REUSEPORT下,因为地址有重用,那么当重用的地址端口尝试连接同一个远端主机的同一端口时(connect()时),就会报EADDRINUSE。
比如本机只有两个地址,127.0.0.1和192.168.0.1,其中后者是可访问因特网的网卡的地址。在SO_REUSEADDR下,并且Socket A绑定了Socket A0.0.0.0:8000, Socket B绑定了192.168.0.1:8000以后,Socket A发起了与远端主机111.13.101.208:80的连接。此时根据路由表规则,连接将被绑定到192.168.0.1,产生的连接ID为{<SOCK_STREAM>, <192.168.0.1>, <8000>, <111.13.101.208>, <80>},Socket A连接成功。但是如果Socket B也想尝试发起与远端主机111.13.101.208:80的连接,就会产生一样的连接ID,所以报了EADDRINUSE。
linux kernel 3.9 引入了最新的SO_REUSEPORT选项,使得多进程或者多线程创建多个绑定同一个ip:port的监听socket,提高服务器的接收链接的并发能力,程序的扩展性更好;此时需要设置SO_REUSEPORT(注意所有进程都要设置才生效)。
使用方法:
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT,(const void *)&reuse , sizeof(int));
SO_REUSEPORT的三个作用:
- 每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()、accept()和connect();提高接收连接的能力。(例如nginx多进程同时监听同一个ip:port)
- 避免了应用层多线程或者进程监听同一ip:port的“惊群效应”。
- 内核层面实现负载均衡,保证每个进程或者线程接收均衡的连接数。
SO_REUSEPORT套接字选项起作用的三个前提:
- 本选项允许完全重复的捆绑,不过只有在想要捆绑同一IP地址和端口的每个套接字都指定了本套接字选项才行。
- 如果被捆绑的IP地址是一个多播地址,那么SO_REUSEADDR和SO_REUSEPORT被认为是等效的
- 只进程用户组相同的服务器进程才能监听同一ip:port (安全性考虑),一个用户便不能窃取其他用户的端口,这一点不同于SO_REUSEADDR。
现在已经存在一个没有启用SO_REUSEPORT选项的套接字,而另外一个设置了SO_REUSEPORT选项的套接字要绑定与第一个套接字相同的地址和端口,这种情况会绑定失败,就算已绑定套接字是一个处于在TIME_WAIT状态的连接,也无法成功;要绑定一个与TIME_WAIT状态相同地址和端口的套接字,要么设置在新套接字上设置SO_REUSEADDR选项,要么在原套接字和将要绑定的套接字上都设置SO_REUSEPORT选项;当然也允许同时设置SO_REUSEADDR和SO_REUSEPORT选项;
4. connect reset by peer
这个提示的出现的情景:己方socket给对方发送数据时,对方因为异常情况回了RST包,己方系统也会关闭这个连接,回收相应的socket资源,并往上通知应用进程,当应用程序调用recv或send进行数据读写时,其返回值为-1,error被设为ECONNRESET,要求应用程序自行处理该异常。
因此,当网络编程中需要处理send()范围值为-1的异常情况,同时检查errorn是否为ECONNRESET,如果是错误码是ECONNRESET,就表明对方已经异常关闭了连接,我们系统收到RST包会也会断掉连接,此时的socket已经是不可用了。此时我们应该调用close(),结束本次通信。
5. broken pipe
如果我们无视ECONNRESET错误,继续往已被RST关闭的连接发送数据时,就会触发“broken pipe”的错误提示,而且send的返回值仍为-1.error被设为EPIPE。因为该连接已经断开了,因此还往这个socket发送数据是一个未定义的行为,因此需要避免这个行为,因为很危险,可能会导致进程异常退出。因为发生这种向一个因为RST关闭的连接发送数据,系统会产生一个SIGPIPE的信号,通知进程处理,但是进程没有接管这个信号,系统默认会杀死进程。
因此针对该情况的解决方法就是,判断我们send的返回值是否为-1且error为ECONNRESET,是则马上close掉连接。