SIGPIPE错误出现的一种场景和网络编程异常处理的梳理

网络编程中异常处理

SIGPIPE信号的使用

在涉及到网络交互的程序中,我们经常会在程序的伊始就执行一个信号注册

signal(SIGPIPE, SIG_IGN);

忽略了 SIGPIPE这个信号错误,那为什么要忽略这个错误?如果这个错误永远都是默认要被忽略,那其存在的意义是啥?通过查阅资料可以了解到,SIGPIPE信号出现的场景是在建立好连接,对端关闭之后,我们这端仍然连续地往套接字中发送消息,才会出现SIGPIPE信号。

那既然知道会出现这种错误情况,如果是对端已经关闭了套接字,就编程上进行判断,不往套接字里写不就搞定了么,而直接关闭套接字不就搞定了么?况且代码中确实会出现这样的错误么?

我对日志做了一下排查,发现了如下结果

[Func:yyy_get_status]: yyy_send ERROR!
[Func:xxx_writen]: writeSocket 2 os Error 32 Broken pipe
[Func:xxx_writen]: writeSocket 2 os Error 32 Broken pipe

排查过程中确认例如上面的日志出现的情况还是非常多的,在很多接口中都有频繁的出现。

一种出错导致SIGPIPE的场景与解决办法

从日志中可以了解到是 yyy_get_status 接口在执行时出错,导致触发了 broken pipe错误。该接口是app端通过 yyy_get_status 关键字请求的接口。设备端使用的web服务器 是多线程 多路复用响应模型,每一条连接由一个独立线程托管处理。从上下文的日志中搜寻发现,yyy_get_status 接口触发了两次,可以猜测APP端在第二次调用接口时,关闭了第一个连接,使得设备端进入了异常的处理分支。那这为何会引起设备端出现该SIG_PIPE的问题呢?分析代码流程

如下为这个接口进行响应的一个主要结构

    while (1)
    {
        sleep(100);
        ...
        /*organize data to pDest  & length iLen */
        ...
        while(writeLen

开头的sleep的是为了避免异常状态下出现死循环,占用cpu。假定在两次xxx_poll之间,也就是sleep的时间,对端关闭了套接字,执行xxx_poll会返回什么呢,这需要具体了解xxx_poll的调用

int xxx_poll(Socket *pSock, int iMask, int iWaitMilSec)
{
    struct pollfd   fd;

    memset(&fd,0, sizeof(fd));

    if(!pSock || !((iMask & MMM_READABLE) || (iMask & MMM_WRITABLE)))
    {
        return 0;
    }
    
    if (iMask & MMM_READABLE) {
        fd.events |= POLLIN | POLLHUP;
    }
    if (iMask & MMM_WRITABLE) {
        fd.events |= POLLOUT;
    }
    fd.fd = pSock->fd;
    
    return poll(&fd, 1, iWaitMilSec);
}

可以看到,该接口使用poll调用实现了读写的超时,在入参为 MMM_WRITABLE的时候,会通过poll 等待 socket处于可写的状态。

那对端关闭套接字的时候,poll是如何返回的呢?通过手写一个简单的交互demo可以知道,poll会返回1,这种情况相当于与 正常可写的情况返回结果一致,返回的是认为可写的fd event的数量。这种情况下,导致上层认为fd只是正常可写,执行第一次 send或者write,可以正常写入本地缓冲区,但会收到对端的一个RST消息;再第二次执行poll时,结果一致,也是返回1,第二次再往socket里面写消息,就会返回错误32,Broken pipe的错误,也就出现了上面的错误打印。如果此时没有注册忽略SIG_PIPE信号,就会引起崩溃。

那么应该如何操作呢?。

poll里面其实已经提供了 检测的机制,将 POLLHUP事件在写套接字时 也应加入监听事件列表。在poll返回时,需判定 POLLHUP事件是否触发,如果触发,则认为当前连接套接字已关闭,也直接关闭即可,这样便不会再执行写操作,也就不会触发SIG_PIPE问题。需要对代码按如下方式进行改造。


int xxx_poll(Socket *pSock, int iMask, int iWaitMilSec)
{
    struct pollfd   fd;

    memset(&fd,0, sizeof(fd));

    if(!pSock || !((iMask & MMM_READABLE) || (iMask & MMM_WRITABLE)))
    {
        return 0;
    }
    
    if (iMask & MMM_READABLE) {
        fd.events |= POLLIN | POLLHUP;
    }
    if (iMask & MMM_WRITABLE) {
        fd.events |= POLLOUT;
    }
    fd.fd = pSock->fd;
    
    if ( (fd.revents & POLLHUP) > 0 ){
		return 0;    /*close needed*/
    }
     /* 返回0表征 poll结果超时,并且无套接字就绪 */
    return poll(&fd, 1, iWaitMilSec);
}

经过如上修改,在这种对端关闭套接字的场景下,才不会触发 SIG_PIPE 问题。但在实际的编程场景中,可能还有非常多其他的场景需要完善返回值和异常处理。很难要求每个人都做这种完善的异常处理,故而需要通过忽略SIG_PIPE信号,让这种错误成为一种一般化的错误,只做记录,而不引起程序退出。

对网络编程中的异常做简单梳理(参考自极客时间,网络编程实战)

我们这里对网络编程中可能出现的异常情况再做一个总结。我们以我们这端是服务器端为例,这是我们目前应用中最常用的使用场景,作为设备端,基于web服务器,为APP端或者客户端提供REST服务。

假设连接已经正常建立,3次握手已经完成,双方在欢快地进行正常的数据传输。

如果正常消息传输结束,正常关闭流程时,APP端发起FIN包,对应用层而言就是调用close调用,另一端通过上述poll或者recv调用,在获知对端已经关闭连接的时候,也关闭当前的套接字,完成一套正常关闭的流程。众所周知,要完成如下的流程

A FIN —— > B
A < —— SYN B

A < —— FIN B
A SYN —— > B

但实际场景中可能会出现如下两种异常场景。

  • 场景1是 程序正常交互时,对端突然挂了,即没有任何响应,也没有关闭套接字,这种情况可能有两种。
    • A是网络挂了,比如手机端wifi断了,或者关闭了4G。或者宽带网络断了。
      • 如果是服务器端在阻塞read,如果没有设置read超时,也没有设置心跳保活的话,那么很不幸,阻塞的read将会一直阻塞下去。如果是独立线程,那么这个线程会持续地存在,直到所处的进程退出。
      • 如果是服务器端在进行write操作,因为write只是将数据写入到内核缓冲区,故而会返回正确,根据TCP协议的属性,linux会将数据包进行重传,直到重传的次数和时间超过了内核设定的阈值,就会将这个连接状态置为异常,并在后续再次read或者write这个套接字时,返回对应的错误。
    • B是系统挂了,比如交互端是客户端,客户端所在的台式机被踢掉电源出现硬关机。那么此时,设备端所在的服务器端,仍然是认为对应的套接字连接是正常的。
      • 这种情况与网络阻断的情况类似,如果对端没有重启,那么就会是与上述的情况完全一致。
      • 如果对端重启,重启之后在原先的端口地址上,是没有之前的连接的信息的。如果还在重传的过程中,对端接收到了重传的消息,这种情况下对端会返回一个RST分节。如果我们调用read调用,就会返回一个非常常见的错误叫做 Connection Reset By Peer 的系统错误。这种情况下很显然就需要执行异常处理,关闭原先的会话。而重新开启。
  • 场景2是 程序正常交互时,对端正常挂了。这种情况比如APP崩溃了,系统层面不管是ios还是Android,都会进行收尾操作将 已经打开的socket连接全部关闭,也就是发送FIN分节给对端。
    • 这时就容易出现我们上面这里描述的异常情况。
    • 虽然与正常流程表现上都是服务器端收到FIN分节,但在IO这一层是无法直接区分的。所以即使对端正常的关闭,也是可能出现如下的异常流程。
    • 对方发送时FIN包时,我们这端没有进行IO的处理时。而我们后续的处理可能就出现两种情况。
      • 如果我们调用write往对端发送消息,会直接接收到一个RST消息,内核层就会感知到该连接异常,如果再调用write发送消息,就会触发SIGPIPE 信号。
      • 如果我们调用read接收消息,那么会直接返回0,即标志对端已关闭连接,需要我们这边进行对应的处理。

网络编程的流程梳理和异常处理时,尤其要注意上述几种情况的异常处理。

参考

极客时间-网络编程实战-TCP并不总是可靠的

你可能感兴趣的:(网络基础,unix编程)