网络socket输入操作分为两个阶段:等待网络数据到达和将到达内核的数据复制到应用进程缓冲区。对这两个阶段不同的处理方式将网络IO分为不同的模型:IO阻塞模型、非阻塞模型、多路复用和异步IO。本文可运行代码链接:https://github.com/killianxu/network_example
一 阻塞模型
阻塞模型原理如下图1.1,当进行系统调用recvfrom时,应用进程进入内核态,内核判断是否已收到数据报,若没有则阻塞直到数据报准备好,接着复制数据到应用进程缓冲区,然后函数返回。
图1.1 阻塞IO模型
阻塞模型缺点:若数据报未准备好,则线程阻塞,不能进行其它操作和网络连接请求。
利用多进程多线程方案,为每个连接创建一个进程或线程,这样一个线程的阻塞不会影响到其它连接,但当遇到连接请求比较多时,会创建较多的进程或线程,严重浪费系统资源,影响进程的响应效率,进程和线程也更容易进入假死状态。
利用线程池或连接池,可以减少资源消耗。线程池利用已有线程,减少线程频繁创建和销毁,线程维持在一定数量,当有新的连接请求时,重用已有线程。连接池尽量重用已有连接,减少连接的创建和关闭。线程池和连接池一定程度上缓解频繁IO的资源消耗,但线程池和连接池都有一定规模,当连接请求数远超过池上线,池系统构成的响应并不比多线程方案好多少。[1]
阻塞模型python实例demo如下:
阻塞模型server端
def start_blocking(self): """同步阻塞server""" self.ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ssock.bind(('', 8080)) self.ssock.listen(5) count = 0 while True: conn, addr = self.ssock.accept() count += 1 print 'Connected by', addr print 'Accepted clinet count:%d' % count data = conn.recv(1024) #若无数据则阻塞 if data: conn.sendall(data) conn.close()
阻塞模型client
def start_blocking(self): self.host = '123.207.123.108' self.port = 8080 self.csock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.csock.connect((self.host, self.port)) data = self.csock.recv(1024) print data
运行server端,并运行两个client实例去连接服务端,运行结果如下图1.2,可以看到虽然有两个客户端去连接,但却只有一个连接上,服务端的socket conn为阻塞套接字,conn.recv(1024)未收到客户端发送的数据,处于阻塞状态,服务端无法再响应另一个客户端的连接。
图1.2 阻塞IO服务端运行结果
二 非阻塞模型
由于阻塞IO无法满足大规模请求的缺点,因此出现了非阻塞模型。非阻塞IO模型如下图1.3所示,当数据报未准备好,recvfrom立即返回一个EWOULDBLOCK错误,可以利用轮询不停调用recvfrom,当数据报准备好,内核则将数据复制到应用进程缓冲区。
图1.3 非阻塞IO模型
非阻塞IO模型需要利用轮询不断调用recvfrom,浪费大量CPU时间,且当内核接收到数据时,需要等到下一次轮询才能复制到应用进程缓冲区,数据得不到立刻处理。
非阻塞模型python demo如下:
非阻塞服务端
def start_noblocking(self): """ 同步非阻塞 """ self.ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ssock.bind(('', 8080)) self.ssock.listen(5) count = 0 while True: conn, addr = self.ssock.accept() conn.setblocking(0) #设置为非阻塞socket count += 1 print 'Connected by', addr print 'Accepted clinet count:%d' % count try: data = conn.recv(1024) #非阻塞,没有数据会立刻返回 if data: conn.sendall(data) except Exception as e: pass finally: conn.close()
运行非阻塞服务端和两个客户端实例,结果如下图1.4所示,服务端接收两个连接请求。由于conn被设置为非阻塞socket,即使客户端并没有向服务端发送数据,conn.recv(1024)也会立即返回,不会阻塞,从而进程可以接收新的连接请求。
图1.4 非阻塞服务端运行结果
三 IO复用
IO复用在linux中包括select、poll、epoll模型三种,这三个IO复用模型有各自的API实现,以select模型为例,调用select函数,进程