Python-select详解

I/O多路复用是在单线程模式下实现多线程的效果,实现一个多I/O并发的效果。看一个简单socket例子:

import socket  

SOCKET_FAMILY = socket.AF_INET
SOCKET_TYPE = socket.SOCK_STREAM  

sockServer = socket.socket()
sockServer.bind((‘0.0.0.0‘, 8888))
sockServer.listen(5)  

while True:
    cliobj, addr = sockServer.accept()
    while True:
        recvdata = cliobj.recv(1024)
        if recvdata:
            print(recvdata.decode())
        else:
            cliobj.close()
            break  

客户端:

import socket  

socCli = socket.socket()
socCli.connect((‘127.0.0.1‘, 8888))
while True:
    data = input("input str:")
    socCli.send(data.encode())  

以上为一个简单的客户端发送一个输入信息给服务端的socket通信的实例,在以上的例子中,服务端是一个单线程、阻塞模式的。如何实现多客户端连接呢,我们可以使用多线程模式,这个当然没有问题。 使用多线程、阻塞socket来处理的话,代码会很直观,但是也会有不少缺陷。它很难确保线程共享资源没有问题。而且这种编程风格的程序在只有一个CPU的电脑上面效率更低。但如果一个用户开启的线程有限的情况下,比如1024个。当第1025个客户端连接是仍然会阻塞。
有没有一种比较好的方式呢,当然有,其一是使用异步socket。

这种socket只有在一些event触发时才会阻塞。相反,程序在异步socket上面执行一个动作,会立即被告知这个动作是否成功。程序会根据这个信 息决定怎么继续下面的操作由于异步socket是非阻塞的,就没有必要再来使用多线程。所有的工作都可以在一个线程中完成。这种单线程模式有它自己的挑 战,但可以成为很多方案不错的选择。它也可以结合多线程一起使用:单线程使用异步socket用于处理服务器的网络部分,多线程可以用来访问其他阻塞资 源,比如数据库。Linux的2.6内核有一系列机制来管理异 步socket,其中3个有对应的Python的API:select、poll和epoll。epoll和pool比select更好,因为 Python程序不需要检查每一个socket感兴趣的event。相反,它可以依赖操作系统来告诉它哪些socket可能有这些event。epoll 比pool更好,因为它不要求操作系统每次都去检查python程序需要的所有socket感兴趣的event。而是Linux在event发生的时候会 跟踪到,并在Python需要的时候返回一个列表。因此epoll对于大量(成千上万)并发socket连接,是更有效率和可扩展的机制
异步I/O处理模型

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

1.优缺点
  select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
    1).单个进程可监视的fd数量被限制
    2).需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
    3).对socket进行扫描时是线性扫描

  poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

  它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

  poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

  epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。在前面说到的复制问题上,epoll使用mmap减少复制开销。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

2.支持一个进程所能打开的最大连接数
  select 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
  poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的
  epoll 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

3 FD剧增后带来的IO效率问题
  select 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
  poll 同上
  epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

4 消息传递方式
  select 内核需要将消息传递到用户空间,都需要内核拷贝动作。
  poll 同上
  epoll epoll通过内核和用户空间共享一块内存来实现的。

下面我们对上面的socket例子进行改造,看一下select的例子:
elect 详细解释,用线程的IO多路复用实现一个读写分离的、支持多客户端的连接请求

import socket
import queue
from select import select  

SERVER_IP = (‘127.0.0.1‘, 9999)  

# 保存客户端发送过来的消息,将消息放入队列中
message_queue = {}
input_list = []
output_list = []  

if __name__ == "__main__":
    server = socket.socket()
    server.bind(SERVER_IP)
    server.listen(10)
    # 设置为非阻塞
    server.setblocking(False)  

    # 初始化将服务端加入监听列表
    input_list.append(server)  

    while True:
        # 开始 select 监听,对input_list中的服务端server进行监听
        stdinput, stdoutput, stderr = select(input_list, output_list, input_list)  

        # 循环判断是否有客户端连接进来,当有客户端连接进来时select将触发
        for obj in stdinput:
            # 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时,说明有新客户端连接进来了
            if obj == server:
                # 接收客户端的连接, 获取客户端对象和客户端地址信息
                conn, addr = server.accept()
                print("Client {0} connected! ".format(addr))
                # 将客户端对象也加入到监听的列表中, 当客户端发送消息时 select 将触发
                input_list.append(conn)
                # 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
                message_queue[conn] = queue.Queue()  

            else:
                # 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list),客户端发送消息将触发
                # 所以判断是否是客户端对象触发
                try:
                    recv_data = obj.recv(1024)
                    # 客户端未断开
                    if recv_data:
                        print("received {0} from client {1}".format(recv_data.decode(), addr))
                        # 将收到的消息放入到各客户端的消息队列中
                        message_queue[obj].put(recv_data)  

                        # 将回复操作放到output列表中,让select监听
                        if obj not in output_list:
                            output_list.append(obj)  

                except ConnectionResetError:
                    # 客户端断开连接了,将客户端的监听从input列表中移除
                    input_list.remove(obj)
                    # 移除客户端对象的消息队列
                    del message_queue[obj]
                    print("\n[input] Client  {0} disconnected".format(addr))  

        # 如果现在没有客户端请求,也没有客户端发送消息时,开始对发送消息列表进行处理,是否需要发送消息
        for sendobj in output_list:
            try:
                # 如果消息队列中有消息,从消息队列中获取要发送的消息
                if not message_queue[sendobj].empty():
                    # 从该客户端对象的消息队列中获取要发送的消息
                    send_data = message_queue[sendobj].get()
                    sendobj.sendall(send_data)
                else:
                    # 将监听移除等待下一次客户端发送消息
                    output_list.remove(sendobj)  

            except ConnectionResetError:
                # 客户端连接断开了
                del message_queue[sendobj]
                output_list.remove(sendobj)
                print("\n[output] Client  {0} disconnected".format(addr))  

epoll实现实例

#!/usr/bin/env python
import select
import socket  

response = b‘‘  

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((‘0.0.0.0‘, 8080))
serversocket.listen(1)
# 因为socket默认是阻塞的,所以需要使用非阻塞(异步)模式。
serversocket.setblocking(0)  

# 创建一个epoll对象
epoll = select.epoll()
# 在服务端socket上面注册对读event的关注。一个读event随时会触发服务端socket去接收一个socket连接
epoll.register(serversocket.fileno(), select.EPOLLIN)  

try:
    # 字典connections映射文件描述符(整数)到其相应的网络连接对象
    connections = {}
    requests = {}
    responses = {}
    while True:
        # 查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,我们会等待1秒来看是否有event发生。
        # 如果有任何我们感兴趣的event发生在这次查询之前,这个查询就会带着这些event的列表立即返回
        events = epoll.poll(1)
        # event作为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。
        for fileno, event in events:
            # 如果是服务端产生event,表示有一个新的连接进来
            if fileno == serversocket.fileno():
                connection, address = serversocket.accept()
                print(‘client connected:‘, address)
                # 设置新的socket为非阻塞模式
                connection.setblocking(0)
                # 为新的socket注册对读(EPOLLIN)event的关注
                epoll.register(connection.fileno(), select.EPOLLIN)
                connections[connection.fileno()] = connection
                # 初始化接收的数据
                requests[connection.fileno()] = b‘‘  

            # 如果发生一个读event,就读取从客户端发送过来的新数据
            elif event & select.EPOLLIN:
                print("------recvdata---------")
                # 接收客户端发送过来的数据
                requests[fileno] += connections[fileno].recv(1024)
                # 如果客户端退出,关闭客户端连接,取消所有的读和写监听
                if not requests[fileno]:
                    connections[fileno].close()
                    # 删除connections字典中的监听对象
                    del connections[fileno]
                    # 删除接收数据字典对应的句柄对象
                    del requests[connections[fileno]]
                    print(connections, requests)
                    epoll.modify(fileno, 0)
                else:
                    # 一旦完成请求已收到,就注销对读event的关注,注册对写(EPOLLOUT)event的关注。写event发生的时候,会回复数据给客户端
                    epoll.modify(fileno, select.EPOLLOUT)
                    # 打印完整的请求,证明虽然与客户端的通信是交错进行的,但数据可以作为一个整体来组装和处理
                    print(‘-‘ * 40 + ‘\n‘ + requests[fileno].decode())  

            # 如果一个写event在一个客户端socket上面发生,它会接受新的数据以便发送到客户端
            elif event & select.EPOLLOUT:
                print("-------send data---------")
                # 每次发送一部分响应数据,直到完整的响应数据都已经发送给操作系统等待传输给客户端
                byteswritten = connections[fileno].send(requests[fileno])
                requests[fileno] = requests[fileno][byteswritten:]
                if len(requests[fileno]) == 0:
                    # 一旦完整的响应数据发送完成,就不再关注写event
                    epoll.modify(fileno, select.EPOLLIN)  

            # HUP(挂起)event表明客户端socket已经断开(即关闭),所以服务端也需要关闭。
            # 没有必要注册对HUP event的关注。在socket上面,它们总是会被epoll对象注册
            elif event & select.EPOLLHUP:
                print("end hup------")
                # 注销对此socket连接的关注
                epoll.unregister(fileno)
                # 关闭socket连接
                connections[fileno].close()
                del connections[fileno]
finally:
    # 打开的socket连接不需要关闭,因为Python会在程序结束的时候关闭。这里显式关闭是一个好的代码习惯
    epoll.unregister(serversocket.fileno())
    epoll.close()
    serversocket.close()  

转自:http://blog.csdn.net/songfreeman/article/details/51179213

时间: 2024-09-30 10:04:14

Python-select详解的相关文章

python正则表达式详解

python正则表达式详解 正则表达式是一个很强大的字符串处理工具,几乎任何关于字符串的操作都可以使用正则表达式来完成,作为一个爬虫工作者,每天和字符串打交道,正则表达式更是不可或缺的技能,正则表达式的在不同的语言中使用方式可能不一样,不过只要学会了任意一门语言的正则表达式用法,其他语言中大部分也只是换了个函数的名称而已,本质都是一样的.下面,我来介绍一下python中的正则表达式是怎么使用的. 首先,python中的正则表达式大致分为以下几部分: 元字符 模式 函数 re 内置对象用法 分组用

python线程详解

#线程状态 #线程同步(锁)#多线程的优势在于可以同时运行多个任务,至少感觉起来是这样,但是当线程需要共享数据时,可能存在数据不同步的问题. #threading模块#常用方法:'''threading.currentThread():返回当前的线程变量threading.enumerate():返回一个包含正在运行的线程的list,正在运行指:线程启动后,结束前,不包含启动前和终止后的线程threading.activeCount():返回正在运行的线程数量,与len(threading.en

python difflib详解

difflib -帮助进行差异化比较 这个模块提供的类和方法用来进行差异化比较,它能够生成文本或者html格式的差异化比较结果,如果需要比较目录的不同,可以使用filecmp模块. class difflib.SequenceMatcher 这是可以用来比较任何类型片段的类,只要比较的片段是可hash的,都可以用来比较,使用非常灵活.他源于1980,s的“完形匹配算法”,并且进行了一系列的优化和改进. 通过对算法的复杂度比较,它由于原始的完形匹配算法,在最坏情况下有n的平方次运算,在最好情况下,

转 python数据类型详解

python数据类型详解 目录 1.字符串 2.布尔类型 3.整数 4.浮点数 5.数字 6.列表 7.元组 8.字典 9.日期 1.字符串 1.1.如何在Python中使用字符串 a.使用单引号(') 用单引号括起来表示字符串,例如: str='this is string'; print str; b.使用双引号(") 双引号中的字符串与单引号中的字符串用法完全相同,例如: str="this is string"; print str; c.使用三引号(''') 利用三

Python列表详解

Python列表详解: 创建一个列表,只要把逗号分隔的不同数据项使用方括号括起来即可. 比如:    list = [1, 2, 3, 4, 5 ]; 与字符串的索引一样,列表索引从0开始. Python列表函数即方法: Python所包含的函数: 1.cmp() 描述: cmp()用于比较两个列表的元素. 语法: cmp (list1,list2) 返回值: 如果比较的元素是同类型的,则比较其值,返回结果. 如果两个元素不是同一种类型,则检查它们是否是数字. 如果是数字,执行必要的数字强制类型

Python 递归函数 详解

Python 递归函数 详解   在函数内调用当前函数本身的函数就是递归函数   下面是一个递归函数的实例: 第一次接触递归函数的人,都会被它调用本身而搞得晕头转向,而且看上面的函数调用,得到的结果会是: 为什么会得出上面的结果呢?因为都把调用函数本身之后的代码给忘记了,就是else之后的python 代码. 实际此递归函数输出的是以下结果: 相信大家看到这里都有点蒙,小编也一样,我第一次看到这个递归函数时,只能理解到第一个结果.那是因为,大部分人在做事情的时候,中断第一件事,被安排去做第二件事

Python数据类型详解——列表

Python数据类型详解--列表 在"Python之基本数据类型概览"一节中,大概介绍了列表的基本用法,本节我们详细学一下列表. 如何定义列表:在[]内以英文里输入法的逗号,,按照索引,存放各种数据类型,每个位置代表一个元素. 回顾一下列表的特点: 1.可存放多个值. 2.按照从左到右的顺序定义列表元素,下标从0开始顺序访问,是有序的. 3.可修改指定索引位置对应的值,可变. 一.列表元素的增加操作 1.追加 用append方法将数据追加到列表的尾部 names = ['Kwan',

ibatis的result标签中用select详解

标签:详解 ibatis的result标签中用select 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://zorro.blog.51cto.com/2139862/821777 <resultMap id="usermoduleprivilege"     class="com.webex.webapp.l10n.common.pojo.UserModulePrivilege">  

python面向对象详解(上)

创建类 Python 类使用 class 关键字来创建.简单的类的声明可以是关键字后紧跟类名: class ClassName(bases): 'class documentation string' #'类文档字符串' class_suite #类体 实例化 通过类名后跟一对圆括号实例化一个类 mc = MyClass() # instantiate class 初始化类 'int()'构造器 def __int__(self): pass 注意:self类似Java的this关键字作用,它代

python字典详解

字典是Python中唯一的內建的映射类型,可以存储任意对象的容器,比如:字符串,列表,元组,自定义对象等:字典由键(key)与值(value)组成,基本语法如下: {key:value, ... ...}字典中每个Key是唯一的,key必须是可哈希(后面我们介绍): 这节我们来看下字典基本知识点: 1>字典定义.遍历.修改:2>理解字典key:4>字典相关函数: 1.字典定义与访问 1.1 直接定义字典: stocks = {'000001':'平安银行', '000002':'万科A'