总结网络IO模型与select模型的Python实例讲解


Posted in Python onJune 27, 2016

网络I/O模型
人多了,就会有问题。web刚出现的时候,光顾的人很少。近年来网络应用规模逐渐扩大,应用的架构也需要随之改变。C10k的问题,让工程师们需要思考服务的性能与应用的并发能力。

网络应用需要处理的无非就是两大类问题,网络I/O,数据计算。相对于后者,网络I/O的延迟,给应用带来的性能瓶颈大于后者。网络I/O的模型大致有如下几种:

  • 同步模型(synchronous I/O)
  • 阻塞I/O(bloking I/O)
  • 非阻塞I/O(non-blocking I/O)
  • 多路复用I/O(multiplexing I/O)
  • 信号驱动式I/O(signal-driven I/O)
  • 异步I/O(asynchronous I/O)

网络I/O的本质是socket的读取,socket在linux系统被抽象为流,I/O可以理解为对流的操作。这个操作又分为两个阶段:

等待流数据准备(wating for the data to be ready)。
从内核向进程复制数据(copying the data from the kernel to the process)。
对于socket流而已,

第一步通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
第二步把数据从内核缓冲区复制到应用进程缓冲区。
I/O模型:
举个简单比喻,来了解这几种模型。网络IO好比钓鱼,等待鱼上钩就是网络中等待数据准备好的过程,鱼上钩了,把鱼拉上岸就是内核复制数据阶段。钓鱼的人就是一个应用进程。

阻塞I/O(bloking I/O)
阻塞I/O是最流行的I/O模型。它符合人们最常见的思考逻辑。阻塞就是进程 "被" 休息, CPU处理其它进程去了。在网络I/O的时候,进程发起recvform系统调用,然后进程就被阻塞了,什么也不干,直到数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据,在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络I/O。大致如下图:

总结网络IO模型与select模型的Python实例讲解

这就好比我们去钓鱼,抛竿之后就一直在岸边等,直到等待鱼上钩。然后再一次抛竿,等待下一条鱼上钩,等待的时候,什么事情也不做,大概会胡思乱想吧。

阻塞IO的特点就是在IO执行的两个阶段都被block了
非阻塞I/O(non-bloking I/O)
在网络I/O时候,非阻塞I/O也会进行recvform系统调用,检查数据是否准备好,与阻塞I/O不一样,"非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 '被' CPU光顾"。

也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

总结网络IO模型与select模型的Python实例讲解

我们再用钓鱼的方式来类别,当我们抛竿入水之后,就看下鱼漂是否有动静,如果没有鱼上钩,就去干点别的事情,比如再挖几条蚯蚓。然后不久又来看看鱼漂是否有鱼上钩。这样往返的检查又离开,直到鱼上钩,再进行处理。

非阻塞 IO的特点是用户进程需要不断的主动询问kernel数据是否准备好。
多路复用I/O(multiplexing I/O)
可以看出,由于非阻塞的调用,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间。结合前面两种模式。如果轮询不是进程的用户态,而是有人帮忙就好了。多路复用正好处理这样的问题。

多路复用有两个特别的系统调用select或poll。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于---前者可以等待多个socket,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。多路复用有两种阻塞,select或poll调用之后,会阻塞进程,与第一种阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也可以理解为"非阻塞"吧。

总结网络IO模型与select模型的Python实例讲解

对于多路复用,也就是轮询多个socket。钓鱼的时候,我们雇了一个帮手,他可以同时抛下多个钓鱼竿,任何一杆的鱼一上钩,他就会拉杆。他只负责帮我们钓鱼,并不会帮我们处理,所以我们还得在一帮等着,等他把收杆。我们再处理鱼。多路复用既然可以处理多个I/O,也就带来了新的问题,多个I/O之间的顺序变得不确定了,当然也可以针对不同的编号。

多路复用的特点是通过一种机制一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。
了解了前面三种模式,在用户进程进行系统调用的时候,他们在等待数据到来的时候,处理的方式不一样,直接等待,轮询,select或poll轮询,第一个过程有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。当时第二个过程都是阻塞的。从整个I/O过程来看,他们都是顺序执行的,因此可以归为同步模型(asynchronous)。都是进程主动向内核检查。

异步I/O(asynchronous I/O)
相对于同步I/O,异步I/O不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。I/O两个阶段,进程都是非阻塞的。

总结网络IO模型与select模型的Python实例讲解

比之前的钓鱼方式不一样,这一次我们雇了一个钓鱼高手。他不仅会钓鱼,还会在鱼上钩之后给我们发短信,通知我们鱼已经准备好了。我们只要委托他去抛竿,然后就能跑去干别的事情了,直到他的短信。我们再回来处理已经上岸的鱼。

同步和异步的区别
通过对上述几种模型的讨论,需要区分阻塞和非阻塞,同步和异步。他们其实是两组概念。区别前一组比较容易,后一种往往容易和前面混合。在我看来,所谓同步就是在整个I/O过程。尤其是拷贝数据的过程是阻塞进程的,并且都是应用进程态去检查内核态。而异步则是整个过程I/O过程用户进程都是非阻塞的,并且当拷贝数据的时是由内核发送通知给用户进程。

总结网络IO模型与select模型的Python实例讲解

对于同步模型,主要是第一阶段处理方法不一样。而异步模型,两个阶段都不一样。这里我们忽略了信号驱动模式。这几个名词还是容易让人迷惑,只有同步模型才考虑阻塞和非阻塞,因为异步肯定是非阻塞,异步非阻塞的说法感觉画蛇添足。

Select 模型
同步模型中,使用多路复用I/O可以提高服务器的性能。
在多路复用的模型中,比较常用的有select模型和poll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。select与poll的底层原理都差不多。千呼万唤始出来,本文的重点select模型。
1.select 原理
网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。

这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是线性的。

2.select 回显服务器
select涉及系统调用和操作系统相关的知识,因此单从字面上理解其原理还是比较乏味。用代码来演示最好不过了。使用python的select模块很容易写出下面一个回显服务器:

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信息。

下面详细解析上述代码的原理。

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(5)

上述代码使用socket初始化一个TCP套接字,并绑定主机地址和端口,然后设置服务器监听。

inputs = [server, sys.stdin]

这里定义了一个需要select监听的列表,列表里面是需要监听的对象(等于系统监听的文件描述符)。这里监听socket套接字和用户的输入。

然后代码进行一个服务器无线循环。

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有一个套接字对象(文件描述符可读)。

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循环。如果没有新的套接字,将会执行下面的代码:

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)

通过套接字连接调用recv函数,获取客户端发送的数据,当数据传输完毕,再把监视的inputs列表中除去该连接。然后关闭连接。

整个网络交互过程就是如此,当然这里如果用户在命令行中输入中断,inputs列表中监视的sys.stdin也会让select返回,最后也会执行下面的代码:

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。

3.select的不足
尽管select用起来挺爽,跨平台的特性。但是select还是存在一些问题。
select需要遍历监视的文件描述符,并且这个描述符的数组还有最大的限制。随着文件描述符数量的增长,用户态和内核的地址空间的复制所引发的开销也会线性增长。即使监视的文件描述符长时间不活跃了,select还是会线性扫描。

为了解决这些问题,操作系统又提供了poll方案,但是poll的模型和select大致相当,只是改变了一些限制。目前Linux最先进的方式是epoll模型。

许多高性能的软件如nginx, nodejs都是基于epoll进行的异步。

Python 相关文章推荐
python中函数默认值使用注意点详解
Jun 01 Python
python爬虫系列Selenium定向爬取虎扑篮球图片详解
Nov 15 Python
Python实现OpenCV的安装与使用示例
Mar 30 Python
详谈python3中用for循环删除列表中元素的坑
Apr 19 Python
python中while和for的区别总结
Jun 28 Python
python 字符串追加实例
Jul 20 Python
详解用Python为直方图绘制拟合曲线的两种方法
Aug 21 Python
浅谈django 重载str 方法
May 19 Python
Python collections.deque双边队列原理详解
Oct 05 Python
python爬虫用scrapy获取影片的实例分析
Nov 23 Python
利用For循环遍历Python字典的三种方法实例
Mar 25 Python
python playwright之元素定位示例详解
Jul 23 Python
结合Python的SimpleHTTPServer源码来解析socket通信
Jun 27 #Python
Python的Tornado框架的异步任务与AsyncHTTPClient
Jun 27 #Python
深入解析Python中的descriptor描述器的作用及用法
Jun 27 #Python
Python中的字符串查找操作方法总结
Jun 27 #Python
解析Python中的__getitem__专有方法
Jun 27 #Python
详解Python中的__getitem__方法与slice对象的切片操作
Jun 27 #Python
Python使用smtplib模块发送电子邮件的流程详解
Jun 27 #Python
You might like
社区(php&&mysql)三
2006/10/09 PHP
PHP生成不同颜色、不同大小的tag标签函数
2013/09/23 PHP
PHP里的单例类写法实例
2015/06/25 PHP
PHP通过引用传递参数用法分析
2016/12/01 PHP
php判断文件上传图片格式的实例详解
2017/09/30 PHP
laravel 自定义常量的两种方案
2019/10/14 PHP
用 Javascript 验证表单(form)中多选框(checkbox)值
2009/09/08 Javascript
jQuery AJAX 调用WebService实现代码
2010/03/24 Javascript
js function定义函数使用心得
2010/04/15 Javascript
jquery中获取元素的几种方式小结
2011/07/05 Javascript
Jjcarousellite 实现图片列表滚动的简单实例
2013/11/29 Javascript
禁止拷贝网页内容的js代码
2014/01/22 Javascript
jquery中交替点击事件的实现代码
2014/02/14 Javascript
使用正则表达式的格式化与高亮显示json字符串
2014/12/03 Javascript
js用拖动滑块来控制图片大小的方法
2015/02/27 Javascript
jQuery中常用的遍历函数用法实例总结
2015/09/01 Javascript
学习JavaScript设计模式(链式调用)
2015/11/26 Javascript
纯JS实现弹性导航条效果
2017/03/06 Javascript
vue+element-ui+ajax实现一个表格的实例
2018/03/09 Javascript
vue 指定组件缓存实例详解
2018/04/01 Javascript
原生js实现获取form表单数据代码实例
2019/03/27 Javascript
Vue 使用计时器实现跑马灯效果的实例代码
2019/07/11 Javascript
Nodejs实现微信分账的示例代码
2021/01/19 NodeJs
[03:08]TI9战队档案 - Vici Gaming
2019/08/20 DOTA
Python的Twisted框架上手前所必须了解的异步编程思想
2016/05/25 Python
python xlsxwriter库生成图表的应用示例
2018/03/16 Python
Square Off美国/加拿大:世界上最聪明的国际象棋棋盘
2018/12/06 全球购物
adidas泰国官网:adidas TH
2020/07/11 全球购物
什么是聚集索引和非聚集索引
2012/01/17 面试题
东方红海科技面试题软件测试方面
2012/02/08 面试题
大学学年自我鉴定
2013/10/28 职场文书
中学实习教师自我鉴定
2013/12/12 职场文书
学校端午节活动方案
2014/08/23 职场文书
浅谈python数据类型及其操作
2021/05/25 Python
i5-10400f处理相当于i7多少水平
2022/04/19 数码科技
Win10 Anaconda安装python-pcl
2022/04/29 Servers