11 非阻塞套接字與IO多路複用(進階)

一、非阻塞套接字

第一部分 基本IO模型

1.普通套接字實現的服務端的缺陷

一次只能服務一個客戶端!python

2.普通套接字實現的服務端的瓶頸!!!

accept阻塞!shell

在沒有新的套接字來以前,不能處理已經創建鏈接的套接字的請求。編程

recv 阻塞!服務器

在沒有接受到客戶端請求數據以前,併發

不能與其餘客戶端創建鏈接!app

3.普通服務器的IO模型

第二部分 非阻塞套接字

1.非阻塞套接字與普通套接字的區別
>>> import socket
>>> server = socket.socket()
# 講socket設置成非阻塞
>>> server.setblocking(False)   # 注意!這必需要在其餘操做以前!
>>> server.bind(('',8080))
>>> server.listen(5)
​
>>> server.accept()     # 沒有鏈接就引起BlockingIOError
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    server.accept()
  File "E:\python\lib\socket.py", line 205, in accept
    fd, addr = self._accept()
BlockingIOError: [WinError 10035] 沒法當即完成一個非阻止性套接字操做。
​
# 使用一個客戶端(普通的就行,不須要非阻塞)鏈接過來
>>> conn,addr = server.accept()     # 有鏈接則正確返回
>>> conn.recv(1024)     # 沒有鏈接數據就引起BlockingIOError
Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    conn.recv(1024)
BlockingIOError: [WinError 10035] 沒法當即完成一個非阻止性套接字操做。
2.使用非阻塞套機字實現阻塞的服務端
# 原來的recv
while True:
    try:
        recv_data = conn.recv(1024)
        break
    except BlockingIOError:
        pass
        
# 原來的accept
while True:
    try:
        conn,addr = server.accept()
        break
    except:BlockingIOError:
        pass
3。非阻塞客戶端套接的注意點

connect操做必定會引起BlockingIOError異常socket

若是鏈接沒有創建,那麼send操做引起OSError異常函數

第三部分 非阻塞IO模型

第四部分 使用非阻塞套接字實現併發

1.總體思路

吃滿 CPU !寧肯用 whileTrue ,也不要阻塞發呆!spa

只要資源沒到,就先作別的事!將代碼順序重排,避開阻塞!操作系統

2.實現了什麼?

併發服務多個客戶端!

3.編程範式
import socket
server = socket.socket()        # 生成套接字
server.setblocking(False)       # 非阻塞
server.bind(('',7788))
server.listen(1000)
​
# 咱們如今生成的非阻塞套接字,非阻塞套接字在執行accept跟recv的時候不會阻塞,可是會報錯,
# 因此咱們寫非阻塞的併發服務器須要用到異常處理
​
all_conn = []   # 用來保存咱們全部的已經生成的套接字(這個客戶端還在鏈接着)
while True:
    # 處理鏈接,生成對等鏈接套接字
    try:
        conn,addr = server.accept()
        conn.setblocking(False)     # conn是新生成的,須要給它設置一下非阻塞
        all_conn.append(conn)   # 這一行代碼的前提是,上面一行代碼正常返回
    except BlockingIOError:
        pass
    for conn in all_conn:
        try:    # 只負責接受數據
            recv_data = conn.recv(1024)
​
            if recv_data:
                res = recv_data.decode()
                print(res)
                conn.send(recv_data)
            else:
                conn.close()
                all_conn.remove(conn) # 客戶端關閉鏈接,就把它移除
        except BlockingIOError:
            pass

二、IO多路複用

第一部分 不完美的CPU利用率

關鍵一: 任何Python操做都是須要花費CPU資源的 !

關鍵二: 若是資源尚未到達,那麼

​ accept、recv以及

​ send(在connect沒有完成時)

​ 操做都是無效的CPU花費 !

關鍵三: 對應BlockingIOError的異常處理

​ 也是無效的CPU花費 !

第二部分 epoll是真正的答案!

IO多路複用技術

咱們把socket交給操做系統去監控

2.epoll是惰性的事件回調

惰性事件回調是由用戶進程本身調用的。

操做系統只起到通知的做用。

3.爲何是epoll ?

目前Linux上效率最高的IO多路複用 技術 !

第三部分 IO多路複用選擇器

1.註冊惰性事件回調
>>> import socket
>>> import selectors
>>> server = socket.socket()
>>> server.bind(('',9000))
>>> server.listen(1000)
>>> selector = selectors.EpollSelector()    # 實例化一個 epoll 選擇器
>>> def create_conn(server):
...     conn,addr = server.accept()
...     return conn
... 
# 套接字、事件、回調函數
>>> selector.register(server,selectors.EVENT_READ,create_conn)  
SelectorKey(fileobj=<socket.socket fd=3,    # 生成一個打包對象
family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 9000)>,   # fileobj是對應套接字
fd=3,
events=1,   # 事件(1 表示EVENT_READ)
data=<function create_conn at 0xb70b7b24>)  # data是對應的回掉函數
2.事件回調
events = selector.select()      # 查詢,返回全部已經準備好的資源打包對象
print(enents)       # 是一個 ‘二元組’ 的列表
[(SelectorKey(fileobj=<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 8888)>, fd=4, events=1, data=<function accept at 0xb71f292c>), 1)]
# 咱們只須要關心,每一個元祖的第一項(即打包對象,其中包含了對應的套接字與回掉函數)
# 接下來並不須要關心是什麼套接字,什麼事件,只須要調用對應的對調函數便可
callbeck = events[0][0].data
sock = events[0][0].fileojb
callbeck(sock)
3.編程範式
# 使用EpollSelector,實現一個併發的服務器
import socket
import selectors    # IO多路複用選擇器的模塊,接口,調用epoll
​
epoll_selector = selectors.EpollSelector()  # 建立一個用來和epoll通訊的選擇器
​
server = socket.socket()
server.bind(('',8888))
server.listen(1000)
​
def read(conn):
    recv_data = conn.recv(1024)
    if recv_data:
        print(recv_data.decode())
        conn.send(recv_data)
    else:
        epoll_selector.unregister(conn)     # 如今數據已經傳輸完了,那我如今就不用再去監控它了,因此# 關閉監控
        conn.close()    # 關閉鏈接
def accept(server):
    conn, addr = server.accept()    # 生成一個對等鏈接套接字
    # 要準備接受數據
    epoll_selector.register(conn, selectors.EVENT_READ, read)
​
epoll_selector.register(server,selectors.EVENT_READ, accept)   # 註冊事件在能夠讀的時候的回調函數
# 事件循環(主動去問epoll,哪些socket能夠回調了,若是有了,那我就回調)
while True:
    events = epoll_selector.select()     # 查詢全部的已經準備好的事件,返回一個列表(二元組列表)
    # a, b = events
    for key, mask in events:    # 第一項是咱們須要用的
        callback = key.data    # 從key裏面把回掉函數拿出來
        sock = key.fileobj     # 從key裏面把咱們註冊的那個socket拿出來
        callback(sock)
相關文章
相關標籤/搜索