天天看点

在处于读写状态时,直接断开网络,导致读写阻塞

一、出现环境:

       用手机连接车机wifi,车机端(arm-linux环境)建立socket(阻塞模式)的TCP服务端,在手机端连上服务端并且正在对socket处于读/写操作时,突然从手机侧主动断开热点(类似有线网络拔掉网线)。此时就会出现read/write函数阻塞的情况,此时即使调用close关闭socket也无法使阻塞退出。

二、问题所在:

        经过层层解剖,定位问题出现的过程是这样的:要想充分理解问题出现的过程,要知道两点

        a.了解阻塞模式的read/write什么情况下会阻塞。每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区,write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。TCP协议独立于write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。当读缓冲区没有数据且socket为阻塞模式时,read()/recv()会挂起阻塞等待数据,直到有数据可读时唤醒read()/recv()。当写缓冲区的可用空间长度小于要发送的数据时write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据(直到所有数据被写入缓冲区 write()/send() 才能返回,如果要写入的数据大于缓冲区的最大长度,那么 将分批写入);当TCP协议正在向网络发送数据时,输出缓冲区不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。close对应socket无法唤醒读写函数。

        b.socket默认情况下是没有监测网络是否断开的功能的(这也是为什么好多应用会增加自己的心跳线程),故处于睡眠状态的读写函数,不会被断开网络所唤醒。

        掌握如上两点,基本可以理出问题所在:当socket处于读状态时断开网络,由于新数据无法发送到服务端,读一直处于阻塞状态。当socket处于写状态时,由于缓冲区可用空间不足新数据大小,并且缓冲区数据无法发送,所以缓冲区可用空间无法增加,导致写一直阻塞。而且处于睡眠状态的读写操作只有通过指定条件才能唤醒,close关闭socket并不能发送唤醒条件。读写会持续阻塞。

三、解决方案:

        在调用close关闭socket之前增加调用shutdown(fd, SHUT_RDWR)函数解决问题。

        头文件:#include <sys/socket.h>

        定义:int shutdown(int s, int how) 

        说明:对套接字的关闭进行更细致的控制,它允许对套接字进行单向关闭或全部禁止。该接口只关闭读写操作的权限,并不能关闭socket,所以调用该接口后,仍然需要调用close来关闭socket。该接口仅影响本端socket 

        参数:s 为待关闭的套接字描述符。

                  how 指定了关闭方式,具体取值如下:

                  SHUT_RD : 将连接上的读通道关闭,此后进程将不能再接收到任何数据,接收缓冲区中还未被读取的数据也将被丢弃,但仍然可以在该套接字上发送数据。

                  SHUT_WR : 将连接上的写通道关闭,此后进程将不能再发送任何数据,发送缓冲区中还未被发送的数据也将被丢弃,但仍然可以在该套接字上接收数据。

                  SHUT_RDWR : 读、写通道都将被关闭。

        返回值:返回 0,出错则返回 -1,错误代码存入 errno 中。

四、解决过程:

        方案一:在最开始时,初步认为该问题出现的原因是socket多线程同时操作导致的(既在读/写的时候close),解决思路定为避开同时操作。于是设想在调用close之前,先调用setsockopt函数设置socket的读写超时,使read/write函数能先退出,再调用close关闭socket,以为能完美解决阻塞问题。但是事与愿违,经测试发现问题依然存在。测试发现,如果开始socket设置为阻塞状态,在某一时刻调用setsockopt函数设置读写超时(此时未处于读写函数的阻塞时间段),则之后再调用读写函数时,设置的超时有效。如果开始socket设置为阻塞状态,在某一时刻调用setsockopt函数设置读写超时(此时已经处于读写函数的阻塞时间段内,既read/recv和write/send已经处于阻塞睡眠状态),设置的超时无效。最终方案一失败

        方案二:经过反复搜寻线索,发现方案一假设的原因是不成立的。此时才意识到根本原因或许是因为网络断开,于是猜测问题出现的原因是服务端的socket不知道此时网络已经异常断开,但是实际服务端的read既读不到数据,write也写不成功。于是修改解决思路,把重点放在使服务端的socket能感知到网络已经断开。看到是socket有个属性SO_KEEPALIVE选项可以监测网络,可以调用setsocketopt( )函数设置该属性。但是该属性有几个弊端:                                                      a.需要消耗额外的宽带和流量(当前网络为无线2.4G,这无疑是给本就不富裕的车机雪上加霜)。

                      b.在短暂的故障期间,keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接(无线网络本就不稳定,这个问题出现的概率应该会较高,可能会大量增加后续的维护成本)。

                      c.SO_KEEPALIVE属性是全局配置,会影响机器上的所有套接字(这简直丧心病狂好吗,我设置的是某个socket,但是它却影响了所有的socket,这完全加重了a和b的影响)。

在没有验证该方案是否可行的情况下,再次抛弃了方案。方案二夭折

        方案三:在延续方案二假设的情况下,又在知识的海洋中继续翻滚。经过了半天的搜索吸收,基本断定了方案二假设的成立,出问题过程如“问题所在”项描述。在了解了出问题的详细过程之后, 又重新整理一下思路,发现shutdown这个函数貌似可以解决问题,通过关闭服务端socket的读写权限,强制让处于阻塞状态的read/write函数返回,经过验证,发现有效。幸福来的就是这么突然

        方案四:该方案是在搜索时发现的解决方案,既在建立socket时将其设置为非阻塞,这样就会影响到很多处理逻辑,并没有实施修改。

继续阅读