select、pool、epool都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就续或是写就绪),能够通知程序进行相应的读写操作。但select、pool、epool本质上都是同步IO,因为他们都需要在读写事件就绪后自己负责读写,也就是说这个读写的过程是阻塞的,而异步IO则不需要自己负责读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
select
select(rlict, wlist, xlist, timeout = None)
- 1
select 函数监视的文件描述符分3类,分别是wtiteds、readfds和exceptfds。调用select函数会阻塞,直到有描述符就绪(有数据可读、可写或者有exxcept),或者超时(timeout指定等待时间,如果立刻返回设为努null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select目前几乎早所有平台上支持,其良好的跨平台支持也是他的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在linux上一般为1024,可以修改宏定义甚至重新编译内核的方式提升这一限制,但这样会造成效率的降低。
poll
int poll(struct pokkfd *fds, unsigned int nfds, int timeout)
- 1
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
struct pollfd{
int fd; /*文件描述符*/
short events;/*请求事件查看*/
short revents;/*返回时间验证*/
};
- 1
- 2
- 3
- 4
- 5
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的文件描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符在获取已经就绪的socket。事实上,同时链接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本,相对于select和poll来说,epoll更加灵活,没有描述符的限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
epoll操作过程
epoll造作过程需要三个接口,分别如下:
int epoll_create(int size);//创建一个wpoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);
- 1
- 2
- 3
1.int epoll_create(int size);
创建一个epoll的句柄,size用来高速内核这个监听的数目已共有多大,这个参数不同于select()中的第一个参数给出最大监听的fd+1的值,参数size并不是限制了wpoll佐能监听的描述符的最大个数,只是对内核初始分配内部数据结构的一个建议。(fd = 文件描述符)
当创建好epoll句柄之后,他就会占用一个fd值,在Linux下如果查看/proc/进程id/fd,是能够看到这个fd的,多以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
函数是对指定描述符fd执行op操作。
参数epfd:是epoll_create()的返回值
参数op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件
参数fd:是需要监听的fd
参数epoll_event:是告诉内核需要监听什么事
3.int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
epoll select例子
#_*_coding:utf-8_*_
import socket, logging
import select, errno
logger = logging.getLogger("network-server")
def InitLog():
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler("network-server.log")
fh.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
if __name__ == "__main__":
InitLog()
try:
# 创建 TCP socket 作为监听 socket
listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
except socket.error as msg:
logger.error("create socket failed")
try:
# 设置 SO_REUSEADDR 选项 对unix套接字的设置
listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except socket.error as msg:
logger.error("setsocketopt SO_REUSEADDR failed")
try:
# 进行 bind -- 此处未指定 ip 地址,即 bind 了全部网卡 ip 上
listen_fd.bind(('', 2003))
except socket.error as msg:
logger.error("bind failed")
try:
# 设置 listen 的 backlog 数
listen_fd.listen(10)
except socket.error as msg:
logger.error(msg)
try:
# 创建 epoll 句柄
epoll_fd = select.epoll()
# 向 epoll 句柄中注册 监听 socket 的 可读 事件
#登记一个新的文件描述符,如果文件描述符已经被创建则引发一个OSError错误
#fd是目标文件描述符的操作
#register(fd[, eventmask])
#events是由不同的EPOLL常熟组成的,EPOLLIN | EPOLLOUT | EPOLLPRI
epoll_fd.register(listen_fd.fileno(), select.EPOLLIN)
except select.error as msg:
logger.error(msg)
connections = {}
addresses = {}
datalist = {}
while True:
# epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
#poll([timeout=-1[, maxevents=-1]]) -> [(fd, events), (...)]
#Wait for events on the epoll file descriptor(文件描述符) for a maximum time of timeout
#in seconds (as float). -1 makes poll wait indefinitely.
#Up to maxevents are returned to the caller.
epoll_list = epoll_fd.poll()
for fd, events in epoll_list:
# 若为监听 fd 被激活
if fd == listen_fd.fileno():
# 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄
conn, addr = listen_fd.accept()
logger.debug("accept connection from %s, %d, fd = %d" % (addr[0], addr[1], conn.fileno()))
# 将连接 socket 设置为 非阻塞
conn.setblocking(0)
# 向 epoll 句柄中注册 连接 socket 的 可读 事件
epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
# 将 conn 和 addr 信息分别保存起来
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
elif select.EPOLLIN & events:
# 有 可读 事件激活
datas = ''
while True:
try:
# 从激活 fd 上 recv 10 字节数据
data = connections[fd].recv(10)
# 若当前没有接收到数据,并且之前的累计数据也没有
if not data and not datas:
# 从 epoll 句柄中移除该 连接 fd
epoll_fd.unregister(fd)
# server 侧主动关闭该 连接 fd
connections[fd].close()
logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
break
else:
# 将接收到的数据拼接保存在 datas 中
datas += data
except socket.error as msg:
# 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况
# 这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理
if msg.errno == errno.EAGAIN:
logger.debug("%s receive %s" % (fd, datas))
# 将已接收数据保存起来
datalist[fd] = datas
# 更新 epoll 句柄中连接d 注册事件为 可写
epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT)
break
else:
# 出错处理
epoll_fd.unregister(fd)
connections[fd].close()
logger.error(msg)
break
elif select.EPOLLHUP & events:
# 有 HUP 事件激活
epoll_fd.unregister(fd)
connections[fd].close()
logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
elif select.EPOLLOUT & events:
# 有 可写 事件激活
sendLen = 0
# 通过 while 循环确保将 buf 中的数据全部发送出去
while True:
# 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置
sendLen += connections[fd].send(datalist[fd][sendLen:])
# 在全部发送完毕后退出 while 循环
if sendLen == len(datalist[fd]):
break
# 更新 epoll 句柄中连接 fd 注册事件为 可读
epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET)
else:
# 其他 epoll 事件不进行处理
continue
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
python select解析
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就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
python select
python的select()方法直接调用操作系统的IO接口,它监控sockets、open、files和pipes(所有带fileno()方法的文件句柄)何时变成readable和writeable,或者通信错误,select()是得同时监控多个连接变得简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过python解释器。
注:select()只用于Unix的文件对象,不适用于windows
下面通过echo server例子来理解select是如何通过单进程实现同时处理多个非阻塞的socket连接的
服务端代码:
#!/usr/bin/env python3
import select
import socket
import sys
import queue
#创建cosket连接
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#设置socket连接为非阻塞
server.setblocking(False)
#设置主机的ip和端口
server_address = ('127.0.0.1', 10000)
#打印信息
print(sys.stderr, 'starting up on %s port %s' % server_address)
#绑定
server.bind(server_address)
#监听的最大连接数为5
server.listen(5)
#将想要从socket客户端接收来的数据放到一个列表中
inputs = [ server ]
#将想要发送到客户端的数据放在一个列表中
outputs = [ ]
#信息队列:接收和发送的数据都会存在这里,有select取出来在发出去
message_queues = {}
while inputs:
print( '\nwaiting for the next event')
#调用select时会阻塞和等待新的连接或数据进来
#readable代表有可接收数据的socket连接
#writable代表可进行发送操作的socket连接
#exceptional代表当连接出错时的报错信息
readable, writable, exceptional = select.select(inputs, outputs, inputs)
#循环取出接收socket
for s in readable:
#如果是一开始的server(监听所有连接的socket),代表已经准备接收一个新的连接了
if s is server:
#准备接收新的连接
connection, client_address = s.accept()
#打印客户端的地址
print('new connection from', client_address)
#为了这个监听的socket可以处理多个连接,将其设置为非阻塞
connection.setblocking(False)
#将接收的socket连接放进inputs列表中
inputs.append(connection)
#在消息字典中创建一个队列,用来装接收的信息
message_queues[connection] = queue.Queue()
else:
#如果不是初始的监听socket,表示socket已经要接收信息了,首先先接受信息
data = s.recv(1024)
#如果接收到了数据
if data:
#打印信息
print(sys.stderr, 'received "%s" from %s' % (data, s.getpeername()) )
#在消息字典的对应队列中将接收的信息添加
message_queues[s].put(data)
#如果循环的这个socket连接不在要发送的socket连接列表里
if s not in outputs:
#将这个socket连接添加到需要发送的socket连接列表里
outputs.append(s)
#如果没有接收到信息,说明已经接受完了,可以断开连接了
else:
#打印信息
print('closing', client_address, 'after reading no data')
#如果没接收到信息,那也就不需要向客户端返回信息,所以如果在发送表中这个socket连接还存在,就把他删除了
if s in outputs:
outputs.remove(s) #既然客户端都断开了,我就不用再给它返回数据了,所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉
inputs.remove(s) #inputs中也删除掉
s.close() #把这个连接关闭掉
#连接删掉了,信息字典中相应的队列信息也就没用了,删掉
del message_queues[s]
#当socket连接在发送列表里的时候
for s in writable:
try:
#获取消息字典里相应的队列信息
next_msg = message_queues[s].get_nowait()
except queue.Empty:
#当字典为空的时候,就是信息都取完了,将连接送发送列表中删除
print('output queue for', s.getpeername(), 'is empty')
outputs.remove(s)
else:
#获取成功的时候,将消息发送出去
print( 'sending "%s" to %s' % (next_msg, s.getpeername()))
s.send(next_msg)
#当连接报错的时候
for s in exceptional:
#打印信息
print('handling exceptional condition for', s.getpeername() )
#将错误的socket连接从接收表中删除
inputs.remove(s)
#如果在发送表中也有,就把发送表中的也清了
if s in outputs:
outputs.remove(s)
#关闭连接
s.close()
#在消息字典中删除相应的信息
del message_queues[s]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
客户端完整代码
#!/usr/bin/env python3
import socket
import sys
#消息文本模板
messages = [ 'This is the message. ',
'It will be sent ',
'in parts.',
]
#需要连接的主机地址
server_address = ('localhost', 10000)
#创建客户端的socket连接列表
socks = [ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
socket.socket(socket.AF_INET, socket.SOCK_STREAM),
]
#打印信息并将socket连接列表中的全部链接连接到目标主机
print(sys.stderr, 'connecting to %s port %s' % server_address)
for s in socks:
s.connect(server_address)
#遍历消息模板
for message in messages:
#向服务端发送信息
for s in socks:
print(sys.stderr, '%s: sending "%s"' % (s.getsockname(), message))
s.send(message)
#接收服务端返回的信息
for s in socks:
data = s.recv(1024)
print >>sys.stderr, '%s: received "%s"' % (s.getsockname(), data)
if not data:
print >>sys.stderr, 'closing socket', s.getsockname()
s.close()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
拓展
selectors模块
该模块允许基于select模块原语构建的高级别和高效的/输出多路复用。鼓励用户使用这个模块,除非他们希望对使用的os级别原语进行精确控制。
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn, addr = sock.accept() # Should be ready
print('accepted', conn, 'from', addr)
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
data = conn.recv(1000) # Should be ready
if data:
print('echoing', repr(data), 'to', conn)
conn.send(data) # Hope it won't block
else:
print('closing', conn)
sel.unregister(conn)
conn.close()
sock = socket.socket()
sock.bind(('localhost', 10000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32