day10-select IO多路复用代码实例

概述

Python的select()方法直接调用操作系统的IO接口,它监控sockets,open files, and pipes(所有带fileno()方法的文件句柄)何时变成readable 和writeable, 或者通信错误,select()使得同时监控多个连接变的简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过Python的解释器。

注意:Using Python’s file objects with select() works for Unix, but is not supported under Windows.

接下来通过echo server例子要以了解select 是如何通过单进程实现同时处理多个非阻塞的socket连接的

代码实现

非阻塞

说明:在设置为非阻塞的条件下,accept和recive都不阻塞了,没有值就报错。也就是说没有连接就不要让它走server.accept()这一步。让select帮助去检测这个100个连接。所以说只要有连接,有活动了,有数据(新连接进来)才accept。

import select
import socket
import sys
import queue

#创建一个socket服务器对象
server = socket.socket()
server.setblocking(False)  #设置不阻塞模式
#绑定端口到socket
server.bind(('localhost', 8003))
#监听传入连接
server.listen(1000)

inputs = [server,] #将自己加入列表(server本身也是一个连接)
outputs= []

readable,writeable,exceptional = select.select(inputs,outputs,inputs)  #1select函数阻塞进程,直到inputs中的套接字被触发(在此例中,套接字接收到客户端发来的握手信号,从而变得可读,满足select函数的“可读”条件),readable返回被触发的socket(服务器socket)#3 select再次阻塞进程,同时监听服务器套接字和获得的客户端套接字
print(readable,writeable,exceptional)
for r in readable:   #2 如果是服务器套接字被触发(监听到有客户端连接服务器)
    conn,addr = server.accept()
    print(conn,addr)
    data = conn.recv(1024)
    print("recv:",data)

#运行输出

[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8003)>] [] []
Traceback (most recent call last):
  File "/Users/huwei/PycharmProjects/s14/module_4/select_socket_server.py", line 22, in <module>
<socket.socket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8003), raddr=('127.0.0.1', 61376)> ('127.0.0.1', 61376)
    data = conn.recv(1024)
BlockingIOError: [Errno 35] Resource temporarily unavailable

Process finished with exit code 1

程序运行解析:我们通过单线程下的I/O多路复用实现非阻塞的socket连接,这里的inputs为空,首先需要加入server本身,也就是说如果有客户端连接我了,就代表我活动了,所以在这里一开始是监测本身server,当加入server到inputs后,就程序就被阻塞了,这时候,有客户端的连接连入进来才不会阻塞,同时当连接进来后select会返回3个数据(readable,writeable,exceptional),而这里inputs并没有来自客户端的连接,而服务器端这时候又在接收数据,因为是非阻塞模式,所以服务器端直接返回一个错误,出现阻塞I/O错误。

select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息,接下来我们需要创建2个列表来包含输入和输出信息来传给select().

因此,我们需要实现在客户端发送数据的时候,内核通知服务器端接收客户端发来的数据,那么就需要将客户端的连接append到inputs监测列表中,下一次select循环时,客户端发送数据到服务器端,通过服务器端判断这时候有新的客户端连接进来了,服务器端就可以接收数据了。

import select
import socket
import sys
import queue

#创建一个socket服务器对象
server = socket.socket()
server.setblocking(False)  #设置不阻塞模式
#绑定端口到socket
server.bind(('localhost', 8002))
#监听传入连接
server.listen(1000)

inputs = [server,] #将自己加入列表(本身也是一个连接)
outputs= []
#inputs = [server,conn] #[conn,] 第一个客户端连接加入到服务器端
#inputs = [server,conn.conn2] #[conn2,] 第二个客户端连接加入到服务器端
while True:
    readable,writeable,exceptional = select.select(inputs,outputs,inputs)  #1,#3 有连接进来会返回三个数据
    print(readable,writeable,exceptional)
    for r in readable:   
        if r is server: #2 如果是服务器套接字被触发(监听到有客户端连接服务器),代表来了一个新连接
            conn,addr = server.accept()
            print("来了个新连接:",addr)
            inputs.append(conn) #是因为这个新建立的连接还没有发送数据过来,现在就接收的话,程序就报错,所以要想实现这个客户端发送数据来时,server端能够知道,就需要让select再监测这个conn
        else: #可能是第二个客户端的conn2
            data = conn.recv(1024)
            print("recv:",data)
            conn.send(data.upper())

运行结果

程序运行解析:在启动服务器端后,启动客户端1并send数据,再启动客户端2并send数据,然后再切换到客户端1后send数据,此时服务器端出现报错,没有接收到客户端1send的数据,这是为什么呢?因为第一次连接是server本身,然后第二次client 1连接到服务器端后,该连接被append到inputs列表中,服务器端程序执行通过conn接收和发送数据,这时候又有客户端连接进来了,加入到inputs列表中,此时活动的连接可能就是conn2,但是程序使用的还是第二次client 1的conn接收和发送数据,而不是第三次的client 2的conn2接收和发送数据,所以出现了报错。

解决方案:将接收和发送数据的实例由conn改为r即可。

       data = r.recv(1024)
       r.send(data.upper())

到这里程序就实现了服务器端和客户端的收发数据,但是我们想实现在服务器端接收到客户端的数据后,不立刻发回给客户端,而是先将它缓存到一个queue中,然后再由select取出来再发出去,接下来我们就对代码进行优化:

import select
import socket
import sys
import queue

# 创建一个socket服务器对象
server = socket.socket()
server.setblocking(False)  # 设置不阻塞模式
# 绑定端口到socket
server.bind(('localhost', 8013))
# 监听传入连接
server.listen(1000)

inputs = [server, ]  # 将自己加入列表(本身也是一个连接)
outputs = []
# inputs = [server,conn] #[conn,]
msg_dict = {}
while True:
    readable, writeable, exceptional = select.select(inputs, outputs, inputs)  # 1,#3 有连接进来会返回三个数据
    print("next:",readable, writeable, exceptional)
    for r in readable:  # 2 如果是服务器套接字被触发(监听到有客户端连接服务器)
        if r is server:
            conn, addr = server.accept()
            print("来了个新连接:", addr)
            inputs.append(conn)  # 是因为这个新建立的连接还没有发送数据过来,现在就接收的话,程序就报错,所以要想实现这个客户端发送数据来时,server端能够知道,就需要让select再监测这个conn
            msg_dict[conn] = queue.Queue() #初始化一个队列,后面存 要返回给这个客户端的数据
        else:
            data = r.recv(1024)
            print("recv:", data)
            if data:
                msg_dict[r].put(data)
                outputs.append(r) #放入返回的连接队列里
            else:
                print("客户端已经断开了",r)
                if r in outputs:
                    outputs.remove(r)
                inputs.remove(r)
                del msg_dict[r]

    for w in writeable: #要返回给客户端的连接列表
        data_to_client = msg_dict[w].get()
        w.send(data_to_client) #返回给客户端源数据

        outputs.remove(w) #确保下次循环的时候writeable,不返回这个已经处理完的连接

    for e in exceptional:
        if e in outputs:
            outputs.remove(e)

        inputs.remove(e)

        del msg_dict[e]

server.close()

程序详解

inputs = [server, ]  

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

然后进行一个无限循环:

while True:
    #调用select函数,阻塞等待
    readable, writeable, exceptional = select.select(inputs, outputs, inputs) 

#调用了select函数,开始循环遍历监听传入的列表inputs。如果没有连接服务器,此时没有建立tcp客户端连接,因此改列表内的对象都是数据资源不可用。因此select阻塞不返回

在启动客户端1后,1个socket连接通信开始,此时inputs中的第一个对象server由不可用变成可用。因此select函数调用返回,此时readable有一个socket对象(文件描述符可读)。

    for r in readable: 
        # 建立连接
        if r is server:
            conn, addr = server.accept()
            print("来了个新连接:", addr)
            # select监听的socket
            inputs.append(conn)

#select返回之后,接下来遍历可读的文件对象,此时的可读中只有一个socket连接(r),调用socket的accept()方法建立TCP三次握手的连接,然后把该连接对象append到inputs监视列表中,表示我们要监视该连接是否有数据IO操作。由于此时readable只有一个可用的对象,因此遍历结束。再回到主循环,再次调用select,此时调用的时候,不仅会遍历监视是否有新的连接需要建立,还要监视刚才追加的客户端的连接。如果该客户端连接的数据到了,select再返回到readable,此时再进行for循环。

如果没有新的socket连接,将会执行下面的代码:

        else:
            #读取客户端连接发送的数据
            data = r.recv(1024)
            print("recv:", data)
            if data:
                #把从客户端send来的数据放入queue中
                msg_dict[r].put(data)
                #同时把该客户端的socket连接加入到output中
                outputs.append(r) #放入返回的连接队列里
            else: #没有客户端没有发送数据?
                print("客户端已经断开了",r)
                if r in outputs:
                    outputs.remove(r) #
                inputs.remove(r)
                del msg_dict[r]

    #如果客户端连接在跟它对应的queue里有数据,就把这个数据取出来再发回给这个客户端
    for w in writeable:
        data_to_client = msg_dict[w].get()
        w.send(data_to_client)

        outputs.remove(w)

    #如果在跟某个socket连接通信过程中出了错误,那么就要将此这个连接对象从inputs和outputs及queue中删除,再把连接关闭
    for e in exceptional:
        if e in outputs:
            outputs.remove(e)

        inputs.remove(e)

        del msg_dict[e]

server.close()

#通过socket连接调用recv函数,获取客户端发送的数据并把数据放入定义好的queue中且将该客户端连接放入output列表中(监控和接收所有要发出去的数据),如果服务器端没有接收到客户端发送的数据,说明客户端已经断开连接,此时删除inputs和outputs中的socket连接及数据队列(全部删除);如果服务器端接收到客户端发送的数据,在下一次循环时将队列中的数据发送出去并将outputs中的客户端的socket连接删除,这样下一次循环select时监测outputs列表中就没有这个客户端的连接,那就会认为这个连接还处于非活动状态。最后在出现异常情况时,将服务器端监视的inputs/outputs/queue中都除去该连接。然后关闭连接。

posted @ 2017-12-16 20:35  Mr.hu  阅读(317)  评论(0编辑  收藏  举报