zoukankan      html  css  js  c++  java
  • 利用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

  • 相关阅读:
    Java中@Override的作用
    微软面试题: LeetCode 152. 乘积最大子数组 出现次数:2
    微软面试题: LeetCode 300. 最长递增子序列 出现次数:2
    微软面试题: LeetCode 76. 最小覆盖子串 出现次数:2
    微软面试题:剑指 Offer 52. 两个链表的第一个公共节点 出现次数:2
    微软面试题: LeetCode 79. 单词搜索 出现次数:2
    微软面试题: LeetCode 39. 组合总和 出现次数:2
    微软面试题: LeetCode 151. 翻转字符串里的单词 出现次数:2
    微软面试题: LeetCode 415. 字符串相加 出现次数:2
    微软面试题: LeetCode 110. 平衡二叉树 出现次数:2
  • 原文地址:https://www.cnblogs.com/along1226/p/5643516.html
Copyright © 2011-2022 走看看