Select-poll-epoll-简介

1。 Python的select()方法直接调用操作系统的IO接口,它监控sockets,open files, and pipes(所有带fileno()方法的文件句柄)何时变成readable 和writeable, 或者通信错误,select()使得同时监控多个连接变的简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过Python的解释器。

2. select()负责监控检测很多个socket链接,与从内核态到用户态的copy没有任何关系。

IO multiplexing就是我们说的select,poll,epoll,(IO多路复用)有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

首先列一下,sellect、poll、epoll三者的区别 

select  select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

poll  poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

         没有文件链接数的限制了。

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

epoll  直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。Windows不支持epoll.

epoll可以同时支持水平触发(100个链接里面有2个活跃了,通知用户后,等待用户取数据。此时数据仍然保存在内核态。下一次还会继续通知用户取数据。)和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

没有最大链接数的限制。epoll只是IO多路复用。银行卡办好了,打电话通知你去取。

需求:用select (多路复用)模拟一个 socket server。可以接收多并发。

1. 一开始是检测自己,如果我有活动了,就说明有客户端要连我了。

#用select去模拟socket,实现单线程下的多路复用

import select
import socket
import queue

server=socket.socket()
server.bind(('localhost',9000))
server.listen(1024)

server.setblocking(False) #设置为不阻塞,accept/recv没有数据都不阻塞,只会报错。

inputs=[server,] #先检测自己,如果我有活动了,说明有客户端要连我了。
outputs=[]

select.select(inputs,outputs,inputs)
#第一个参数:操作系统发现100个里面有1个在活动,就会返回这100个。需要检测哪些链接就放进来。
#第二个参数:
#第三个参数:让操作系统检测100个的哪个有问题,就把有问题的返回。
server.accept()

 运行结果:卡住了,有客户端进来时才会不卡。

C:abccdxdddOldboypython-3.5.2-embed-amd64python.exe C:/abccdxddd/Oldboy/Py_Exercise/Day10/select_socket_server.py

2.服务器端

#用select去模拟socket,实现单线程下的多路复用

import select
import socket
import queue

server=socket.socket()
server.bind(('localhost',9000))
server.listen(1024)

server.setblocking(False) #设置为不阻塞,accept/recv没有数据都不阻塞,只会报错。

inputs=[server,] #先检测自己,如果我有活动了,说明有客户端要连我了。
#inputs=[server,conn]
outputs=[]
while True:
    readable,writeable,exceptional=select.select(inputs,outputs,inputs)
    #第一个参数:操作系统发现100个里面有1个在活动,就会返回这100个。需要检测哪些链接就放进来。
    #第二个参数:
    #第三个参数:让操作系统检测100个的哪个有问题,就把有问题的返回。
    print(readable,writeable,exceptional)
    for r in readable:
        if r is server: #代表来了一个新链接
            conn,addr=server.accept()
            print('来了个新链接',addr)
            inputs.append(conn) #是因为这个新建立的连接还没有发数据过来,现在就接收的话,程序会报错。
            #所以要想实现这个客户端发数据来时server端能知道,就需要让select再监测这个Conn。
        else:
            data=conn.recv(1024)
            print('收到数据',data)
            conn.send(data)

 客户端:

import socket
HOST = 'localhost'  # The remote host
PORT = 9000  # The same port as used by the server

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

while True:
    msg = bytes(input(">>:"), encoding="utf8")
    s.sendall(msg)
    data = s.recv(1024)
    # print(data)
    print('Received', repr(data))  #repr:格式化输出

s.close()

运行结果: 有2个链接的情况下,无法多次接收数据

C:abccdxdddOldboypython-3.5.2-embed-amd64python.exe C:/abccdxddd/Oldboy/Py_Exercise/Day10/select_socket_server.py
[<socket.socket fd=240, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000)>] [] []
来了个新链接 ('127.0.0.1', 53605)
[<socket.socket fd=336, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 53605)>] [] []
收到数据 b'1'
[<socket.socket fd=240, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000)>] [] []
来了个新链接 ('127.0.0.1', 60337)
[<socket.socket fd=348, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000), raddr=('127.0.0.1', 60337)>] [] []
收到数据 b'123'

3. server端进行修改:

#用select去模拟socket,实现单线程下的多路复用

import select
import socket
import queue

server=socket.socket()
server.bind(('localhost',9000))
server.listen(1024)

server.setblocking(False) #设置为不阻塞,accept/recv没有数据都不阻塞,只会报错。

inputs=[server,] #先检测自己,如果我有活动了,说明有客户端要连我了。
#inputs=[server,conn]
outputs=[]
while True:
    readable,writeable,exceptional=select.select(inputs,outputs,inputs)
    #第一个参数:操作系统发现100个里面有1个在活动,就会返回这100个。需要检测哪些链接就放进来。
    #第二个参数:
    #第三个参数:让操作系统检测100个的哪个有问题,就把有问题的返回。
    print(readable,writeable,exceptional)
    for r in readable:
        if r is server: #代表来了一个新链接
            conn,addr=server.accept()
            print('来了个新链接',addr)
            inputs.append(conn) #是因为这个新建立的连接还没有发数据过来,现在就接收的话,程序会报错。
            #所以要想实现这个客户端发数据来时server端能知道,就需要让select再监测这个Conn。
        else:
            data=r.recv(1024)
            print('收到数据',data)
            r.send(data)

 至此运行正常

原文地址:https://www.cnblogs.com/momo8238/p/7373771.html