這里用Python實現了一個echo程序的服務端和客戶端,客戶端發出的東西,服務端打上一個時間戳后給客戶端發回去。主要是實踐一下Python的socket編程
Python的socket相關的比較低層的接口都在標准庫中的socket module來實現的,這個module中定義的屬性包括一些常量,如下面34行的AF_INET,SOCK_STREAM,全局函數ntohl(byte order translation),另外還有一個類socket,這個Socket Object里面包裝了像listen, accept這些函數。socket module里面的全局函數socket就返回這樣一個Socket Object, 如下server端程序34行
1,服務端程序, 程序中的注釋有比較詳細的說明。
1 #!/usr/bin/python 2 #encoding=utf-8 3 4 # 5 # echo server, 對每一個請求都單獨fork出一個進程來處理 6 # 7 8 from socket import * #low-level networking interface 9 import time # Time access and conversions 10 import os # for os.fork() 11 import sys # for sys.exit() 12 13 def func(tcpCliSock, addr): # python中沒有單獨的聲明,函數必須在使用前定義 14 try: 15 while True: 16 data = tcpCliSock.recv(BUFSIZE) # BUFSIZE表示一次返回的最大的接收數據量,這個接口和unix中的recv 17 if not data: 18 print 'connection end with : ', addr 19 break 20 print ' request from ', addr , 'data :', data 21 tcpCliSock.send('[%s] %s' % (time.ctime(), data)) 22 23 tcpCliSock.close() 24 except KeyboardInterrupt: #從輸出可以看出若父進程的ctl-c異常會先在子進程中捕獲,再在父進程中捕獲 25 print ' Ctl-c caught in chlid Process'27 tcpCliSock.close() 28 sys.exit() 29 30 HOST = 'localhost' 31 PORT = 2012 # port是integer 32 BUFSIZE = 1024 33 ADDR = (HOST, PORT) 34 35 tcpServerSock = socket(AF_INET, SOCK_STREAM) #這里soket函數,以及括號里的參數都是 socket module里的提拱的函數(全局), 而返回的是一個Socket Object, 是socket module里定義的一個class 36 tcpServerSock.bind(ADDR) 37 tcpServerSock.listen(10) #10表accept的等待隊列最大數 38 39 try: 40 while True: 41 print 'waiting for connection ...' 42 tcpCliSock, addr = tcpServerSock.accept() #accept返回的是一個pair,(conn, address), 其中conn是一個Socket Object, address是另一端發起連接的端口地址,地址也是一個pair, (ip, port) 43 print 'connected from :', addr, ' local:', tcpCliSock.getsockname(), '------remote:', tcpCliSock.getpeername() 44 45 # fork出一個子進程來處理單獨的請求 46 pid = os.fork() 47 48 if pid > 0: 49 print 'fork child process pid = ', pid, 'to process ', addr 50 # os.waitpid(pid, os.WNOHANG) #還是沒有起到回收的作用 51 tcpCliSock.close() #這個close很重要 52 53 if pid == 0 : 54 tcpServerSock.close() #在子進程中關掉 tcpServerSock 55 func(tcpCliSock, addr) #子進程中運行func 56 sys.exit() #可以簡單理解為退出子進程 57 58 tcpServerSock.close() 59 60 except KeyboardInterrupt: # 截獲Ctr-c 異常,來關掉Server 61 print ' Ctl-C stop server' 62 tcpServerSock.close() 63 sys.exit()
對於服務端的這個小程序,盡量做了一些完善,在fork出子進程后關掉tcpServerSock, 在子進程和父進程中都捕獲Ctrl-c終止程序的異常,讓server退出優雅些;
對於34行調用socket.socket()傳的兩個參數AF_INET,SOCK_STREAM表示創建基於網絡的socket(套接字socket分為Unix socket和Internet socket),SOCK_STREAM表示是tcp socket。
用單獨的進程處理每個獨立的客戶端,45行用了os.fork(),這里subprocess模塊不適用。os.fork()同樣的在父進程中返回子進程id,在子進程中返回0, 48行打印出子進程id
第50行的os.waitpid(),本來是想子進程退出的時候被父進程及時回收,不要變成defunct , 僵屍進程,但是沒有達到效果,注掉了
2. 客戶端程序
1 #!/usr/bin/python 2 #encoding=utf-8 3 4 # 5 # echo程序客戶端,服務端把每條消息打上時間戳后返回 6 # 7 8 from socket import * 9 import sys 10 11 BUFSIZE = 1024 12 serverAddr = ('localhost', 2012) 13 clientSock = socket(AF_INET, SOCK_STREAM) 14 15 try: 16 clientSock.connect(serverAddr) #若要連接的服務端沒有啟動,這里會拋出異常 17 except Exception as e: #不知道這里捕獲什么具體的異常了,就用的一個比較基層的類Exception 18 print 'exception catched : ' ,e 19 sys.exit() 20 21 while True: 22 data = raw_input('input data :') 23 if not data: # 直接回車就沒有輸入內容,退出 24 print 'client quit' 25 break 26 clientSock.send(data) 27 data = clientSock.recv(BUFSIZE) 28 print 'recieve : ', data 29 30 clientSock.close() # 要close一下
注意第17行捕獲異常的語法。
3,根據上面的程序來看看tcp socket的幾個關閉狀態
tcp有5個和關閉相關的狀態, 見節末的那張圖
上面服務端程序51行的代碼很關鍵,在父進程在把tcpCliSock關掉,實際上父進程的server的2012端口一直在listen,而fork出的子進程的那個tcpCliSock和client建立傳輸數據的連接。當我們啟動server,再啟動一個client連上一個server的時候,查看相關的tcp連接是這樣的。 第一行是父進程用來listen的, 后兩個依次是server fork出的子進程和client進程
這時候如果用ctr-c停掉server, 相關的tcp狀態變為
LISTEN的tcp socket在close()后就自動變成CLOSED的狀態了,可以看到這一對確定連接的tcp socket,服務端主動發起的關閉請求(根據上面的代碼,捕獲KeyboardException的時候有發起關閉),server端發送一個FIN然后接收到ACK變成了 FIN_WAIT2狀態,而client端接收到了FIN然后回一個ACK成了CLOSE_WAIT狀態。 注意client端沒有發起FIN,也就是client socket沒有close()
如果是client端主動關閉,則相關的tcp狀態變為
這里因為是新起的進程來實驗,所以pid跟上面的不一樣了,下面那個TIME_WAIT是client端socket的狀態。而server端的socket是CLOSED狀態了。TIME_WAIT是主動關閉一端最終的狀態。這里client主動發起關閉請求時,server端接到回ACK后也主到發起FIN關閉了socket。所以這里的處理是完整的。
關於這幾個tcp的狀態,這篇來自coolshell.cn上的文章有較為詳細的說明。下面引用一篇這篇文章中的圖
后續
在上面server端主動關閉tcp連接時,關閉並不完整,如果client端回ACK后能再主動發起關閉請求,就完整了,server端可以這樣做,在 ctl-c中止服務時向client發送一個空串,client接到后也主動關閉socket,然后退出程序就好了。
server端如下修改, 在25行后加上一行代碼。
25 print ' Ctl-c caught in chlid Process' 26 tcpCliSock.send('') 27 tcpCliSock.close()
但是這里有一個問題,client端阻塞在raw_input那個地方,實際上必須要輸入一點東西才能接收到server端的內容,如何比如用多線程改到能即時做到接收這個空串然后關掉socket,然后退出程序?