使用yield实现协程操作例子
#!/usr/bin/env python3
#yield实现协程
import time,queue
def consumer(name):
print("开始吃包子。。。")
while True:
new_baozi = yield #函数暂时在这里停止
print("%s 开始吃包子 %s " % (name, new_baozi))
def producer():
c1 = con.__next__() #consumer通过yield变成了迭代器,要通过__next__方法来执行
c2 = con2.__next__()
n = 0
while n < 5:
n += 1
con.send(n) #向con中的yield发送n,即把函数中的yield换成n
con2.send(n)
print("制作了 包子 %s " % n)
if __name__ == "__main__":
con = consumer('lalala')
con2 = consumer('hahaha')
producer()
greenlet是用C实现的协程模块,相比与python自带的yield,他可以使你在任意函数之随意切换。
#!/usr/bin/env python3
#通过greenlet来实现协程
from greenlet import greenlet
def test1():
print(12)
gr2.switch() #在这里实现跳转,转到test2
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch() #在这里实现跳转,转到test1
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是greenlet,它是以C扩展模块形式介入Python的轻量级协程。在greenlet全部运行在主程序操作系统进程的内部,但它们被协作式调度。
#!/usr/bin/env python3
#通过gevent来实现协程
import gevent
def func1():
print(" the fun1 start...")
gevent.sleep(2) #遇到堵塞就跳转(根据堵塞的时间长短)
print(' the fun1 end...')
def func2():
print("\033[31;1m the func2 start \033[0m")
gevent.sleep(1)
print('\033[31;1m the func2 end \033[0m')
def func3():
print("\033[32;1m the func3 start \033[0m")
gevent.sleep(5)
print('\033[32;1m the func3 end \033[0m')
def func4():
print("\033[33;1m the func4 start \033[0m")
gevent.sleep(4)
print('\033[33;1m the func4 end \033[0m')
gevent.joinall(
[
gevent.spawn(func1),
gevent.spawn(func2),
gevent.spawn(func3),
gevent.spawn(func4),
]
)
同步可异步的性能区别
#!/usr/bin/env python3
#同步和异步性能的区分
import gevent
def tasf(n):
gevent.sleep(1)
print("The func print %s" % n)
def tongbu():
for i in range(10):
tasf(i)
def yibu():
threads = [gevent.spawn(tasf, i) for i in range(10)]
gevent.joinall(threads)
print('tongbu')
tongbu()
print('yibu')
yibu()
上面的程序的主要部分时间tasf函数封装到greenlet内部线程的gevent.spawn。初始化的greenlet列表存放在threads列表中,次数组被传给gevent.joinall函数后,后者阻塞当前的流程,并执行所有给定的greenlet。执行流程只会在所有greenlet执行完成后才会继续向下走。
当遇到阻塞是会自动切换任务
#!/usr/bin/env python3
#遇到阻塞是自动区分
from gevent import monkey;monkey.patch_all()
import gevent
from urllib.request import urlopen
def f(url):
print('GET: %s' % url)
resp = urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))
gevent.joinall([
gevent.spawn(f,'https://www.baidu.com'),
gevent.spawn(f,'https://www.sina.com'),
])
通过gevent实现单线程下的多socket并发
#!/usr/bin/env python3
#通过gevent实现单线程下的多socket并发
#server端
import gevent
from gevent import socket,monkey
monkey.patch_all()#将python标准库中的网络借口不阻塞
def server(port):
s = socket.socket()
s.bind(('127.0.0.1',port))
s.listen(500)
while True:
cli, addr = s.accept()
gevent.spawn(handle_request, cli) #执行handle_request函数,cli是参数
def handle_request(conn):
try:
while True:
data = conn.recv(1024) #接收数据,这里设置成不阻塞
print("recv:",data)
conn.send(data)
if not data:
conn.shutdown(socket.SHUT_WR) #如果接收为空值,结束
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server(8001)
#!/usr/bin/env python3
#通过gevent实现单线程下的多socket并发
#client端
import socket
host = "127.0.0.1"
port = 8001
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
while True:
user_input = bytes(input(),encoding = "utf8")
s.sendall(user_input)
data = s.recv(1024)
print(repr(data))
s.close()
100个并发的socket链接
#!/usr/bin/env python3
#通过gevent实现单线程下的多socket并发
#client端2
import socket
import threading
def sock_conn():
client = socket.socket()
client.connect(('127.0.0.1',8001))
count = 0
while True:
client.send(('hello %s % count').encode('utf-8'))
data = client.recv(1024)
print('[%s]recv from server:' % threading.get_ident(),data.decode())
count += 1
client.close()
for i in range(100):
t = threading.Thread(target=sock_conn)
t.start()
通常,我们写服务器处理模型的时候,有以下几种模型:
以上几种方式各有优缺点:
综合考虑各方面因素,一般普遍认为第三种方式是大多网络服务器采用的方式
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个时间循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
让我们用例子来比较和对比一下单线程、多线程以及事件驱动模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有三个任务需要完成,每个任务都在等待I/O操作室阻塞自身。阻塞在I/O操作桑所花的时间已经用灰色表示出来了。
当应用程序需要在任务间共享可变数据时,这也是个不错的选择,因为这不需要采用同步处理。
网络应用通常都符合上述特点,这使得他们能够很好的切合事件驱动模型。
在进行解释之前,首先要说明几个概念:
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)位4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保护用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两个部分,一部分为内核空间,一部分作为用户空间。针对linux操作系统而言,将最高的1G字节供内核使用,成为内核空间,而将较低的3G字节供个进程使用,称为用户空间。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并回复以前挂起的某个进程的执行。这种行为称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
总之就是非常消耗资源
注:进程控制块,是操作系统核心中的一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其他进程并发执行的进程。或者说,操作系统是根据PCB来对并发执行的进程进行控制可管理的。PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用户描述进程情况及控制进程运行所需的全部信息
正在执行的进程,由于期待的某些事情未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作等,则有系统自动执行阻塞原语(Block),是自己有运行状态变为阻塞状态。可见进程的阻塞是进程自身的一种阻塞行为,也因此只有处于运行状态的进程(获得CPU),才能将其转为阻塞状态。当进程进入阻塞状态,是不占CPU资源的。
文件描述符(File descriptor)是计算机科学中的一个术语。是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,他是一个索引值,指向内核为每一个进程所维护的该进程打开文件 的记录表。当程序大爱一个现有文件或者穿件一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往之适用于UNIX、Linux这样的操作系统。
缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区中拷贝到应用程序的地址空间。
缓存I/O的缺点:
对于一次I/O访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,他会经历两个阶段:
1. 等待数据准备
2. 将数据从内核拷贝到进程中
正式因为这两个阶段,linux系统产生了下面五种网路模式的方案:
- 阻塞I/O(blocking IO)
- 非阻塞I/O(nonblocking IO)
- I/O多路复用(IO multiplexing)
- 信号驱动I/O(signal driven IO)
- 异步I/O(asynchronous IO)
注:信号驱动IO在实际中并不常用,所以下面只提及剩下的四中IO模型
在linux中,没默认情况下所有的socket都是blocking,一个典型的读操作流程是这样的:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待。也就是说数据被拷贝到操作系统内核的缓冲区是需要一个过程的。而用户在这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的量的阶段都被block了
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行度操作是,流程是这个样子的:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么他并不会block用户进程,而是立刻返回一个error。从用户的角度讲,它发起一个read操作后,并不需要等待,而是马上得到一个结果。用户进程判断结果是个error时,他就知道数据还么有准备好,于是他可以再次发送read操作。一旦kernel中的数据转备好了,并且有再次收到了用户进程的system call,那么他马上就将数据拷贝到了用户内存,然后返回。
所以, nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
I/O 多路复用就是本文讲到的select、pool、epool,有些地方也称这种I/O方式为event driven IO(事件驱动IO)。select、epool的好吃就在于单个进程就可以同时处理多个网络连接的IO。他的基本原理就是select、pool、epool这个function会不断地轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程都会被block,而同时,kernel会“监听”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O多路复用的特点是通过一种机制,一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图跟阻塞IO的图其实没有太大的不同,事实上,还等差一些。因为这里需要使用两个system call(select和recvfrom),而阻塞IO(blocking IO)只调用了一个system call(recvfrom)。但是,用select的有事在于他可以同时处理多个connection。
所以,如果处理连接数不是很高 的话,使用select、epool的web server不一定比使用multithreading + nlocking IO的web server性能更好,可能延迟还更大。select、epool的优势并不是对于单个链接能处理的更快,而是在于能处理更多的链接。
在 IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的,只不过process是被select这个函数block,而不是被socket IO给block。
Linux下的asynchronous IO其实用的很少,流程如下:
用户进程发起read操作之后,立刻就可以做其他的事情。而另一方面,从kernel的角度,当他收到一个asynchronous read之后,首先他会立刻返回。所以不会对用户进程产生任何的block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操作完成,而non-blockig IO在kernel还准备数据的情况下立刻返回
同步IO(synchronous IO)和异步IO(asynchronous)的区别
在说明同步IO和异步IO的区别之前,需要先给出两个定义。POSIX的定义是这个样子的:
二者的区别就在于同步IO做IO操作的时候会将process阻塞,安装这个定义,之前所述的阻塞IO(blocking IO),非阻塞IO(non-blocking IO),IO多路复用(IO multiiplexing)都属于同步IO。
注:虽然非阻塞IO没有被阻塞,但是也是同步IO,因为定义中提高的IO操作是指真实的IO操作,就是例子中的recvfrom这个system call。非阻塞IO在执行recvform这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是当kernel准备好数据的时候,recvform会将数据从kernel拷贝到 用户内存中,这时候进程是被block了,在这段时间内进程是被block的。
而异步IO不一样,当进程发起IO操作后,就直接返回,再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整体过程中,进程完全没有被block。
各个IO模型的比较示意图:
通过上面的图片,可以发现非阻塞IO和异步IO的区别还是很明显的。在非阻塞IO中,虽然劲曾大部分时间不会被block,但是他仍要求进程去主动的check,并且当数据准备完成后,越要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而异步IO则完全同。他就像是用户进程将整个IO操作交给了其他人(kernel)完成,然后别人做完之后发信号通知。在此期间,用户进程不需要检查IO的操作状态,也不需要主动的拷贝数据。
select、pool、epool都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就续或是写就绪),能够通知程序进行相应的读写操作。但select、pool、epool本质上都是同步IO,因为他们都需要在读写事件就绪后自己负责读写,也就是说这个读写的过程是阻塞的,而异步IO则不需要自己负责读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
select
select(rlict, wlist, xlist, timeout = None)
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)
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
struct pollfd{
int fd; /*文件描述符*/
short events;/*请求事件查看*/
short revents;/*返回时间验证*/
};
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.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
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()方法直接调用操作系统的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]
客户端完整代码
#!/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()
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)