在简明网络I/O模型文章能够知道经常使用的IO
模型。其中同步模型中,使用多路复用I/O
能够提升服务器的性能。node
在多路复用的模型中,比较经常使用的有select
模型和poll
模型。这两个都是系统接口,由操做系统提供。固然,Python
的select
模块进行了更高级的封装。select
与poll
的底层原理都差很少。下面就介绍select
。python
网络通讯被Unix
系统抽象为文件的读写,一般是一个设备,由设备驱动程序提供,驱动能够知道自身的数据是否可用。支持阻塞操做的设备驱动一般会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block
或non-block
操做。设备的文件的资源若是可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。nginx
这些设备的文件描述符被放在一个数组中,而后select
调用的时候遍历这个数组,若是对于的文件描述符可读则会返回改文件描述符。当遍历结束以后,若是仍然没有一个可用设备文件描述符,select
让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历以前那个监视的数组。每次遍历都是线性的。数组
select
涉及系统调用和操做系统相关的知识,所以单从字面上理解其原理仍是比较乏味。用代码来演示最好不过了。使用python
的select
模块很容易写出下面一个回显服务器:服务器
1网络 2app 3curl 4异步 5socket 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 |
import select import socket import sys
HOST = 'localhost' PORT = 5000 BUFFER_SIZE = 1024
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen(5)
inputs = [server, sys.stdin] running = True
while True: try: # 调用 select 函数,阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) except select.error, e: break
# 数据抵达,循环 for sock in readable: # 创建链接 if sock == server: conn, addr = server.accept() # select 监听的socket inputs.append(conn) elif sock == sys.stdin: junk = sys.stdin.readlines() running = False else: try: # 读取客户端链接发送的数据 data = sock.recv(BUFFER_SIZE) if data: sock.send(data) if data.endswith('\r\n\r\n'): # 移除select监听的socket inputs.remove(sock) sock.close() else: # 移除select监听的socket inputs.remove(sock) sock.close() except socket.error, e: inputs.remove(sock)
server.close() |
运行上述代码,使用curl
访问http://localhost:5000
,便可看命令行返回请求的HTTP request
信息。
下面详细解析上述代码的原理。
1 2 3 |
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen(5) |
上述代码使用socket
初始化一个TCP
套接字,并绑定主机地址和端口,而后设置服务器监听。
1 |
inputs = [server, sys.stdin] |
这里定义了一个须要select
监听的列表,列表里面是须要监听的对象(等于系统监听的文件描述符)。这里监听socket
套接字和用户的输入。
而后代码进行一个服务器无线循环。
1 2 3 4 5 |
try: # 调用 select 函数,阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) except select.error, e: break |
调用了select
函数,开始循环遍历监听传入的列表inputs
。若是没有curl
服务器,此时没有创建tcp
客户端链接,所以改列表内的对象都是数据资源不可用。所以select
阻塞不返回。
客户端输入curl http://localhost:5000
以后,一个套接字通讯开始,此时input
中的第一个对象server
由不可用变成可用。所以select
函数调用返回,此时的readable
有一个套接字对象(文件描述符可读)。
1 2 3 4 5 6 |
for sock in readable: # 创建链接 if sock == server: conn, addr = server.accept() # select 监听的socket inputs.append(conn) |
select
返回以后,接下来遍历可读的文件对象,此时的可读中只有一个套接字链接,调用套接字的accept()
方法创建TCP
三次握手的链接,而后把该链接对象追加到inputs
监视列表中,表示咱们要监视该链接是否有数据IO
操做。
因为此时readable
只有一个可用的对象,所以遍历结束。再回到主循环,再次调用select
,此时调用的时候,不只会遍历监视是否有新的链接须要创建,仍是监视刚才追加的链接。若是curl
的数据到了,select
再返回到readable
,此时在进行for
循环。若是没有新的套接字,将会执行下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
try: # 读取客户端链接发送的数据 data = sock.recv(BUFFER_SIZE) if data: sock.send(data) if data.endswith('rnrn'): # 移除select监听的socket inputs.remove(sock) sock.close() else: # 移除select监听的socket inputs.remove(sock) sock.close() except socket.error, e: inputs.remove(sock) |
经过套接字链接调用recv
函数,获取客户端发送的数据,当数据传输完毕,再把监视的inputs
列表中除去该链接。而后关闭链接。
整个网络交互过程就是如此,固然这里若是用户在命令行中输入中断,inputs
列表中监视的sys.stdin
也会让select
返回,最后也会执行下面的代码:
1 2 3 |
elif sock == sys.stdin: junk = sys.stdin.readlines() running = False |
有人可能有疑问,在程序处理sock
链接的是时候,假设又输入了curl
对服务器请求,将会怎么办?此时毫无疑问,inputs
里面的server
套接字会变成可用。等如今的for
循环处理完毕,此时select
调用就会返回server
。若是inputs
里面还有上一个过程的conn
链接,那么也会循环遍历inputs
的时候,再一次针对新的套接字accept
到inputs
列表进行监视,而后继续循环处理以前的conn
链接。如此有条不紊的进行,直到for
循环结束,进入主循环调用select
。
任什么时候候,inputs
监听的对象有数据,下一次调用select
的时候,就会繁返回readable
,只要返回,就会对readable
进行for
循环,直到for
循环结束在进行下一次select
。
主要注意,套接字创建链接是一次IO
,链接的数据抵达也是一次IO
。
尽管select
用起来挺爽,跨平台的特性。可是select
仍是存在一些问题。select
须要遍历监视的文件描述符,而且这个描述符的数组还有最大的限制。随着文件描述符数量的增加,用户态和内核的地址空间的复制所引起的开销也会线性增加。即便监视的文件描述符长时间不活跃了,select
仍是会线性扫描。
为了解决这些问题,操做系统又提供了poll
方案,可是poll
的模型和select
大体至关,只是改变了一些限制。目前Linux
最早进的方式是epoll
模型。
许多高性能的软件如nginx
, nodejs
都是基于epoll
进行的异步。