Python網絡編程篇之select和epoll


1. select 原理

在多路復⽤的模型中, ⽐較常⽤的有select模型和epoll模型。 這兩個都是系統接⼝, 由操作系統提供。 當然, Pythonselect模塊進⾏了更⾼級的封裝。

⽹絡通信被Unix系統抽象為⽂件的讀寫, 通常是⼀個設備, 由設備驅動程序提供, 驅動可以知道⾃身的數據是否可⽤。 ⽀持阻塞操作的設備驅動通常會實現⼀組⾃身的等待隊列, 如讀/寫等待隊列⽤於⽀持上層(⽤戶層)所需的blocknon-block操作。 設備的⽂件的資源如果可⽤( 可讀或者可寫) 則會通知進程, 反之則會讓進程睡眠, 等到數據到來可⽤的時候, 再喚醒進程。這些設備的⽂件描述符被放在⼀個數組中, 然后select調⽤的時候遍歷這個數組, 如果對於的⽂件描述符可讀則會返回改⽂件描述符。 當遍歷結束之后,如果仍然沒有⼀個可⽤設備⽂件描述符, select讓⽤戶進程則會睡眠, 直到等待資源可⽤的時候在喚醒, 遍歷之前那個監視的數組。 每次遍歷都是依次進⾏判斷的。 

# -*- 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]

客戶端

# -*- 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⽬前⼏乎在所有的平台上⽀持, 其良好跨平台⽀持也是它的⼀個優點。

缺點

select的⼀個缺點在於單個進程能夠監視的⽂件描述符的數量存在最⼤限制,Linux上⼀般為1024, 可以通過修改宏定義甚⾄重新編譯內核的⽅式提升這⼀限制, 但是這樣也會造成效率的降低。⼀般來說這個數⽬和系統內存關系很⼤, 具體數⽬可以cat /proc/sys/fs/filemax察看。 32位機默認是1024個。 64位機默認是2048.socket進⾏掃描時是依次掃描的, 即采⽤輪詢的⽅法, 效率較低。當套接字⽐較多的時候, 每次select()都要通過遍歷FD_SETSIZESocket完成調度, 不管哪個Socket是活躍的, 都遍歷⼀遍。 這會浪費很多CPU間。 

2. epoll的優點:

1. 沒有最⼤並發連接的限制, 能打開的FD(指的是⽂件描述符, 通俗的理解就是套接字對應的數字編號)的上限遠⼤於1024

2. 效率提升, 不是輪詢的⽅式, 不會隨着FD數⽬的增加效率下降。 只有活躍可⽤的FD才會調⽤callback函數; 即epoll最⼤的優點就在於它只管活躍的連接, ⽽跟連接總數⽆關, 因此在實際的⽹絡環境中, epoll的效率就會遠遠⾼於selectpoll。 說明

EPOLLIN ( 可讀)

EPOLLOUT ( 可寫)

EPOLLET ET模式)

epoll對⽂件描述符的操作有兩種模式: LTlevel trigger) 和ETedge 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()

 


免責聲明!

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



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