python實現並發服務器實現方式(多線程/多進程/select/epoll)


python實現並發服務器實現方式(多線程/多進程/select/epoll)

 

並發服務器開發

並發服務器開發,使得一個服務器可以近乎同一時刻為多個客戶端提供服務。實現並發的方式有多種,下面以多進程,多線程,IO多路復用等方式實現並發。這里使用網絡編程中的TCP服務器和客戶端通信為例子。

多進程並發阻塞

利用進程把客戶端和服務器進行管理,當有新的客戶端連接到服務器時,就創建一個新的進程來管理,通過操作系統的調度,從而實現了並發的操作

from multiprocessing import Process from socket import * def recv_data(new_socket, client_info): print("客戶端{}已經連接".format(client_info)) # 接受數據 raw_data = new_socket.recv(1024) while raw_data: print(f"收到來自{client_info}的數據:{raw_data}") raw_data = new_socket.recv(1024) new_socket.close() def main(): # 實例化socket對象 socket_server = socket(AF_INET, SOCK_STREAM) # 設置端口復用 socket_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 綁定IP地址和端口 socket_server.bind(("", 7788)) # 改主動為被動,監聽客戶端 socket_server.listen(5) while True: # 等待連接 new_socket, client_info = socket_server.accept() p = Process(target=recv_data, args=(new_socket, client_info)) p.start() # 多進程會復制父進程的內存空間,所以父進程中new_socket也必須關閉 new_socket.close() if __name__ == '__main__': main() 

多線程並發阻塞

多線程和多進程類似,只是線程間共享內存空間,要注意變量的管理

from threading import Thread from socket import * def recv_data(new_socket, client_info): print("客戶端{}已經連接".format(client_info)) # 接受數據 raw_data = new_socket.recv(1024) while raw_data: print(f"收到來自{client_info}的數據:{raw_data}") raw_data = new_socket.recv(1024) new_socket.close() def main(): # 實例化socket對象 socket_server = socket(AF_INET, SOCK_STREAM) # 設置端口復用 socket_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 綁定IP地址和端口 socket_server.bind(("", 7788)) # 改主動為被動,監聽客戶端 socket_server.listen(5) while True: # 等待連接 new_socket, client_info = socket_server.accept() p = Thread(target=recv_data, args=(new_socket, client_info)) p.start() # 多線程共享一片內存區域,所以這里不用關閉 # new_socket.close() if __name__ == '__main__': main() 

多路復用IO---select模型

在操作系統層面上,系統提供了一個select接口,它會輪詢給定的文件描述符狀態,如果其中有描述符的狀態改變,select()就會返回有變化的文件描述符。

from socket import * import select # 實例化對象 socket_server = socket(AF_INET, SOCK_STREAM) # 綁定IP和端口 socket_server.bind(("", 7788)) # 將主動模式改為被動模式 socket_server.listen(5) # 創建套接字列表 socket_lists = [socket_server] # 等待客戶端連接 while True: # 只監聽讀的狀態,程序阻塞在這,不消耗CPU,如果列表里面的值讀狀態變化后,就解阻塞 read_lists, _, _ = select.select(socket_lists, [], []) # 循環有變化的套接字 for sock in read_lists: # 判斷是否是主套接字 if sock == socket_server: # 獲取新連接 new_socket, client_info = socket_server.accept() print(f"客戶端:{client_info}已連接") # 添加到監聽列表中 socket_lists.append(new_socket) else: # 不是主客戶端,即接收消息 raw_data = sock.recv(1024) if raw_data: print(f"接收數據:{raw_data.decode('gb2312')}") else: # 如果沒有數據,則客戶端斷開連接 sock.close() # 從監聽列表中刪除該套接字 socket_lists.remove(sock) 

優點:良好的跨平台支持

缺點:1.監測的文件描述符數量有最大限制,Linux系統一般為1024,可以修改宏定義或者內核進行修改,但是會造成效率低下;2.對文件描述符采用輪詢機制,每個文件描述符都會詢問一遍,這樣很消耗CPU時間

多路復用IO---epoll模型

為了解決select輪詢機制造成的效率低下問題,則引入了epoll接口。相較於select的兩大優勢。1.沒有文件描述符最大數量的限制(最大數量則看內存大小);2.采用時間通知機制,當文件描述符狀態有變時,主動通知內核進行調度。其中print注釋是為了打印對象,查看對象是什么。

from socket import * import select # 創建socket對象 sock_server = socket(AF_INET, SOCK_STREAM) # 綁定IP和端口 sock_server.bind(("", 7788)) # 將主動模式設置為被動模式,監聽連接 sock_server.listen(5) # 創建epoll監測對象 epoll = select.epoll() # print("未注冊epoll對象:{}".format(epoll)) # 注冊主套接字,監控讀狀態 epoll.register(sock_server.fileno(), select.EPOLLIN) # print("注冊了主套接字后:{}".format(epoll)) # 創建字典,保存套接字對象 sock_dicts = {} # 創建字典,保存客戶端信息 client_dicts = {} while True: # print("所有套接字:{}".format(sock_dicts)) # print("所有客戶端信息:{}".format(client_dicts)) # 程序阻塞在這,返回文件描述符有變化的對象 poll_list = epoll.poll() # print("有變化的套接字:{}".format(poll_list)) for sock_fileno, events in poll_list: # print("文件描述符:{},事件:{}".format(sock_fileno, events)) # 判斷是否是主套接字 if sock_fileno == sock_server.fileno(): # 創建新套接字 new_sock, client_info = sock_server.accept() print(f"客戶端:{client_info}已連接") # 注冊到epoll監測中 epoll.register(new_sock.fileno(), select.EPOLLIN) # 添加到套接字字典當中 sock_dicts[new_sock.fileno()] = new_sock client_dicts[new_sock.fileno()] = client_info else: # 接收消息 raw_data = sock_dicts[sock_fileno].recv(1024) if raw_data: print(f"來自{client_dicts[sock_fileno]}的數據:{raw_data.decode('gb2312')}") else: # 關閉連接 sock_dicts[sock_fileno].close() # 注銷epoll監測對象 epoll.unregister(sock_fileno) # 數據為空,則客戶端斷開連接,刪除相關數據 del sock_dicts[sock_fileno] del client_dicts[sock_fileno] 
 
 

IO多路復用和線程池在提高並發性上應用場景的區別

多路復用適用於需要保持大量閑置(區別於計算密集型)長連接的業務場景,例如聊天室。這樣的好處是能夠避免不斷的創建新線程,導致系統資源浪費。需要注意,多路復用本質上是復用單線程的,回調函數的執行必然是有可能長時間阻塞的,所以如果涉及到耗時的計算密集型任務,則會大大降低系統處理其它連接的響應速度。

線程池則適合短連接並發的情況,比如普通的web業務系統,Tomcat的Servlet容器默認選擇就是線程池(雖然3.0后支持異步,但一般情況下不常使用)。由於處理短連接的線程很快會退出,因此能夠充分發揮線程池復用線程的好處。

當然,多路復用和線程池可以結合起來使用,效果也許更好,但代碼復雜度也會相應提高,需要更好的設計。建議根據業務場景選擇相應的技術,避免過早優化。

 

一點補充:很多人不知道協程該歸於哪個技術范疇。協程除了在用戶態通過棧切換實現控制流的切換以外,還通常將多路復用和線程池結合起來。比如go語言內置的協程就是在多線程的基礎上實現了一套調度策略,調度策略的實現建立在操作系統內核提供的IO多路復用技術之上,同時go語言參考計算機硬件情況自動將協程綁定在若干個系統線程之上,從而實現資源的高效率利用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM