在select模塊中, 有三種方法實(shí)現(xiàn)IO多路復(fù)用并發(fā)服務(wù)器
- select
- poll
- epoll
select的原理: 在多路復(fù)用的模型中,比較常用的有select模型和epoll模型钩蚊。這兩個(gè)都是系統(tǒng)接口逊朽,由操作系統(tǒng)提供搔预。當(dāng)然资锰,Python的select模塊進(jìn)行了更高級(jí)的封裝。
網(wǎng)絡(luò)通信被Unix系統(tǒng)抽象為文件的讀寫寡喝,通常是一個(gè)設(shè)備糙俗,由設(shè)備驅(qū)動(dòng)程序提供,驅(qū)動(dòng)可以知道自身的數(shù)據(jù)是否可用预鬓。支持阻塞操作的設(shè)備驅(qū)動(dòng)通常會(huì)實(shí)現(xiàn)一組自身的等待隊(duì)列巧骚,如讀/寫等待隊(duì)列用于支持上層(用戶層)所需的block或non-block操作。設(shè)備的文件的資源如果可用(可讀或者可寫)則會(huì)通知進(jìn)程格二,反之則會(huì)讓進(jìn)程睡眠劈彪,等到數(shù)據(jù)到來可用的時(shí)候,再喚醒進(jìn)程顶猜。
這些設(shè)備的文件描述符被放在一個(gè)數(shù)組中粉臊,然后select調(diào)用的時(shí)候遍歷這個(gè)數(shù)組,如果對(duì)于的文件描述符可讀則會(huì)返回改文件描述符驶兜。當(dāng)遍歷結(jié)束之后扼仲,如果仍然沒有一個(gè)可用設(shè)備文件描述符远寸,select讓用戶進(jìn)程則會(huì)睡眠,直到等待資源可用的時(shí)候在喚醒屠凶,遍歷之前那個(gè)監(jiān)視的數(shù)組驰后。每次遍歷都是依次進(jìn)行判斷的。
例如使用select實(shí)現(xiàn)echo(回顯)服務(wù)器
import select
import socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 7788))
server.listen(5)
inputs = [server, sys.stdin]
running = True
while True:
# 調(diào)用 select 函數(shù)矗愧,阻塞等待
readable, writeable, exceptional = select.select(inputs, [], [])
# 數(shù)據(jù)抵達(dá)灶芝,循環(huán)
for sock in readable:
# 監(jiān)聽到有新的連接
if sock == server:
conn, addr = server.accept()
# select 監(jiān)聽的socket
inputs.append(conn)
# 監(jiān)聽到鍵盤有輸入
elif sock == sys.stdin:
cmd = sys.stdin.readline()
running = False
break
# 有數(shù)據(jù)到達(dá)
else:
# 讀取客戶端連接發(fā)送的數(shù)據(jù)
data = sock.recv(1024)
if data:
sock.send(data)
else:
# 移除select監(jiān)聽的socket
inputs.remove(sock)
sock.close()
# 如果檢測(cè)到用戶輸入敲擊鍵盤,那么就退出
if not running:
break
server.close()
但是在底層原理中, select和epoll 都是使用輪詢?cè)韥韺?shí)現(xiàn)的. epoll是觸發(fā)通知機(jī)制
epoll的優(yōu)點(diǎn):
- 沒有最大并發(fā)連接的限制唉韭,能打開的FD(指的是文件描述符夜涕,通俗的理解就是套接字對(duì)應(yīng)的數(shù)字編號(hào))的上限遠(yuǎn)大于1024
- 效率提升,不是輪詢的方式属愤,不會(huì)隨著FD數(shù)目的增加效率下降女器。只有活躍可用的FD才會(huì)調(diào)用callback函數(shù);即epoll最大的優(yōu)點(diǎn)就在于它只管你“活躍”的連接住诸,而跟連接總數(shù)無關(guān)驾胆,因此在實(shí)際的網(wǎng)絡(luò)環(huán)境中,epoll的效率就會(huì)遠(yuǎn)遠(yuǎn)高于select和poll贱呐。
import socket
import select
# 創(chuàng)建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 可重復(fù)綁定
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定本機(jī)地址端口
s.bind(("", 7788))
# 變?yōu)楸粍?dòng)服務(wù)器
s.listen(1024)
# 創(chuàng)建一個(gè)epoll對(duì)象
epoll = select.epoll()
# 在epoll中注冊(cè)s套接字(注意此處不是直接用是s, 而是使用s的fileno)
epoll.register(s.fileno(), select.EPOLLIN|select.EPOLLET)
# 創(chuàng)建兩個(gè)字典, 來保存fileno和與其對(duì)應(yīng)的套接字和地址
connections = {}
addresses = {}
# 開始等待客戶端發(fā)送來的信息
while True:
# 對(duì)epoll中的套接字進(jìn)行掃描
epollList = epoll.poll()
# 對(duì)掃描到的事件進(jìn)行判斷
for fd,events in epollList:
# 如果判斷是s套接字
if fd == s.fileno():
conn,addr = s.accept()
print("有新的客戶端到來...%s"%str(addr))
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
epoll.register(conn.fileno(), selecte.EPOLLIN|select.EPOLLET)
# 如果是接收到了數(shù)據(jù)
elif events == select.EPOLLIN:
recvData = connections[fd].recv(1024)
if len(recvData) > 0:
print("recvData: %s"%recvData)
else:
epoll.unregister(fd)
connections[fd].close()
print("%s....offline....."%str(addresses[fd]))
值得注意的是,只有在linux2.6以上系統(tǒng)中才會(huì)有epoll, 在mac系統(tǒng)中是kqueue.