python开发之路之I/O多路复用

前言

  1.什么是I/O多路复用?

    

  我们都知道,在同一时刻,我们的服务器端只能处理同1个客户端,即客户端和服务器端处于一对一的模式。即服务器端和客户端在进行请求、响应都是1对1的进行。

  但是,现在的需求是:我们要让多个客户端连接至服务器端,而且服务器端需要处理来自多个客户端请求,这样的话,传统的模式就实现不了了,此时我们该采用什么方式来处理呢?

  解决方法:

     采用I/O多路复用机制。在python网络编程中,I/O多路复用机制就是用来解决多个客户端连接请求服务器端,而服务器端能正常处理并响应给客户端的一种机制。

     书面上来说,就是通过1种机制:可以监听多个文件描述符,一旦描述符就绪,能够通知程序进行相应的读写操作。

  2.什么是文件描述符?

    在网络中,一个socket对象就是1个文件描述符,在文件中,1个文件句柄(即file对象)就是1个文件描述符。其实可以理解为就是一个“指针”或“句柄”,指向1个socket或file对象,当file或socket发生改变时,这个对象对应的文件描述符,也会发生相应改变。

  3.在python中I/O操作包括哪些?

  • 网络操作,即建立socket对象,进行建立连接,发送、接收、处理请求、响应等
  • 文件操作,即建立file对象,进行文件的读、写操作
  • 终端操作。即进行交互式输入输出等操作

  4.I/O多路复用

   在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。

   I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求

   与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。

  5.I/O多路复用的主要应用场景如下:

  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
  • 服务器需要同时处理多种网络协议的套接字

一、python的select模块

  在Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll ,从而实现IO多路复用。

  在Linux中,I/O多路复用具体介绍如下所示:

 

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()时便得到通知。

  select方法,用来监视文件句柄,如果句柄发生变化,则获取该句柄

    select(rlist, wlist, xlist[, timeout]) -> (rlist, wlist, xlist)

    句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)

    参数: 可接受四个参数(前三个必须),rlist, wlist, xlist[, timeout]
    返回值:三个列表:rlist, wlist, xlist

    1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中
    2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中
    3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中
    4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
    当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。

  示例1,linux下监视终端操作的实例,即把输入的东西读出来。(在linux环境下使用该示例)

  • 其实我们输入的东西,在linux下是放在sys模块下的stdin管道里面的,系统要使用的时候是在stdin里面读、取的。
  • 读取输入管道里面的东西,采用:sys.stdin.readline()
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import select
import threading
import sys

while True:  #监听select的第一个参数:sys.stdin,这里表示监听用户输入,我们的sys.stdin相当于终端描述符,相当于文件的句柄。  #如果监听到有更改,即用户有输入,select就会感知到,然后就把变化的文件句柄保存至列表,并将列表返回给第一个参数:readable  #如果没有改变,即用户没有输入,select就会感知到没有改变,readable列表就是空的列表[]
    readable,writeable,error = select.select([sys.stdin,],[],[],1)
    if sys.stdin in readable:
        print "select get stdin",sys.stdin.readline()

  示例2:监听多个I/O(这里指socket对象),用浏览器访问,并打印访问的客户端地址

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import select

sk = socket.socket()
ip_port = (‘127.0.0.1‘,8888)
sk.bind(ip_port)
sk.listen(5)
#设置不阻塞
sk.setblocking(False)

sk1 = socket.socket()
ip_port1 = (‘127.0.0.1‘,9999)
sk1.bind(ip_port1)
sk1.listen(5)
#设置不阻塞
sk1.setblocking(False)

inputs = [sk,sk1,]

while True:
    rList,w,e = select.select(inputs,[],[],2)
    for r in rList:
        conn,addr = r.accept()
        print addr
sk.close()

  

在浏览器中打开2个窗口,分别输入127.0.0.1:8888和127.0.0.1:8888,访问结果显示如下:

时间: 2024-07-30 12:54:18

python开发之路之I/O多路复用的相关文章

python开发之路之线程、进程、协程

一.多进程和多线程 共同点: 让多个CPU同时处理请求 区别: 1.多线程中的线程在内存空间这一点上是共享的,进程与进程使用的是不同的内存空间.即创建线程不需要开辟内存空间,而创建新的进程需要为其分配新的内存空间 全局解释器锁(GIL) 在每一个进程的“出口”,是python特有的.它的作用是:做到了1个限制,什么限制呢,如果有2个线程同时被调度了,此时全局解释器锁就限制同时只能有1个穿过全局解释器锁,才能被CPU调度 那什么时候该使用多进程,什么时候该使用多线程呢? I/O密集型用多线程 计算

python开发之路-LuffyCity

阅读目录 一.python基础语法 二.python基础之字符编码 三.python基础之文件操作 四.python基础小练习 五.python之函数基础 六.python之函数对象.函数嵌套.名称空间与作用域.装饰器 七.python之迭代器.生成器.面向过程编程 八.python之三元表达式.列表推导式.生成器表达式.递归.匿名函数.内置函数 python内置函数分类 九.python之初识模块 十.python之常用模块 十一.python之面相对象 十二.python之面向对象高级 十三

python开发之路---第二章之--基本函数介绍

1.函数: 是指一组语句的集合,通过一个名字(函数名)封装起来,要想执行这个函数,只需要调用函数名即可 优点: # 减少重复代码 # 使程序可扩展 # 使程序易维 栗子1: 1 def sayhi(): 2 print ('hello') 3 4 sayhi() ## sayhi 是指向函数的内存地址,加了() 就是执行这个函数 # 如下又是一种,这个是可以给函数插入参数,参数可以根据调用时传入的参数的不同,来决定内部执行代码的流程 eg:(1) 1 def sayhi(name): 2 pri

python开发之路---第二章之--函数的作用域

## Python中一个函数就是一个作用域 age = 18 def func1(): age = 73 def func2(): print(age) return func2 val = func1() val() # 代码定义完成后,作用域就生成了,作用域向上查找 原文地址:https://www.cnblogs.com/LoveElsa/p/10276820.html

python 开发之路(2)

阅读目录 第一篇:python入门 第二篇:数据类型.字符编码.文件处理 第三篇:函数 第四篇:模块与包 第五篇:常用模块 第六篇:面向对象 第七篇:面向对象高级 第八篇:异常处理 第九篇:网络编程 第十篇:并发编程 第十一篇:Mysql系列........ 原文地址:https://www.cnblogs.com/wangyh702/p/12306320.html

python开发之路-day02

数据类型 1 什么是数据? x=10,10是我们要存储的数据 2 为何数据要分不同的类型 数据是用来表示状态的,不同的状态就应该用不同的类型的数据去表示 3 数据类型 数字(整形,长整形,浮点型,复数) 字符串 字节串:在介绍字符编码时介绍字节bytes类型 列表 元组 字典 集合 4 按照以下几个点展开数据类型的学习 #一:基本使用 1 用途 2 定义方式 3 常用操作+内置的方法 #二:该类型总结 1 存一个值or存多个值 只能存一个值 可以存多个值,值都可以是什么类型 2 有序or无序 3

python开发之路---第二章之--文件操作

'''1.r ## 是只读模式2.w ## 创建新文件并覆盖写3.rb ## 二进制方式读4.wb ## 二进制方式写5.ab ## 追加写6.w+ ## 写读模式,支持写完后读取写的内容7.r+ ## 读写模式,读完之后,再写(追加)8. ''' '''f.tell() # 返回当前文件读取到的光标字节的位置f.seek(5) # 强制把光标调整到指定字节位置,如果从这个位置写,后面有内容的话,会覆盖写f.flush() # 强制把内容从内存刷到硬盘中f.readline() ## 每次读一行

python开发之路---第二章之--嵌套函数

1 def func1(): 2 print('alex') 3 4 def func2(): 5 print('eric') # 1. func1() # 加了() 就代表执行这个函数, 执行结果是'alex' 1 ef func13(): 2 print('alex') 3 4 def func23(): 5 print('eric') 6 7 func23() # 1.func1() # 这里执行结果就是2个都答应了,alex和eric # 总结# 1.函数内部可以再次定义函数 # 2.函

python开发之路---第二章之--函数之高阶函数

## 一个函数的变量为另一个函数,这种函数就称为高阶函数 ## return 一个函数名也可以称为高阶函数 def func(x,y): return x+y def cale(x): return x f = cale(func) a = f(4,5) print (a) ## 这里的实际上执行的是func的函数,所以不传参数的话会报错,必须传入2个参数, 原文地址:https://www.cnblogs.com/LoveElsa/p/10280029.html