1. select 原理數組
在多路復⽤的模型中, ⽐較常⽤的有select模型和epoll模型。 這兩個都是系統接⼝, 由操做系統提供。 固然, Python的select模塊進⾏了更⾼級的封裝。併發
⽹絡通訊被Unix系統抽象爲⽂件的讀寫, 一般是⼀個設備, 由設備驅動程序提供, 驅動能夠知道⾃身的數據是否可⽤。 ⽀持阻塞操做的設備驅動一般會實現⼀組⾃身的等待隊列, 如讀/寫等待隊列⽤於⽀持上層(⽤戶層)所需的block或non-block操做。 設備的⽂件的資源若是可⽤( 可讀或者可寫) 則會通知進程, 反之則會讓進程睡眠, 等到數據到來可⽤的時候, 再喚醒進程。這些設備的⽂件描述符被放在⼀個數組中, 而後select調⽤的時候遍歷這個數組, 若是對於的⽂件描述符可讀則會返回改⽂件描述符。 當遍歷結束以後,若是仍然沒有⼀個可⽤設備⽂件描述符, select讓⽤戶進程則會睡眠, 直到等待資源可⽤的時候在喚醒, 遍歷以前那個監視的數組。 每次遍歷都是依次進⾏判斷的。 app
# -*- coding: utf-8 -*- # 2017/11/25 22:55 # select 模擬一個socket server,注意socket必須在非阻塞狀況下才能實現IO多路複用。 # 接下來經過例子瞭解select 是如何經過單進程實現同時處理多個非阻塞的socket鏈接的。 import select import socket import queue server = socket.socket() server.bind(('localhost',9000)) server.listen(1000) server.setblocking(False) # 設置成非阻塞模式,accept和recv都非阻塞 # 這裏若是直接 server.accept() ,若是沒有鏈接會報錯,因此有數據才調他們 # BlockIOError:[WinError 10035] 沒法當即完成一個非阻塞性套接字操做。 msg_dic = {} inputs = [server,] # 交給內核、select檢測的列表。 # 必須有一個值,讓select檢測,不然報錯提供無效參數。 # 沒有其餘鏈接以前,本身就是個socket,本身就是個鏈接,檢測本身。活動了說明有連接 outputs = [] # 你往裏面放什麼,下一次就出來了 while True: readable, writeable, exceptional = select.select(inputs, outputs, inputs) # 定義檢測 #新來鏈接 檢測列表 異常(斷開) # 異常的也是inputs是: 檢測那些鏈接的存在異常 print(readable,writeable,exceptional) #[<socket.socket fd=500, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 61685)>] [] [] for r in readable: if r is server: # 有數據,表明來了一個新鏈接 conn, addr = server.accept() print("來了個新鏈接",addr) inputs.append(conn) # 把鏈接加到檢測列表裏,若是這個鏈接活動了,就說明數據來了 # inputs = [server.conn] # 【conn】只返回活動的鏈接,但怎麼肯定是誰活動了 # 若是server活動,則來了新鏈接,conn活動則來數據 msg_dic[conn] = queue.Queue() # 初始化一個隊列,後面存要返回給這個客戶端的數據 else: try : data = r.recv(1024) # 注意這裏是r,而不是conn,多個鏈接的狀況 print("收到數據",data) # r.send(data) # 不能直接發,若是客戶端不收,數據就沒了 msg_dic[r].put(data) # 往裏面放數據 outputs.append(r) # 放入返回的鏈接隊列裏 except ConnectionResetError as e: print("客戶端斷開了",r) if r in outputs: outputs.remove(r) #清理已斷開的鏈接 inputs.remove(r) #清理已斷開的鏈接 del msg_dic[r] ##清理已斷開的鏈接 for w in writeable: # 要返回給客戶端的鏈接列表 data_to_client = msg_dic[w].get() # 在字典裏取數據 w.send(data_to_client) # 返回給客戶端 outputs.remove(w) # 刪除這個數據,確保下次循環的時候不返回這個已經處理完的鏈接了。 for e in exceptional: # 若是鏈接斷開,刪除鏈接相關數據 if e in outputs: outputs.remove(e) inputs.remove(e) del msg_dic[e]
客戶端socket
# -*- coding: utf-8 -*- # 2017/11/25 22:55 import socket client = socket.socket() client.connect(('localhost', 9000)) while True: cmd = input('>>> ').strip() if len(cmd) == 0 : continue client.send(cmd.encode('utf-8')) data = client.recv(1024) print(data.decode()) client.close()
優勢函數
select⽬前⼏乎在全部的平臺上⽀持, 其良好跨平臺⽀持也是它的⼀個優勢。測試
缺點spa
select的⼀個缺點在於單個進程可以監視的⽂件描述符的數量存在最⼤限制,在Linux上⼀般爲1024, 能夠經過修改宏定義甚⾄從新編譯內核的⽅式提高這⼀限制, 可是這樣也會形成效率的下降。⼀般來講這個數⽬和系統內存關係很⼤, 具體數⽬能夠cat /proc/sys/fs/filemax察看。 32位機默認是1024個。 64位機默認是2048.對socket進⾏掃描時是依次掃描的, 即採⽤輪詢的⽅法, 效率較低。當套接字⽐較多的時候, 每次select()都要經過遍歷FD_SETSIZE個Socket來完成調度, 無論哪一個Socket是活躍的, 都遍歷⼀遍。 這會浪費不少CPU時間。 操作系統
2. epoll的優勢:code
1. 沒有最⼤併發鏈接的限制, 能打開的FD(指的是⽂件描述符, 通俗的理解就是套接字對應的數字編號)的上限遠⼤於1024server
2. 效率提高, 不是輪詢的⽅式, 不會隨着FD數⽬的增長效率降低。 只有活躍可⽤的FD纔會調⽤callback函數; 即epoll最⼤的優勢就在於它只管你「活躍」的鏈接, ⽽跟鏈接總數⽆關, 所以在實際的⽹絡環境中, epoll的效率就會遠遠⾼於select和poll。 說明
EPOLLIN ( 可讀)
EPOLLOUT ( 可寫)
EPOLLET ( ET模式)
epoll對⽂件描述符的操做有兩種模式: LT( level trigger) 和ET( edge trigger) 。
LT模式是默認模式, LT模式與ET模式的區別以下:
LT模式: 當epoll檢測到描述符事件發⽣並將此事件通知應⽤程序, 應⽤程序能夠不⽴即處理該事件
ET模式: 當epoll檢測到描述符事件發⽣並將此事件通知應⽤程序, 應⽤程序必須⽴即處理
# -*- coding: utf-8 -*- # 2017/11/26 13:54 import socket import select # 建立套接字 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 設置能夠重複使⽤綁定的信息 s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 綁定本機信息 s.bind(("127.0.0.1",9000)) # 變爲被動 s.listen(10) # 建立⼀個epoll對象 epoll=select.epoll() # 測試, ⽤來打印套接字對應的⽂件描述符 print(s.fileno()) print(select.EPOLLIN|select.EPOLLET) # 註冊事件到epoll中 # epoll.register(fd[, eventmask]) # 注意, 若是fd已經註冊過, 則會發生異常 # 將建立的套接字添加到epoll的事件監聽中 epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET) connections = {} addresses = {} # 循環等待客戶端的到來或者對⽅發送數據 while True: # epoll 進⾏ fd 掃描的地⽅ -- 未指定超時時間則爲阻塞等待 epoll_list=epoll.poll() # 對事件進⾏判斷 for fd,events in epoll_list: print(fd) print(events) # 若是是socket建立的套接字被激活 if fd == s.fileno(): conn,addr=s.accept() print('有新的客戶端到來%s'%str(addr)) # 將 conn 和 addr 信息分別保存起來 connections[conn.fileno()] = conn addresses[conn.fileno()] = addr # 向 epoll 中註冊 鏈接 socket 的 可讀 事件 epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET) elif events == select.EPOLLIN: # 從激活 fd 上接收 recvData = connections[fd].recv(1024) if len(recvData)>0: print('recv:%s'%recvData) else: # 從 epoll 中移除該 鏈接 fd epoll.unregister(fd) # server 側主動關閉該 鏈接 fd connections[fd].close() print("%s---offline---"%str(addresses[fd]))
client
# -*- coding: utf-8 -*- # 2017/11/25 22:55 import socket client = socket.socket() client.connect(('127.0.0.1', 9000)) while True: cmd = input('>>> ').strip() if len(cmd) == 0 : continue client.send(cmd.encode('utf-8')) data = client.recv(1024) print(data.decode()) client.close()