在進入正文之前強烈推薦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抓包截圖
最後讓我們來看一下服務端收到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