Python網絡編程-IO阻塞與非阻塞及多路復用


前言

問題:普通套接字實現的服務端的缺陷

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

                       

accept阻塞!

在沒有新的套接字來之前,不能處理已經建立連接的套接字的請求

recv 阻塞!

在沒有接受到客戶端請求數據之前,不能與其他客戶端建立連接

可以用非阻塞接口來嘗試解決這個問題

 IO阻塞與非阻塞

阻塞IO模型

  阻塞IO(blocking IO)的特點:就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。

 什么是阻塞呢?想象這種情形,比如你等快遞,但快遞一直沒來,你會怎么做?有兩種方式:

  • 快遞沒來,我可以先去睡覺,然后快遞來了給我打電話叫我去取就行了。
  • 快遞沒來,我就不停的給快遞打電話說:擦,怎么還沒來,給老子快點,直到快遞來。

很顯然,你無法忍受第二種方式,不僅耽擱自己的時間,也會讓快遞很想打你。
而在計算機世界,這兩種情形就對應阻塞和非阻塞忙輪詢。

  • 非阻塞忙輪詢:數據沒來,進程就不停的去檢測數據,直到數據來。
  • 阻塞:數據沒來,啥都不做,直到數據來了,才進行下一步的處理。

非阻塞IO模型

非阻塞式IO中,用戶進程其實是需要不斷的主動詢問kernel數據准備好了沒有

非阻塞如何利用

  • 吃滿 CPU !
  • 寧可用 while True ,也不要阻塞發呆!
  • 只要資源沒到,就先做別的事!

服務器端

import socket

CONN_ADDR = ('127.0.0.1', 9999)
conn_list = []  # 連接列表
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 開啟socket
sock.setblocking(False) # 設置為非阻塞
sock.bind(CONN_ADDR)  # 綁定IP和端口到套接字
sock.listen(5)          # 監聽,5表示客戶端最大連接數
print('start listen')
while True:
    try:
        conn, addr = sock.accept()  # 被動接受TCP客戶的連接,等待連接的到來,收不到時會報異常
        print('connect by ', addr)
        conn_list.append(conn)
        conn.setblocking(False) # 設置非阻塞
    except BlockingIOError as e:
        pass

    tmp_list = [conn for conn in conn_list]
    for conn in tmp_list:
        try:
            data = conn.recv(1024) # 接收數據1024字節
            if data:
                print('收到的數據是{}'.format(data.decode()))
                conn.send(data)
            else:
                print('close conn',conn)
                conn.close()
                conn_list.remove(conn)
                print('還有客戶端=>',len(conn_list))
        except IOError:
            pass

客戶端

import socket

client = socket.socket()
client.connect(('127.0.0.1', 9999))

while True:
    msg = input(">>>")
    if msg != 'q':
        client.send(msg.encode())
        data = client.recv(1024)
        print('收到的數據{}'.format(data.decode()))
    else:
        client.close()
        print('close client socket')
        break

輸出結果

 

 非阻塞IO模型優點:實現了同時服務多個客戶端,能夠在等待任務完成的時間里干其他活了(包括提交其他任務,也就是 “后台” 可以有多個任務在“”同時“”執行)。

 但是非阻塞IO模型絕不被推薦

非阻塞IO模型缺點:不停地輪詢recv,占用較多的CPU資源。

                                 對應BlockingIOError的異常處理也是無效的CPU花費 !

如何解決:多路復用IO

 

 

 多路復用IO

把socket交給操作系統去監控,相當於找個代理人(select), 去收快遞。快遞到了,就通知用戶,用戶自己去取。

阻塞I/O只能阻塞一個I/O操作,而I/O復用模型能夠阻塞多個I/O操作,所以才叫做多路復用

使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,感覺效率更差。

但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,

即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

epoll是目前Linux上效率最高的IO多路復用技術。

epoll是惰性的事件回調,惰性事件回調是由用戶進程自己調用的,操作系統只起到通知的作用。

epoll實現並發服務器,處理多個客戶端

import socket
import selectors

# 注冊一個epllo事件
# 1. socket
# 2.事件可讀
# 3.回調函數 把一個函數當成變量傳到函數里

def recv_data(conn):
    data = conn.recv(1024)

    if data:
        print('接收的數據是:%s' % data.decode())
        conn.send(data)
    else:
        e_poll.unregister(conn)
        conn.close()

def acc_conn(p_server):
    conn, addr = p_server.accept()
    print('Connected by', addr)
    # 也有注冊一個epoll
    e_poll.register(conn,selectors.EVENT_READ,recv_data)


CONN_ADDR = ('127.0.0.1', 9999)
server = socket.socket()
server.bind(CONN_ADDR)
server.listen(6) # 表示一個客戶端最大的連接數

# 生成一個epllo選擇器實例 I/O多路復用,監控多個socket連接
e_poll = selectors.EpollSelector() # window沒有epoll使用selectors.DefaultSelector()實現多路復用
e_poll.register(server, selectors.EVENT_READ, acc_conn)

# 事件循環
while True:
    # 事件循環不斷地調用select獲取被激活的socket
    events = e_poll.select()
    #print(events)
    """[(SelectorKey(fileobj= < socket.socket
     laddr = ('127.0.0.1',9999) >,……data = < function acc_conn at 0xb71b96ec >), 1)]
    """
    for key, mask in events:
        call_back = key.data
        #print(key.data)
        call_back(key.fileobj)

輸出結果


多路復用模型,使用select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,

            

 


免責聲明!

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



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