“一演示就出BUG”——[Errno 10053]引发的深思

本博客由闲散白帽子胖胖鹏鹏胖胖鹏潜力所写,仅仅作为个人技术交流分享,不得用做商业用途。转载请注明出处,禁止未经许可将本博客内所有内容转载、商用。

       “武侠小说里看到过一段话,大意是练习歪门邪道的功夫,很快就能小有成就,但永远成不了高手。而名门正牌的武功虽然入门艰辛,进步缓慢,却是成为一代宗师的必由之路。”

——林沛满《Wireshark网络分析就是这么简单》

        最近人们总是喜欢谈“佛系”,但是研究人员如果“佛系”起来,可能要吃到不少的苦头。作为一直闲散白帽子,写代码调bug是少不得的事情,可是我个人比较随性,debug时候经常重启一下或是乱来一通碰碰运气,我个人称为“玄学Debug”。凭借着个人的运气,也解决了不少的麻烦。可是,一个人哪能经常获得这种运气,如果真的有,我获取应该去澳门发(ge)家(zhong)致(hao)富(du)。前几天的事情就给我自己好好地上了一课。

一、“诶?他为啥总是报错10053?”

        前一阵写了一段代码,代码的功能是这样的。有一个合法的用户A,使用SSL协议接入服务器,服务器A用户和服务器建立起链接之后,需要A提供自己的用户名和密码进行身份校验;此时,一个恶意用户E获得了A的用户名和密码,E也与服务器建立SSL链接,并向服务器提供了A的用户名和密码进行了身份验证。那么在服务器看来,他收到了来自A的先后两个链接,就会关闭第一个链接(与A建立的那个),保留第二个(与E建立的那个)。但是用户A却并不知情,于是乎又重新登入服务器。按照之前所说,此时E又会被踢下线,如此循环往复,互相竞争和服务器之间的通信权利。为了方便理解,画了一个草图(省略了SSL过程)。


        A和E互相争夺和服务器的链接权,被挤掉线的一方不断重连。其中A和服务器的代码都由别人实现,我负责写E的部分,用python实现了一下,虽然我个人不推崇博客里面填充代码,为了后面方便分析,还是要贴上。

def send_message():
    #发送给服务器消息的序号
    id =1
    #建立socket并和服务器建立SSL链接
    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    ssl_sock = ssl.wrap_socket(s)
    ssl_sock.connect((server_host,port))
    
    #此处汇报用户名和密码给服务器,即用来验证身份
    data=helloServer 
    id+=1
    ssl_sock.write(data)
    #服务器返回认证结果
    data = ssl_sock.recv(1024)
    
    data = ssl_sock.recv(1024)"""#!!!!!----------->第一个坑"""
    while True:
        #使用阻塞方法,死等服务器发送过来的数据包
        data = ssl_sock.recv(1024)
	#判断是不是业务逻辑
        if data.find("Duty")!=-1:
            if data.find("get_info")!=-1:
                data2=sys_info
				id+=1
                ssl_sock.write(data2)
            elif data.find("get_name")!=-1:
                data2=next_action
				id+=1
                ssl_sock.write(data2)
        else:
	    #如果不是业务逻辑,就发一个心跳包
            data2=heartbeat_msg #发送心跳包,维持链接用的
            id+=1
            ssl_sock.write(data2)
            data = ssl_sock.recv(1024)
	    sleep(1)"""#!!!!!----------->第二个坑"""

if  __name__ == '__main__':
	#死循环,不断地竞争和服务器的链接,发生异常之后,重新链接
	while True:
	    try:
	        send_message()
            except Exception,err:
		print err

        这是一个简单粗暴的实现方式,在和服务器建立起链接之后,首先进行身份验证,之后不断地死循环等待服务器发来的各种请求,如果此时收到的不是业务逻辑代码,就发一个心跳包,为了等待服务器返回消息,我使用sleep休眠了一下。因为考虑到可能出现各种错误,于是在main函数里面使用try—except结构捕获异常。(大家注意第16行,不是我手抖加上去的,在调试的时候发现是需要这样做才能接收到正确数据的,不要问我为什么,归功于我当时“玄学Debug”)

        那么就开始测试吧,服务器Server上线、用户A成功建立连接,我们的用户E也开始运行。但是,很快屏幕上不断弹出来10053。查了查报错原因[1],原文是这样说的:

Software caused connection abort.
An established connection was aborted by the software in your host computer, possibly due to a data transmission time-out or protocol error.

翻译过来就是“连接中断,已建立的链接被计算机软件终止,可能是由于传输超时或者协议错误”。那就是说,sleep太久了?注释掉!while循环前面的recv也一起注释掉!然而结果却依旧是10053。可是为什么呢?

二、“师兄快来!”

        想来想去没有想通,喊来师兄帮忙Debug,这里插播个广告,我师兄的博客ASCII0x03,以及腾讯云+社区。为了探寻错误的原因,我们打印出了和服务器通信的每一条消息,打印日志结果如下。

E->Server: hello,passwd
Server->E: Log Sucess
Server->E: ''#空字符串
E->Server: heartbeat
Server->E: ''#空字符串
E->Server: heartbeat
Server->E: ''#空字符串
E->Server: heartbeat
[Errorno 10053]

        这里服务器返回了一个空字符串,用len()函数看一下这个字符串的长度,发现是0。查了下Python doc里面关于recv函数的定义,直接附上原文:

socket.recv(bufsize[, flags])¶
Receive data from the socket. The return value is a string representing the data received. 
The maximum amount of data to be received at once is specified by bufsize. 
See the Unix manual page recv(2) for the meaning of the optional argument flags; it defaults to zero.

        按照官方的定义,recv函数会返回接受到的字符串长度,而且默认recv函数是要阻塞执行的。嗯?接收到了0字节并且返回了字符串?和官方说明不一样啊!难道是官方出错,本着求真精神仔细阅读,发现官方说明文档里还有一句话“See the Unix manual page”,那么继续跟进吧。Linux Man-Page[3]中对于recv的描述和python文档中的没有什么差别,但是其中一段话引起了我们的注意。

If no messages are available at the socket, the receive calls wait for a message to arrive, 
unless the socket is nonblocking (see fcntl(2)), in which case the value -1 is returned and the external variable errno is set to EAGAIN or EWOULDBLOCK. 
The receive calls normally return any data available, up to the requested amount,
rather than waiting for receipt of the full amount requested.

        也就是说,无论是python还是还是man,都强调recv是一个阻塞函数,fcntl是一个非阻塞函数,并且会返回错误号码。那么假设Python使用的recv函数确实是阻塞状态的,但是我们却收到的len=0的数据包,那是不是说明我们的链接发生了错误呢?

三、拨云见日

        为了了解真相,师兄使用Wireshark抓取了通信的数据包,这一抓果然发现了问题。下图是我们抓到的数据包。



        其中ip地址209为我们的本机地址,119为我们的服务器地址。可以看到前面建立SSL连接成功,而且也发送了验证数据(1数据包468-1498)。但是第二个方框(2594,2595),服务器向我们发送了一个FIN,根据四次挥手[4],也就是此时服务器想要和我们断开连接,我们只做出了ACK回应(2596),服务器再等我们发送FIN。后面的数据(2570-2727)都是我们发送的心跳包,可见服务器也进行了接收确认,但是并没有回应任何有效数据。最终,服务器等待超时,发出RST强行终止了链接。

        看到这里,我们应该已经弄懂了之前打印的消息为什么为空,而且也弄懂了为什么应该阻塞的recv函数没有阻塞。代码输出和链接状态示意图如下。图中E代表本机代码,Server代表服务器,最左边的字符串为本机打印出来的字符串。


        但是,既然已经开始研究问题了,那就研究个透彻。为什么服务器像我发送FIN的时候,我调用recv() 返回‘’?又为什么我发送的数据服务器能够接受呢?

四、溯其本源

        通过查阅资料[5],我们发现socket关闭时可以采用close()函数,也可以使用shutdown函数。在使用shutdown()关闭链接时,程序会等待阻塞函数执行完毕后直接接释放socket引用;而使用close()关闭链接时,会发出FIN包等完成四次握手,之后等待阻塞进程结束,释放socket引用。也就是说,相比于shutdown,close更加礼貌一些。而我们之前受到了服务器发过来的FIN,说明服务器已经准备close了。而我们却以为服务器还在正常通信,继续发送数据包。那么,如何解释第三节的两个问题呢?

        针对第一个问题,我们发现[6]中进行了较为详细的叙述。“如果一方调用close()或是直接退出,那么使用read(等同于使用recv)将会返回0。但是不清楚执行write时会发生什么,我觉得可能发生EPIPE异常......在一方接收到FIN之后,继续read()将会返回0。你必须检查read()返回值是不是0”。到此为止,我们已经完全了解了recv()返回空字符串的原因。

        针对第二个问题,[7]对此做出了解释。“当A和B使用TCP通信时,如果B关闭了socket,并且B的接收队列中有剩余数据的话,B不会遵循标准的socket关闭协议,而是向A发送TCP RST消息,此时A使用recv方法时,会产生异常......为了解决这个问题,我们需要在调用close()之前,调用shutdown()(参考man page 第6章关于shutdown的定义),这样能够防止RST出现。但是不要移除close()。......如果你这样做的话,recv()将会返回0,而不是-1(Error)。”。根据shutdown()的定义,他提供了三种模式,SHUT_RD、SHUT_WR、SHUT_RDWR,通过实际分析,猜测服务器使用的是SHUT_WR,所以我们才能在服务器发出FIN之后,继续向服务器发送心跳包,而经过一段时间后,服务器等待超时,发现了自己的接收缓存区中有我们发过去的心跳包,所以向我们发回了RST数据,这也就完美解释了我们为什么会见到10053这个错误号。

        最后总结一下此次debug过程。从发现bug到解决bug前前后后花费了4个小时,其中两个小时花费在了乱改代码碰运气阶段,这可能是自己至今都存在的一个缺点。不愿意去花时间探究原理,而且倾向于投机取消,想着凭自己的运气。可是,代码是最讲道理的东西,问题也是如此,不能只看表面,而应该细致地去研究他的成因、找到他背后的道理。这样才能够更好的根除问题。最后,再次感谢我的师兄给予我的大力帮助!

附上师兄的博客链接~

https://www.cnblogs.com/ascii0x03/

参考文献:

[1] Windows Sockets Error Codes. https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668(v=vs.85).aspx

[2] Python Socket Document. https://docs.python.org/2/library/socket.html

[3] Linux Manual Page. http://man7.org/linux/man-pages/man2/recvmsg.2.html

[4] 四次挥手 .https://baike.baidu.com/item/%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B/7794287?fr=aladdin

[5] socket关闭:shutdown和close的区别.  https://blog.baishancloud.com/tech/programming/network/2017/11/22/close-shutdown.html

[6] Socket FaQ. https://www.softlab.ntua.gr/facilities/documentation/unix/unix-socket-faq/unix-socket-faq-2.html

[7] TCP RST:Calling close() on a socket with data in the receive queue. http://cs.ecs.baylor.edu/~donahoo/practical/CSockets/TCPRST.pdf

猜你喜欢

转载自blog.csdn.net/zhuzhuzhu22/article/details/80092604