select 原理
io多路複用:沒有使用多進程和多線程的情況下完成多個套接字的使用
select 能夠完成一些套接字的檢查,從頭到尾檢查一遍後,標記哪些套接字是否可以收數據,返回的時候,就返回能接收數據的套接字,返回的是列表。select是由操作系統提供的,效率要高些,非常快的方式檢測哪些套接字可以接收數據。select是跨平臺的,在window也可以用。
from socket import *
import select
import sys
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server_socket.bind(("", 9999))
server_socket.listen(5)
# 客戶列表
socket_list = [server_socket, sys.stdin] # 讀取的列表
writeable_list = []
try:
while True:
print("-----11111----")
read_list, write_list, error_list = select.select(socket_list, writeable_list, socket_list)
# print(read_list, write_list, error_list)
for sock in read_list:
if sock == server_socket:
print("sock==server_socket")
new_socket, new_addr = server_socket.accept()
# 將new_socket添加到列表、
socket_list.append(new_socket) # [server_socket,new_socket]
elif sock == sys.stdin:
msg = sys.stdin.readline()
print("鍵盤輸入的內容:", msg)
else:
recv_msg = sock.recv(1024)
if len(recv_msg) == 0:
print("客戶端斷開了")
socket_list.remove(sock)
elif len(recv_msg) > 0:
print("收到消息:", recv_msg.decode("utf-8"))
# 回聲
sock.send(recv_msg) # 發送同樣的信息
except:
print("----->有異常了")
finally:
server_socket.close()
網絡通信被Unix系統抽象爲文件的讀寫,通常是一個設備,由設備驅動程序提供,驅動可以知道自身的數據是否可用。支持阻塞操作的設備驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用於支持上層(用戶層)所需的block或non-block操作。設備的文件的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。
這些設備的文件描述符被放在一個數組中,然後select調用的時候遍歷這個數組,如果對於的文件描述符可讀則會返回改文件描述符。當遍歷結束之後,如果仍然沒有一個可用設備文件描述符,select讓用戶進程則會睡眠,直到等待資源可用的時候在喚醒,遍歷之前那個監視的數組。每次遍歷都是依次進行判斷的。
select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增長。同時,由於網絡響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。
select 回顯服務器
echo(回顯)服務器代碼
from socket import AF_INET,socket,SO_REUSEADDR,SOCK_STREAM,SOL_SOCKET
from select import select
def main():
#創建tcp的socket套接字
server_socket = socket(AF_INET,SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
#綁定端口
server_socket.bind(("",9999))
#設置監聽
server_socket.listen(5)
#客戶端列表
socket_lists = [server_socket]
try:
while True:
#檢測列表client_lists那些socket可以接收數據,
#檢測列表[]那些套接字(socket)可否發送數據
#檢測列表[]那些套接字(socket)是否產生了異常
print("select--111")
#這個select函數默認是堵塞,當有客戶端鏈接的時候解除阻塞,
# 當有數據可以接收的時候解除阻塞,當客戶端斷開的時候解除阻塞
readable, wirteable,excep = select(socket_lists,[],[])
# print("select--2222")
# print(111)
for sock in readable:
#接收數據
if sock == server_socket:
print("sock == server_socket")
#有新的客戶端鏈接進來
new_socket,new_address = sock.accept()
#新的socket添加到列表中,便於下次socket的時候能檢查到
socket_lists.append(new_socket)
else:
# print("sock.recv(1024)....")
#此時的套接字sock是直接可以取數據的
recv_data = sock.recv(1024)
if len(recv_data) > 0:
print("從[%s]:%s" % (str(new_address),recv_data))
sock.send(recv_data)
else:
print("客戶端已經斷開")
#客戶端已經斷開,要移除
sock.close()
socket_lists.remove(sock)
finally:
#關閉套接字
server_socket.close()
if __name__ == "__main__":
main()
總結:
優點支持跨平臺
select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點。
缺點
1)select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,Linux上一般爲1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。一般來說這個數目和系統內存關係很大,具體數目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048
2)對socket進行掃描時是依次掃描的,即採用輪詢的方法,效率較低。
3)當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。
poll解決了套接字有上限的問題
poll解決了套接字有上限的問題,效率和select一樣,都是輪詢方式。
select -->最多1024個套接字-->採用輪詢方式進程檢測套接字是否可以接收等
poll -->解決了支持套接字上線問題-->採用輪詢方式進程檢測
epoll-->解決支持上限問題-->採用的是事件通知
selec和poll都是輪詢檢測方式,效率比較低, epoll採用的事件通知機制,這個時候epoll效率遠高於select和poll
epoll的優點
1)沒有最大併發連接的限制,能打開的FD(指的是文件描述符,通俗的理解就是套接字對應的數字編號)的上限遠大於1024。File Description
2)效率提升,不是輪詢的方式,不會隨着FD數目的增加效率下降。只有活躍可用的FD纔會調用callback函數;即epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,epoll的效率就會遠遠高於select和poll。
3)epoll採用的事件通知機制
epoll服務器代碼from select import *
from socket import *
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.bind(("", 9999))
server_socket.listen(5)
# 創建epoll對象
epol = epoll()
# 註冊事件
epol.register(server_socket.fileno(), EPOLLIN | EPOLLET)
# 裝socket的列表
socket_list = {}
# 裝socket的地址
socket_addr = {}
while True:
print("-------1111111-------")
epoll_list = epol.poll() # [(fd,事件),(),(),()]
print("--------22222------")
for fd, event in epoll_list:
# 有新的連接
if fd == server_socket.fileno():
new_socket, new_addr = server_socket.accept()
# \往子典中添加數據
socket_list[new_socket.fileno()] = new_socket
socket_addr[new_socket.fileno()] = new_addr
# 註冊事件
epol.register(new_socket.fileno(), EPOLLIN | EPOLLET)
elif event == EPOLLIN:
print("哈哈哈!收到數據了")
new_socket = socket_list[fd]
new_addr = socket_addr[fd]
# 讀取數據
content = new_socket.recv(1024)
if len(content) > 0:
print("收到數據是:", content.decode("utf-8"))
else:
epol.unregister(fd) # 取消註冊
new_socket.close()
print(new_addr, "下線了....")
EPOLLIN和EPOLLET 說明
EPOLLIN (可讀)
EPOLLOUT (可寫)
EPOLLET (ET模式)
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:
LT模式:當epoll檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll時,會再次響應應用程序並通知此事件。直到你出來爲止。
ET模式:當epoll檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。下次將不會在通知。
ET要比LT效率高