在高級篇二中,咱們講解了5中經常使用的IO模型,理解這些經常使用的IO模型,對於編寫服務器程序有很大的幫助,能夠提升咱們的併發速度!由於在網絡中通訊主要的部分就是IO操做。在這一篇當中咱們會重點講解在第二篇當中提到的IO複用模型,即select機制。其實select機制有一些缺陷,後來產生了一種更加高效的機制epoll,稍後會講解!html
1、select機制web
1. 原理:select能夠理解成一個監聽器,能夠監聽多個文件描述符。當某個文件描述符的狀態發生改變了(可讀/可寫),操做系統就會發送消息給應用程序,去處理數據。編程
2. 優勢:幾乎全部平臺都支持,跨平臺支持性較好。數組
3. 缺點:服務器
(1). 當個進程/線程可監視的文件描述符數量有限制。網絡
(2). 對文件描述符的掃描是線性的,採用輪詢的方式,每次都是從頭一直掃描到結尾,當文件描述符的列表變大時,會至關浪費時間和CPU併發
(3). 把包含大量文件描述符的數組從內核空間拷貝到用戶空間,當數組小的時候可能還好,可是隨着數組的增大會變得很浪費資源。socket
4. 水平觸發:tcp
當select()把狀態發生變化的文件描述符報告給進程以後,若是進程沒有進行任何處理,那麼下次select()還會報告這些文件描述符。函數
2、epoll
epoll能夠當作是select/poll(本質就是select)的增強版,打破了不少select的約束,以及添加了一些其餘的功能!
1. 爲何epoll效率很高呢?
epoll最大的特色是隻告訴服務器有哪些文件描述符(fd)發生了變化。若是服務器不去處理相應的fd,那麼操做系統就會把這個fd丟棄,再也不給服務器發送消息(邊緣觸發)!除此以外,epoll是採用事件監聽的方式通知,這也是epoll的魅力所在!
2.原理:
(1). 註冊在epoll中的文件描述符,操做系統的事件監聽會去監聽文件描述符集合(fd_set)
(2). 若是有fd發生了變化,那麼事件監聽會向操做系統報告發生變化的fd
(3). 操做系統會給服務器發送消息,通知它你關注的fd有變化,去處理吧
(4). 此時服務器就去共享內存中讀取數據了!
3. 優勢:
(1). 沒有最大鏈接數的限制。
(2). 不採用輪詢的方式去處理fd,而是採用事件監聽的方式,即哪一個fd有事件發生,OS通知服務器使用相應的回調函數來處理fd
(3). 內存拷貝:當有數據到來時,操做系統會給服務器發送通知去處理數據。經過採用共享內存的方式加快用戶空間與內核空間消息的傳遞速度。
4. 誤區:
並非在任何狀況下,epoll都要比select/poll高效,只有當不少鏈接請求到來時纔會很高效!
3、epoll編程模型
(1). 建立1個epoll對象
(2). 告訴epoll對象,在指定的fd上監聽指定的事件
(3). 詢問epoll對象,自從上次查詢後,哪些fd上發生了哪些事件
(4). 在這些fd上執行一些操做
(5). 告訴epoll對象,修改fd列表或註冊事件,並監控
(6). 重複步驟3-5,直到完成
(7). 銷燬epoll對象
4、代碼實現
1 """
2 利用非阻塞和epoll來實現一個服務器 3 """
4 import socket 5
6 import select 7
8
9 class WebServer: 10 """定義一個web服務器"""
11
12 def __init__(self): 13 # 1.建立TCP 服務器
14 self.tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 # 複用端口
16 self.tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 17 # 2.綁定端口
18 self.tcp_server.bind(('', 6767)) 19 # 3.設爲被動套接字
20 self.tcp_server.listen(128) 21
22 def run(self): 23 """運行一個服務器"""
24 #1.把服務器設置爲非阻塞模式
25 self.tcp_server.setblocking(False) 26 #2.建立一個epoll對象併爲服務器註冊一個可接受鏈接的事件
27 epoll = select.epoll() 28 epoll.register(self.tcp_server.fileno(), select.EPOLLIN) 29 client_dict = dict() # 讓fd與client創建關聯
30 #3.服務器接受客戶端的請求
31 while True: 32 # 4.監聽epoll中哪一個fd發生了什麼事件
33 epoll_list = epoll.poll() 34 for fd, event in epoll_list: 35 if fd == self.tcp_server.fileno(): 36 # 有客戶端來鏈接被動套接字服務器
37 client, addr = self.tcp_server.accept() 38 # print(addr)
39 # 把客戶端註冊到epoll中
40 epoll.register(client.fileno(), select.EPOLLIN) 41 # 把客戶端和客戶端對應的fd添加到client字典中去
42 client_dict[client.fileno()] = client 43 else: 44 # 有客戶端發送數據過來,可是該如何去得到這個客戶端呢?
45 data = client_dict[fd].recv(1024).decode('utf-8') 46 if data: 47 # 說明客戶端發送數據過來了
48 print(data) 49 client_dict[fd].send('我已經收到你的數據了!\n'.encode('utf-8')) 50 else: 51 # 說明客戶端已經關閉了
52 client_dict[fd].close() 53
54 client_dict.popitem() 55 # 須要把該客戶端註冊的事件取消掉
56 epoll.unregister(fd) 57
58 # 遍歷client字典中每一個客戶端對應的fd
59 for item in client_dict.items(): 60
61 print('fd:{}--->addr:{}'.format(item[0], item[1])) 62 print('-'*50) 63 # 關閉服務器
64 self.tcp_server.close() 65
66
67
68 def main(): 69 #1.初始化一個TCP服務器
70 server = WebServer() 71 #2.運行一個服務器
72 server.run() 73
74
75 if __name__ == '__main__': 76 main()