通过socket系统调用错误码分析网络异常

2020-07-16

在进行网络编程中,经常会遇到一些奇怪的网络异常或错误,这些错误其实通过分析socket 的send和recv函数的返回值和错误码就可以得到具体的错误原因,进而可以对症下药解决网络问题。

send的返回值分析

返回值错误码含义解决措施
>0返回值表示成功拷贝到发送缓冲区的字节数
=0发送的数据长度为0检查发送的数据是否为空
-1EACCESSO_BROADCAST没被设置却尝试向一个广播地址发送数据检查发送地址是否正确,或设置SO_BROADCAST
-1EAGAIN非阻塞模式读操作被阻塞或者读超时正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次
-1EWOULDBLOCK非阻塞模式下无数据可读或接收操作被阻塞或者接收超时正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次
-1EBADF使用的sockfd是无效检查socket的建立是否成功
-1ECONNRESET本连接收到了rst包,对方异常关闭了双方连接,本连接已经关闭了Connection reset by peer,在收到RST后的socket继续发送数据,会生成SIGPIPE信号, 导致进程退出(默认的系统处理SIGPIPE信号的方式) 。如果对 SIGPIPE 进行忽略处理, 二次调用write方法时, 会返回-1, 同时errno置为SIGPIPE。这类情况的处理方法是遇到ECONNRESET的错误码就调用close关闭连接。
-1EFAULT访问了无效的用户地址空间,即指向缓冲区的指针有误检查缓冲区指针是否有分配空间,空间是否异常回收了
-1EHOSTUNREACH对方地址不可达检查一下对方的网络状态,如ping探测一下
-1EINTR由于信号中断,没写成功任何数据。正常,本次调用没有写入任何数据到发生缓冲区,下次调用需要再次写入
-1EMSGSIZE发送的数据过大,无法发送出现在UDP发送数据时,发送的数据大于MTU 1500字节时发生,可以 调整发送数据的大小,分批发送;或者调大MTU,但不推荐
-1ENETDOWN本地网络接口无法正常工作检查网卡,检查网络是否正常
-1ENETUNREACH无网络路由检查路由
-1ENOBUFS网卡的发送队列已满,一般表示网卡无法发送数据了UDP头域有一个16bit的字段用于标识UDP的包大小,所以一个UDP包数据长度最大为65K
-1EPIPE尝试往一个已经断开的socket发送数据,第一次会产生ECONNRESET,如果后续继续这样做,系统会产生SIGPIPE信号通知线程,错误码变为EPIPE会产生broken pipe的错误,正确处理方式是close掉这个连接。建议应用根据需要处理SIGPIPE信号,至少不要用系统缺省的处理方式处理这个信号,系统缺省的处理方式是退出进程,这样你的应用就很难查处处理进程为什么退出。
-1EADDRNOTAVAIL指定的本地地址已经不再可用了ifconif查看自己的IP是否变化了,是否与socket使用的IP不同
-1EINVAL函数传参错误检查传参

recv的返回值分析

返回值错误码含义解决措施
>0成功接收到的数据字节数
0对方已正常关闭连接应用进程调用close关闭本方连接
-1EAGAIN非阻塞模式下无数据可读或接收操作被阻塞或者接收超时正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次
-1EWOULDBLOCK非阻塞模式下无数据可读或接收操作被阻塞或者接收超时正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次
-1EBADF使用的sockfd是无效检查socket的建立是否成功
-1ECONNRESET本连接收到了rst包,对方异常关闭了双方连接,本连接已经关闭了Connection reset by peer,在收到RST后的socket继续读数据,会生成SIGPIPE信号, 导致进程退出(默认的系统处理SIGPIPE信号的方式) 。如果对 SIGPIPE 进行忽略处理, 二次调用recv方法时, 会返回-1, 同时errno置为SIGPIPE。这类情况的处理方法是遇到ECONNRESET的错误码就调用close关闭连接。
-1EFAULT访问了无效的用户地址空间,即指向缓冲区的指针有误检查缓冲区指针是否有分配空间,空间是否异常回收了
-1EINTR由于信号中断返回,没有任何数据可用。正常,本次调用无数据可读,可以继续处理后面逻辑,下一个循环再读一次
-1ENOBUFS系统无法再分配内部缓冲区,内存不足系统层面的问题,检查系统内存使用情况
-1ENOTCONN面向连接的socket尚未连接检查是否判断了connet的状态,可能是connet失败了但自己还是继续调用recv读数据
-1ETIMEDOUT连接超时,TCP keepalive 超时触发对方已经可能连接关闭了,我们调用close关闭自己的连接
-1EINVAL函数传参错误检查传参
-1ECONNREFUSED对方拒绝网络连接调用close关连接,检查接收方的网络访问策略
-1EPIPE尝试往一个已经断开的socket读取数据,第一次会产生ECONNRESET,如果后续继续这样做,系统会产生SIGPIPE信号通知线程,错误码变为EPIPE会产生broken pipe的错误,正确处理方式是close掉这个连接。建议应用根据需要处理SIGPIPE信号,至少不要用系统缺省的处理方式处理这个信号,系统缺省的处理方式是退出进程,这样你的应用就很难查处处理进程为什么退出。

特别:返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。

总结常见的网络异常情况

1. bind()时的address already used

bind()时失败,错误码为EADDRINUSE。

原因:

  1. 有线程或进程占用着该IP和端口,导致bind失败。
  2. 进程运行,然后重启了,因为有time_wait状态的存在,需要等待2msl的时间才能释放端口,在释放端口前进行bind,也会失败。
  3. 有进程使用了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_REUSEADDRsocketAsocketBResult
ON/OFF192.168.0.1:21192.168.0.1:21Error (EADDRINUSE)
ON/OFF192.168.0.1:2110.0.0.1:21OK
ON/OFF10.0.0.1:21192.168.0.1:21OK
OFF0.0.0.0:21192.168.1.0:21Error (EADDRINUSE)
OFF192.168.1.0:210.0.0.0:21Error (EADDRINUSE)
ON0.0.0.0:21192.168.1.0:21OK
ON192.168.1.0:210.0.0.0:21OK
ON/OFF0.0.0.0:210.0.0.0:21Error (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的三个作用:

  1. 每一个进程有一个独立的监听socket,并且bind相同的ip:port,独立的listen()、accept()和connect();提高接收连接的能力。(例如nginx多进程同时监听同一个ip:port)
  2. 避免了应用层多线程或者进程监听同一ip:port的“惊群效应”。
  3. 内核层面实现负载均衡,保证每个进程或者线程接收均衡的连接数。

SO_REUSEPORT套接字选项起作用的三个前提:

  1. 本选项允许完全重复的捆绑,不过只有在想要捆绑同一IP地址和端口的每个套接字都指定了本套接字选项才行。
  2. 如果被捆绑的IP地址是一个多播地址,那么SO_REUSEADDR和SO_REUSEPORT被认为是等效的
  3. 只进程用户组相同的服务器进程才能监听同一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掉连接。