利用select实现伪并发的socket

使用socket模块可以实现程序之间的通信,但是server在同一时刻只能和一个客户端进行通信,如果要实现一个server端可以和多个客户端进行通信可以使用

1.多线程

2.多进程

3.select I/O多路复用

来实现服务器端和多个客户端进行通信,本文将会介绍使用select实现伪并发。

I/O多路复用指:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。I/O多路复用的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select /epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

小试牛刀:利用select监听终端输入

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

while True:
    readable,writeable,errable = select.select([sys.stdin,],[],[],5)
    print readable
    print type(readable)
    if sys.stdin in readable:
        print "you input: ",sys.stdin.readline()
监听终端输入

执行结果:

 select.select()方法的参数解释:

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

 由此解释以上代码的执行结果:

1.程序将sys.stdin句柄写入到了select.select()方法的第一个句柄序列中,所以sys.stdin句柄将会被select监听着

2.一旦sys.stdin这个句柄发生了变化,select.select()方法将会返回一个列表,这个列表中包含了变化的句柄,这个列表就是readable

3.判断sys.stdin是否在这个列表中,在的话就执行下边的语句

4.select.select的第四个参数是超时时间,如果3秒内没有文件句柄发送变化,就返回空的列表

5.超时时间select.select方法的第四个参数为3

利用selct监听socket

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

sk = socket.socket()
ip_port = ("127.0.0.1",7777)
sk.bind(ip_port)
sk.listen(5)
sk.setblocking(False)

while True:
    readable,writeable,errable = select.select([sk,],[],[],1)
    for s in readable:
        cnn,add = s.accept()
        cnn.sendall("欢迎")
 #       data = cnn.recv(1024)
        print add
select监听socket

利用select监听多个端口

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

sk1 = socket.socket()
ip_port = ("127.0.0.1",7777)
sk1.bind(ip_port)
sk1.listen(5)
sk1.setblocking(False)

sk2 = socket.socket()
ip_port = ("127.0.0.1",7778)
sk2.bind(ip_port)
sk2.listen(5)
sk2.setblocking(False)

while True:
    readable,writeable,errable = select.select([sk1,sk2,],[],[],1)
    for s in readable:
        cnn,add = s.accept()
        cnn.sendall(repr(add))
        print add
select监听多个端口的socket

----------------------------------------------------------我是分割线---------------------------------------------------------------------

以上的内容其实只是为了演示select能够监听变化文件描述符的功能,下面的才是使用select的妙处所在

利用select同时和多个客户端进行交互

#/usr/bin/env python
#-*- coding:utf-8 -*-
import time
import socket
import select
#创建socket对象
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt
#设置监听的IP与端口
sk.bind(('127.0.0.1',6666))
#设置client最大等待连接数
sk.listen(5)
sk.setblocking(False) #这里设置setblocking为Falseaccept将不在阻塞,但是如果没有收到请求就会报错
inputs = [sk,] #将sk这个对象加入到列表中,并且赋值给inputs
#原因:看上例conn是客户端对象,客户是一直连接着呢,连接的时候状态变了,连接上之后,连接上之后,还是服务端的socket 有关吗?
#是不是的把他改为动态的?

while True:
    readable_list, writeable_list, error_list = select.select(inputs,[],[],1)  #把第一个参数设为列表动态的添加
    time.sleep(2) #测试使用
    print "inputs list :",inputs     #打印inputs列表,查看执行变化
    print "file descriptor :",readable_list #打印readable_list ,查看执行变化

    for r in readable_list:
        if r == sk:  #这里判断,如果是客户端连接过来的话他不是sk,如果是服务端的socket连接过来的话是sk
            conn,address = r.accept()
            inputs.append(conn)
            print address
        else:
        #如果是客户端,接受和返回数据
            client_data = r.recv(1024)
            r.sendall(client_data)

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

import socket

client = socket.socket()
client.connect(('127.0.0.1',6666))
client.settimeout(5)

while True:
    client_input = raw_input('please input message:').strip()
    client.sendall(client_input)
    server_data = client.recv(1024)
    print server_data

select socket server - client
client

交互过程详解:

#1  默认,sk这个对象文件句柄就在inputs列表中select监听客户端的请求,当有客户端请求过来 client1 ---> server
#用户捕获了变化readable_list = [sk,]  那么循环是有值得,判断r = sk 说明是一个新的请求链接,然后把client链接加入到inputs里 inputs = [sk,conn1,]
#如果现在什么都不做,那么select无法捕获到变化:readable_list = []
#执行看下:
inputs list : [<socket._socketobject object at 0x0000000002C66798>] #默认inputs list 就有一个server socket sk 对象
file descriptor : [<socket._socketobject object at 0x0000000002C66798>]  #当有客户端请求过来时候,sk发生了变化,select捕获到了
('127.0.0.1', 62495)
inputs list : [<socket._socketobject object at 0x0000000002C66798>, <socket._socketobject object at 0x0000000002C66800>]  #第二次循环的时候,inputs = [sk,conn1,]
file descriptor : [] #第二次循环的时候readable_list = [] 因为客户端没有做任何操作,没有捕获到变化所以为空

#2 又有一个新的链接过来了,谁变化了?  sk 他变化了,有人向他发起了一个请求链接,那么现在inputs = [sk,conn1,conn2]  readable_list = [sk]
#本次循环完成之后再循环的时候 inputs = [sk,conn1,conn2,]  readable_list = [] 因为我们没有继续做操作

#第一个链接
inputs list : [<socket._socketobject object at 0x0000000002C56798>]  #默认只有一个对象
file descriptor : []
inputs list : [<socket._socketobject object at 0x0000000002C56798>]  
file descriptor : [<socket._socketobject object at 0x0000000002C56798>] #当捕获到,判断是否是新链接,如果是加入到inputs列表中监控
('127.0.0.1', 62539)
inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>]  #inputs列表变更为了[sk,conn1]
file descriptor : []  #因为没有后续的操作,这里没有捕获到异常所以列表为空

#第二个链接
inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>]  #第一个链接没有做任何操作
file descriptor : [<socket._socketobject object at 0x0000000002C56798>] #第二个链接过来了被捕获到,判断是否为新链接
('127.0.0.1', 62548)
inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>, <socket._socketobject object at 0x0000000002C56868>] #加入到inputs列表中
file descriptor : []
inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>, <socket._socketobject object at 0x0000000002C56868>]
file descriptor : []
inputs list : [<socket._socketobject object at 0x0000000002C56798>, <socket._socketobject object at 0x0000000002C56800>, <socket._socketobject object at 0x0000000002C56868>]
file descriptor : []
process

 优化点一:当某一个客户端断开连接之后,该客户端的socket描述符还是在服务端的监听列表中的,能不能将已经断开连接的客户端的socket描述符删除掉?

#/usr/bin/env python
#-*- coding:utf-8 -*-
import time
import socket
import select
#创建socket对象
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt
#设置监听的IP与端口
sk.bind(('127.0.0.1',6666))
#设置client最大等待连接数
sk.listen(5)
sk.setblocking(False) #这里设置setblocking为Falseaccept将不在阻塞,但是如果没有收到请求就会报错
inputs = [sk,] #将sk这个对象加入到列表中,并且赋值给inputs
#原因:看上例conn是客户端对象,客户是一直连接着呢,连接的时候状态变了,连接上之后,连接上之后,还是服务端的socket 有关吗?
#是不是的把他改为动态的?

while True:
    readable_list, writeable_list, error_list = select.select(inputs,[],[],1)  #把第一个参数设为列表动态的添加
    time.sleep(2) #测试使用
    print "inputs list :",inputs     #打印inputs列表,查看执行变化
    print "file descriptor :",readable_list #打印readable_list ,查看执行变化

    for r in readable_list:
        if r == sk:  #这里判断,如果是客户端连接过来的话他不是sk,如果是服务端的socket连接过来的话是sk
            conn,address = r.accept()
            inputs.append(conn)
            print address
        else:
        #如果是客户端,接受和返回数据
            client_data = r.recv(1024)
            if client_data:
                r.sendall(client_data)
            else:
                inputs.remove(r)#如果没有收到客户端端数据,则移除客户端句柄 因为,不管是正常关闭还是异常关闭,client端的系统底层都会发送一个消息

select socket server - server release client-connect
server

优化点二:假如说我们需要将客户端发来的数据写入到数据库中,不同客户端发来的消息要存放在不同的表中,那怎么办?

  难点:

    1.我们并不知道消息和客户端的对应关系

      2.假如说在同一时间发送了大量的消息,来不及写怎么办?

解决这一问题我们需要引入一个新的数据结构-------> Queue(队列)

#!/usr/bin/env python
#-*- coding:utf-8 -*-
__author__ = 'luo_t'
import select
import socket
import Queue
import time

sk = socket.socket()
sk.bind(('127.0.0.1',6666))
sk.listen(5)
sk.setblocking(False) #定义非阻塞
inputs = [sk,]  #定义一个列表,select第一个参数监听句柄序列,当有变动是,捕获并把socket server加入到句柄序列中
outputs = [] #定义一个列表,select第二个参数监听句柄序列,当有值时就捕获,并加入到句柄序列
message = {}
#message的样板信息
#message = {
#    'c1':队列,[这里存放着用户C1发过来的消息]例如:[message1,message2]
#    'c2':队列,[这里存放着用户C2发过来的消息]例如:[message1,message2]
#}


while True:
    readable_list, writeable_list, error_list = select.select(inputs,outputs,[],1)
    #文件描述符可读 readable_list    只有第一个参数变化时候才捕获,并赋值给readable_list
    #文件描述符可写 writeable_list   只要有值,第二个参数就捕获并赋值给writeable_list
    #time.sleep(2)
    print 'inputs:',inputs
    print 'output:'
    print 'readable_list:',readable_list
    print 'writeable_list:',writeable_list
    print 'message',message
    for r in readable_list: #当readable_list有值得时候循环
        if r == sk:  #判断是否为链接请求变化的是否是socket server
            conn,addr = r.accept() #获取请求
            inputs.append(conn) #把客户端对象(句柄)加入到inputs里
            message[conn] = Queue.Queue() #并在字典里为这个客户端连接建立一个消息队列
        else:
            client_data = r.recv(1024) #如果请求的不是sk是客户端接收消息
            if client_data:#如果有数据
                outputs.append(r)#把用户加入到outpus里触发select第二个参数
                message[r].put(client_data)#在指定队列中插入数据
            else:
                inputs.remove(r)#没有数据,删除监听链接
                del message[r] #当数据为空的时候删除队列~~
    for w in writeable_list:#如果第二个参数有数据
        try:
            data = message[w].get_nowait()#去指定队列取数据 并且不阻塞
            w.sendall(data) #返回请求输入给client端
        except Queue.Empty:#反之触发异常
            pass
        outputs.remove(w) #因为第二个参数有值得时候就触发捕获值,所以使用完之后需要移除它
        #del message[r]
    print '%s' %('-' * 40)

select socket server - server read | write separation
读写分离的版本

总结:

使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
但这个模型依旧有着很多问题。首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很 多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了queue,Solaris提供了/dev/poll,…。如果需要实现 更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具 有较好跨平台能力的服务器会比较困难。
    其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。如下例,庞大的执行体1的将直接导致响应事件2的执行体迟迟得不到执行,并在很大程度上降低了事件探测的及时性。

I/O多路复用的使用场景

#(1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
#(2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
#(3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
#(4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
#(5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
'''与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。'''

参考资料:

http://www.cnblogs.com/luotianshuai/p/5098408.html

原文地址:https://www.cnblogs.com/along1226/p/5643516.html