標籤:python socketserver
IO多工
IO多工是指:通過一種機制,可以監視多個描述符,一旦某個系統描述符就緒(一般是讀就緒或者寫就緒)能夠通知程式進行相應的讀寫操作
執行個體化例子就是在SocketServer模組中,用戶端和服務端建立好串連,此時服務端通過監聽conn這條鏈路,一旦用戶端發送了資料,conn鏈路狀態就發生變化,服務端就知道有資料要接收...
650) this.width=650;" src="http://s1.51cto.com/wyfs02/M02/8B/36/wKiom1hGxWDSjOg7AAAbVVcxUhU589.png" title="IO多工.png" alt="wKiom1hGxWDSjOg7AAAbVVcxUhU589.png" />
Linux系統中同時存在select、pull、epoll三種IO多工機制
windows中只有select機制
1)select
select本質上是通過設定或者檢查存放fd標誌位的資料結構來進行下一步處理。這樣所帶來的缺點是:
1 單個進程可監視的fd數量被限制
2 需要維護一個用來存放大量fd的資料結構,這樣會使得使用者空間和核心空間在傳遞該結構時複製開銷大
3 對socket進行掃描時是線性掃描
2)pull
poll本質上和select沒有區別,它將使用者傳入的數組拷貝到核心空間,然後查詢每個fd對應的裝置狀態,如果裝置就緒則在裝置等待隊列中加入一項並繼續遍曆,如果遍曆完所有fd後沒有發現就緒裝置,則掛起當前進程,直到裝置就緒或者主動逾時,被喚醒後它又要再次遍曆fd。這個過程經曆了多次無謂的遍曆。
它沒有最大串連數的限制,原因是它是基於鏈表來儲存的,但是同樣有一個缺點:大量的fd的數組被整體複製於使用者態和核心地址空間之間,而不管這樣的複製是不是有意義。
poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
3)epoll
epoll支援水平觸發和邊緣觸發,最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變為就需態,並且只會通知一次。
在前面說到的複製問題上,epoll使用mmap減少複製開銷。
還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,核心就會採用類似callback的回調機制來啟用該fd,epoll_wait便可以收到通知
直接來看看sockserver利用select多工機制實現的偽並發執行個體
用戶端
import socket # 建立一個socket執行個體sk = socket.socket()sk.connect((‘127.0.0.1‘, 9999)) # 串連成功後列印服務端發送的訊息recv_bytes = sk.recv(1024)recv_str = str(recv_bytes, encoding=‘utf-8‘)print(recv_str) # 開始迴圈互動發送資料while True: inp = input(">>>:") sk.sendall(bytes(inp, encoding=‘utf-8‘)) ret = sk.recv(1024) print(ret)sk.close()
服務端
import socketimport select # 建立一個socket對象並綁定IP連接埠sk = socket.socket()sk.bind((‘127.0.0.1‘, 9999,))sk.listen(5) while True: # 開始監聽sk(服務端)對象 如果sk發生變化 表示有用戶端來串連 此時rlist裡面的值為[sk,] rlist, wlist, elist, = select.select([sk, ], [], [], 1) print(rlist) # 遍曆rlist 如果有用戶端來串連 就會被加入到rlist列表 for r in rlist: # 新用戶端來串連 conn, address = r.accept() conn.sendall(bytes(‘hello‘, encoding=‘utf-8‘))
開啟select監聽後,我們啟動服務端看看效果
[] [] # 空列表[][<socket.socket fd=244, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘127.0.0.1‘, 9999)>] # 發生改變的sk狀態[][] # 空列表[]# 啟動服務後,一旦有用戶端串連 sk狀態就會改變 sk就會被添加進rlist列表中被列印出來
用戶端效果
hello>>>:# 服務端接受了串連請求 並發送了一個hello訊息
上面我們只是應用了select中的一個參數rlist,詳細的參數資訊應該是
控制代碼列表11, 控制代碼列表22, 控制代碼列表33 = select.select(控制代碼序列1, 控制代碼序列2, 控制代碼序列3, 逾時時間) 參數: 可接受四個參數(前三個必須)傳回值:三個列表 select方法用來監視檔案控制代碼,如果控制代碼發生變化,則擷取該控制代碼。1、當 參數1 序列中的控制代碼發生可讀時(accetp和read),則擷取發生變化的控制代碼並添加到 傳回值1 序列中2、當 參數2 序列中含有控制代碼時,則將該序列中所有的控制代碼添加到 傳回值2 序列中3、當 參數3 序列中的控制代碼發生錯誤時,則將該發生錯誤的控制代碼添加到 傳回值3 序列中4、當 逾時時間 未設定,則select會一直阻塞,直到監聽的控制代碼發生變化 當 逾時時間 = 1時,那麼如果監聽的控制代碼均無任何變化,則select會阻塞 1 秒,之後返回三個空列表 如果監聽的控制代碼有變化,則直接執行
所以我們可以繼續完善上面的代碼 類比實現一個完整的socketserver並發程式
import socketimport select # 建立一個socket對象並綁定IP連接埠sk = socket.socket()sk.bind((‘127.0.0.1‘, 9999,))sk.listen(5) # 建立一個新用戶端列表 有訊息狀態改變的用戶端列表和一個儲存具體訊息的字典inputs = [sk, ]outputs = []message = {} while True: # 開始監聽sk(服務端)對象 如果sk發生變化 表示有用戶端來串連 此時rlist裡面的值為[sk,] # 監聽conn對象 如果conn發生變化 表示用戶端有訊息過來了 此時rlist的值為[用戶端, ] rlist, wlist, elist, = select.select([sk, ], [], [], 1) print(len(inputs), len(rlist), len(wlist)) # 遍曆rlist 查看發生狀態改變的連結 for r in rlist: if r == sk: # 新用戶端來串連 conn, address = r.accept() conn.sendall(bytes(‘hello‘, encoding=‘utf-8‘)) # conn是什麼? 其實是socket對象 串連成功後添加進用戶端列表rlist inputs.append(conn) # 以該用戶端為鍵添加字典元素 message[conn] = [] else: # 有人給我發了訊息 try: ret = r.recv(1024) # r.send(ret) if not ret: raise Exception(‘中斷連線‘) else: # 如果是已經建立串連的用戶端發來訊息 添加進訊息用戶端列表wlist outputs.append(r) # 具體訊息寫入字典 message[r].append(ret) except Exception as e: # 如果用戶端異常斷開 清理列表 inputs.remove(r) del message[r] # 讀寫分離 專門負責發訊息 for w in wlist: # 讀取字典的最後訊息 msg = message[w].pop() resp = msg + bytes(‘response‘, encoding=‘utf-8‘) w.sendall(resp) # 訊息讀取完成將該用戶端從wlist刪除 outputs.remove(w)
SocketServer模組
SocketServer是內部使用IO多工以及多線程和多進程,從而實現並發處理多個用戶端請求的Socket服務端
每個用戶端請求串連到伺服器時,SockServer服務端會在伺服器上建立一個線程或者進程專門來負責處理當前用戶端的所有請求
650) this.width=650;" src="http://s2.51cto.com/wyfs02/M02/8B/32/wKioL1hGxW2hFlHiAABOzz2ukG4953.png" title="socketserver.png" alt="wKioL1hGxW2hFlHiAABOzz2ukG4953.png" />
ThreaddingTCPServer
ThreaddingTCPServer實現的Socket伺服器內部會為每個client建立一個“線程”,該線程用來和用戶端進行互動
使用ThreaddingTCPServer:
1)建立一個自訂類 去繼承socketserver.BaseRequestHandler的類
2)自訂類中必須有一個名稱為handle的方法
3)在handle中發送和收取用戶端訊息使用普通欄位self.request
服務端
import socketserverimport subprocess # 自訂類 繼承socketserver.BaseRequestHandlerclass Myserver(socketserver.BaseRequestHandler): # handle方法 def handle(self): self.request.sendall(bytes(‘歡迎致電10086 請輸入1-9 0轉人工服務...‘, encoding=‘utf-8‘)) while True: data = self.request.recv(1024) if len(data) == 0: break self.request.send(cmd_res) if __name__ == ‘__main__‘: server = socketserver.ThreadingTCPServer((‘127.0.0.1‘, 8009), Myserver) server.serve_forever()
用戶端
import socket ip_port = (‘127.0.0.1‘,8009)sk = socket.socket()sk.connect(ip_port)sk.settimeout(5) while True: data = sk.recv(1024) print ‘receive:‘,data inp = raw_input(‘please input:‘) sk.sendall(inp) if inp == ‘exit‘: break sk.close()
ThreadingTCPServer源碼剖析
ThreadingTCPServer中類別關係圖
650) this.width=650;" src="http://s4.51cto.com/wyfs02/M02/8B/32/wKioL1hGxXqzYDx_AACNyttKXUs742.png" title="ThreadingTCPServer.png" alt="wKioL1hGxXqzYDx_AACNyttKXUs742.png" />
內部調用流程:
1)執行 BaseServer.__init__ 方法,將自訂的繼承自SocketServer.BaseRequestHandler 的類 MyRequestHandle賦值給 self.RequestHandlerClass
2)執行 TCPServer.__init__ 方法,建立服務端Socket對象並綁定 IP 和 連接埠
3)執行 BaseServer.server_forever 方法,While 迴圈一直監聽是否有用戶端請求到達 ...
當用戶端串連到達伺服器
4)執行 ThreadingMixIn.process_request 方法,建立一個 “線程” 用來處理請求
5)執行 ThreadingMixIn.process_request_thread 方法
6)執行 BaseServer.finish_request 方法
7)執行 self.RequestHandlerClass() 即:執行 自訂 MyRequestHandler 的構造方法(自動調用基BaseRequestHandler的構造方法,在該構造方法中又會調用 MyRequestHandler的handle方法)
精簡源碼
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sk.bind((‘127.0.0.1‘,8009))sk.listen(5) while True: r, w, e = select.select([sk,],[],[],1) print ‘looping‘ if sk in r: print ‘get request‘ request, client_address = sk.accept() t = threading.Thread(target=process, args=(request, client_address)) t.daemon = False t.start() sk.close()
本文出自 “改變從每一天開始” 部落格,請務必保留此出處http://lilongzi.blog.51cto.com/5519072/1880174
自動化營運Python系列之IO多工、SocketServer源碼分析