IPython下的網絡協議學習-------以echo回顯程序爲例

在進入正文之前強烈推薦IPython 來學習網絡編程,和C語言繁瑣的語法和編譯相比,IPython交互的輸入輸出效率奇高,打開兩個IPython頁面就可以方便的編寫服務端和客戶端程序了。推薦anaconda環境,集成了很多必備的庫,以及IPython notebook 。

廢話說完了,下面進入正題。本文主要以簡單的回顯程序爲例,實踐測試了不同異常情況下服務端和客戶端的反應,由此逐步提高程序的健壯性。異常情況主要參考網絡編程著作《UNIX網絡編程》第三版。

我的服務器代碼和客戶端代碼是這樣的:

#Echo Server
import socket
import sys
import signal
import os
def server_echo(connefd):
    while 1:
        try :
            print ("before recv")
            mesg = connefd.recv(8)
            print (mesg)
            if not mesg:
                return
            connefd.send(mesg)
        except os.error:
            print ("socket error")      
            return 


def signal_handlers(signo,frame): #信號處理函數
    print ("child %d terminnated",os.wait())

serverPort = 50000
listenfd = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
listenfd.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
listenfd.bind(("",serverPort))
listenfd.listen(5)
signal.signal(signal.SIGCHLD,signal_handlers)  
while 1:
    connefd,address = listenfd.accept()
    childpid = os.fork()
    if(childpid == 0):
        listenfd.close()
        server_echo(connefd)
        exit(0)
    connefd.close()


#客戶端
import socket
connefd = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connefd.connect(('127.0.0.1',5000))
while 1:
    sentence = input("Enter your sentence:")
    if sentence == "":
            connefd.close()
            break
    connefd.send(sentence.encode())
    mesg = connefd.recv(8)
    print mesg

代碼很簡單,比較值得一提的是服務器程序中利用系統調用fork函數進行實現併發服務器。除此之外,必須提供一個SIGCHLD信號的處理程序。當子進程結束時,內核會自動發送SIGCHLD信號給父進程,默認處理方式是忽略。那麼我們爲什麼要處理SIGCHLD信號呢?原因是如果不主動處理SIGCHLD會產生大量殭屍進程

一個進程在調用exit命令結束自己的生命的時候,其實它並沒有真正的被銷燬,而是留下一個稱爲殭屍進程(Zombie)的數據結構(系統調用exit,它的作用是使進程退出,但也僅僅限於將一個正常的進程變成一個殭屍進程,並不能將其完全銷燬)

因此,僅僅調用exit()不能完全的釋放進程資源,我們必須**捕捉(capture)**SIGCHLD信號,在父進程的信號處理程序中釋放資源。爲此,Unix系統提供了wait,waitpid等函數獲取子進程的終止狀態,並釋放其資源。那麼wait和waitpid這些函數有什麼作用呢?顧名思義,wait函數是一個等待的函數,父進程調用wait函數來等待一個子進程終止,並清理第一個終止子進程的“屍體”。那麼waitpid函數的作用又是什麼呢?waitpid函數相當於一個加強版的wait函數,他的函數參數中可以設定需要等待的子進程ID,如果沒有子進程已終止則可選擇不阻塞等選項。參閱UNPV1第三版可以瞭解waitpid更詳細的信息。

接下來就UNP 5.11節 至 5.16節的小節所述的異常情況進行模擬,並總結一些異常處理方法和編程技巧。

(1) 服務器程序在accept返回前連接終止

accept函數是服務器程序調用的一個函數,作用是服務器接受一個ESTABLISHED狀態的TCP連接,並生成一個新的套接字,叫做已連接套接字(connected socket) 。函數接受的套接字是由listening socket 維護的 已連接隊列中的隊首的連接,如果隊列爲空,則accept函數阻塞。
注意:TCP連接在客戶端調用connect函數返回時就已經完成連接,這意味着服務端在調用 accept函數之前TCP連接已經建立。

爲了模擬accept函數在返回前連接終止,我們可以讓客戶端在connect函數返回後,利用SO_LINGER套接字選項來改變close函數的行爲(讓其向服務端發送RST報文段)。在RST報文段發送之前,我們的服務端accept函數應該返回。因此可以在服務端調用accept函數前利用input函數或是sleep函數阻塞程序,當RST報文端發送後再調用accept函數。服務端和客戶端代碼改變如下:

#服務端
while 1:
    input("Enter Any key to continue")  #添加的阻塞語句
    connefd,address = listenfd.accept()
#客戶端:
connefd.connect(('127.0.0.1',5000))
connefd.setsockopt(socket.SOL_SOCKET,
                    socket.SO_LINGER,struct.pack("ii",True,0))#添加的套接字選項,改變了套接字close函數的執行內容,關於SO_LINGER套接字選項可以查閱UNP7.5節
connefd.close()

讓我們運行代碼測試一下結果,同時可以利用tcpdump或者是Wireshark抓包工具監測RST報文段是否發送成功。
客戶端代碼執行時的Wireshark抓包截圖
客戶端代碼執行時的Wireshark抓包截圖

最後讓我們來看一下服務端收到RST報文段後服務端的響應。

ConnectionResetError                      Traceback (most recent call last)
<ipython-input-1-4ac06058bc37> in <module>()
     32     if(childpid == 0):
     33         listenfd.close()
---> 34         server_echo(connefd)
     35         os._exit(0)
     36     connefd.close()

<ipython-input-1-4ac06058bc37> in server_echo(connefd)
      8       #  try :
      9         print ("before recv")
---> 10         mesg = connefd.recv(8)
     11         print (mesg)
     12         if not mesg:

ConnectionResetError: [Errno 104] Connection reset by peer

​

可以看到服務端返回一個ConnectionResetError錯誤,而且繼續調用accept函數,之後其他客戶端也能繼續連接服務端正常運行程序。我們也能捕捉ConnectionResetError,進而實現自定義的錯誤處理方法。

後續將繼續對不同異常情況進行試驗,記錄學習TCP/IP協議的過程與經驗。

W.Richard Stevens.UNIX網絡編程 卷1:套接字聯網API[M].北京:人民郵電出版社,2010

發佈了31 篇原創文章 · 獲贊 26 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章