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

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

 

併發服務器開發

併發服務器開發,使得一個服務器能夠近乎同一時刻爲多個客戶端提供服務。實現併發的方式有多種,下面以多進程,多線程,IO多路複用等方式實現併發。這裏使用網絡編程中的TCP服務器和客戶端通訊爲例子。python

多進程併發阻塞

利用進程把客戶端和服務器進行管理,當有新的客戶端鏈接到服務器時,就建立一個新的進程來管理,經過操做系統的調度,從而實現了併發的操做web

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多路複用和線程池在提升併發性上應用場景的區別

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

線程池則適合短鏈接併發的狀況,好比普通的web業務系統,Tomcat的Servlet容器默認選擇就是線程池(雖然3.0後支持異步,但通常狀況下不常使用)。因爲處理短鏈接的線程很快會退出,所以可以充分發揮線程池複用線程的好處。異步

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

 

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

相關文章
相關標籤/搜索