“一演示就出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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章