在涉及到网络交互的程序中,我们经常会在程序的伊始就执行一个信号注册
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
排查过程中确认例如上面的日志出现的情况还是非常多的,在很多接口中都有频繁的出现。
从日志中可以了解到是 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
但实际场景中可能会出现如下两种异常场景。
网络编程的流程梳理和异常处理时,尤其要注意上述几种情况的异常处理。
极客时间-网络编程实战-TCP并不总是可靠的