1. 程式人生 > >“一演示就出BUG”——[Errno 10053]引發的深思

“一演示就出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個小時,其中兩個小時花費在了亂改程式碼碰運氣階段,這可能是自己至今都存在的一個缺點。不願意去花時間探究原理,而且傾向於投機取消,想著憑自己的運氣。可是,程式碼是最講道理的東西,問題也是如此,不能只看表面,而應該細緻地去研究他的成因、找到他背後的道理。這樣才能夠更好的根除問題。最後,再次感謝我的師兄給予我的大力幫助!

附上師兄的部落格連結~

參考文獻:

[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