python的HttpServer出現socket.accept()阻塞卡死

最近想用python做一個我微信公衆號的後臺,結果發現,服務器剛啓動的一個多小時微信發的消息是有回覆,但過幾個小時之後,所有給服務器發現的請求都沒有回覆了,找了兩天問題,昨晚上還弄半夜3點。總算把問題給解決了。

服務器用的是下邊這個類:

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):

和一個處理http各種do_請求的Handler:

class myHandler(BaseHTTPRequestHandler):

我這裏只處理三個do_請求,do_GET,do_POST,do_HEAD.
作爲http服務器,這部分所有的處理邏輯都是一樣的。不多說了。下面我說一下問題出現的原因和處理辦法。
出現問題的情況
先看下邊服務器log輸出,可以看到有不明身份的域名對服務器的433端口使用了get請求。
python的HttpServer出現socket.accept()阻塞卡死 - ♂蘋果 - 眼睛想旅行
 這個域名會每過幾個小時就對我的服務器進行一次get請求。當出現這個get請求之後,服務器就好像掛掉了一樣,所有再向服務器發送的任何請求都不會有迴應。感覺很奇怪,但因爲對方使用的是三級域名請求,我又沒有辦法使用IP地址過濾的方法拒絕他,只有當服務器被請求掛掉之後,我纔能有辦法獲取到對方的IP地址。
我就先把這個叫作請求攻擊吧,因爲每次請求之間都間隔幾個小時,就折騰了兩天時間,開始的時候以爲是多線程對象調用引起的線程死鎖,在這個處理思路上就花了一天時間,然後使用單線程啓動服務器後看到上邊的log,就知道問題所在了。
對問題情況模擬重現
作爲程序員在解決bug的問題上,最重要的是要找到解決這個bug的鑰匙,也就是讓bug可以人爲控制的重現。但凡不能重現的bug,都是讓人揪心的bug.下邊是bug重現方法。其實也很簡單。
1.再次啓動服務器,
2.用我電腦上的socket調試工具連接服務器的443端口
3.當socket成功連接上服務器的443端口,但不發送任何數據
4.給http服務器發送任意請求
5.服務器不會對任何請求進行迴應
6.當從socket工具斷開443端口連接時,服務器這時會回覆第4步發送的http請求
找到問題了,也可以重現了,那接下來就是對問題的處理。
查看python庫的httpServer的源碼,找到出現問題的位置
 查看到python庫代碼之後,發現HttpServer在有客戶端連接上後,HTTPServer的_handle_request_noblock()函數中會調用socket的一個socket.accept()方法,這個方法返回客戶端IP地址端口以及回覆消息的request連接。
當我只作443端口的連接,而不發http請求時,HTTPServer就是阻塞在這個socket.accept()方法調用上。而accept並沒有超時參數設置。
mac os系統下,python的HTTPServer相關源碼在下邊路的兩個文件中:

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/BaseHTTPServer.py

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/SocketServer.py

這兩個文件一個是Socket服務器,一個是HTTP服務器,自然了,HTTP服務器類是繼承自Socket類的了。
先是SocketServer得到客戶端連接,會觸發自已的_handle_request_noblock()函數,在這個函數中,連接和請求會轉發給HTTPServer來處理。看下圖代碼部分:
python的HttpServer出現socket.accept()阻塞卡死 - ♂蘋果 - 眼睛想旅行
 handle_request是服務器發出的一次客戶端請求檢測,超時沒有發現客戶端就調用超時handle_timeout()函數並返回。
當發現有客戶端連接時,會調用_handle_request_nolock()方法,在這個方法中有一個獲取客戶端請求和客戶端地址的get_request()方法。這個方法其實在HTTPServer中有實現,看下圖代碼:
python的HttpServer出現socket.accept()阻塞卡死 - ♂蘋果 - 眼睛想旅行
 在這裏,調用了socket.accept()函數用以返回客戶端請求和客戶端連接地址信息。當只作socket連接而不發送http請求時,這個accept就被阻塞了,python的HTTPServer是多線程連接,單線程處理消息,所以這個消息不接收到,就會阻塞後邊的所有請求。
想辦法解決問題 
找到問題所在了,那麼接下來就是真正的解決問題,我試過了很多辦法,包括在accept()之前查看客戶端信息,設置socket設置方法setsocketopt()或者設置超時,但都解決不了。最後想到看python有沒有調用函數超時的處理邏輯,一搜,還真有。下邊是找到的處理辦法:
我作的處理邏輯:
python的HttpServer出現socket.accept()阻塞卡死 - ♂蘋果 - 眼睛想旅行
 到些問題理論上得到解決,我設置的accept()調用超時是2秒。接下來就是測試。
問題還是存在
以爲找到了解決辦法,但我的http服務器是在子線程裏啓動的,上邊python調用函數超時裝飾器辦法還是解決不了問題。
python的HttpServer出現socket.accept()阻塞卡死 - ♂蘋果 - 眼睛想旅行
 好吧,既然這樣,有兩種辦法來解決這個問題,
第一種,找一個可以在子線程中使用的函數調用超時解決方案。
第二種,把http服務器放到主線程裏,把任務處理邏輯放到子線程裏。
以上兩種方法理論上都是可以了。第二中方法要改很多代碼,但實現起來技術難度要低一些。但我想試試第二種方法,於是到處找解決方案。到是讓我找到一個。
用文中所說的方法,好像可以實現子線程調用函數超時,趕緊試一下。
經過測試,文中的方法是把accept()方法放在了另一個新建的子線程中去調用。這樣httpserver的accept()將不會被阻塞,所有新的http請求都可以正常相應了,雖基socket連接攻擊端看到socket還是保持連接着的,但實際上socket連接的accept進程已經被終止了,如果這時候socket連接攻擊端發送消息時,就會顯示socket已斷開。
總結
在處理這個問題時,找到了一些相關的socket知識。在這裏收藏一下,說不定以後會用到。
python socket.setsocketopt()方法中參數的設置方法比較奇特,socket底層其實是C語言寫的,在設置指針數據類型的參數時用到了struct庫,
可以看一下例子:
python的HttpServer出現socket.accept()阻塞卡死 - ♂蘋果 - 眼睛想旅行
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章