• doc


    http://www.cnblogs.com/phennry/p/5645369.html

    接着上篇博客我们继续介绍socket网络编程,今天主要介绍的内容:IO多路复用、多线程、补充知识点。

    一、IO多路复用

        IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用于以下场合:

    • 当客户端处理多个描述符时(一般是交互式输入和网络套接字),必须使用IO复用;

    • 当一个客户通过处理过个套接字时,而这种情况是可能的,但很少出现;

    • 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般也要用到IO复用;

    • 如果服务器纪要处理TCP,又要处理UDP时;

    • 如果一个服务器要处理多个服务或多个协议时,使用IO复用。

    IO多路复用的事件方式有三种,分别是:select、poll、epoll。

    下面我们就介绍下这三种事件方式:

    1、select

        首先select是可以跨平台的,select函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,知道有描述符就绪(有数据可读、可写或者有except异常),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。具体用法,请看下面代码:

     服务器端:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import socket
    import select               #select的监听是有个数限制1024
     
    sk = socket.socket()
    sk.bind(('127.0.0.1'9999,))
    sk.listen(5)
     
    inputs = [sk,]
    while True:
        rlist,w,x, = select.select(inputs,[],[],1)
        print(len(inputs),len(rlist))
        #监听sk(服务端)对象如果sk对象发生变化,表示有客户端来连接了,此时rlist值为[sk,]
        #监听conn对象,如果conn发生变化时,表示客户端有新消息发送过来,此时rlist的值为[客户端]
        #当s1向服务端发送消息时,rlist =[s1]
     
        for in rlist:
            if == sk:                     #判断新客户来连接
                conn,address = r.accept()   #conn也是socket的对象
                inputs.append(conn)
                conn.sendall(bytes('hello',encoding='utf-8'))
            else:
                r.recv(1024)                 #等待接收客户端发来消息

    客户端:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
     
    import socket
     
    sk = socket.socket()
     
    sk.connect(("127.0.0.1",9999,))
    data = sk.recv(1024)
    print(data)
     
    while True:
        inp = input('>>>:')
        sk.sendall(bytes(inp,encoding='utf-8'))
        print(sk.recv(1024))
    sk.close()

    select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。上面的例子只用来监视sk对象和conn对象。

    从上面的例子我们可以判断出如果同时多个客户端连接过来,某一个断开的话,服务器端会报错,为了解决这个问题我们将代码修改如下:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import socket
    import select
     
    sk = socket.socket()
    sk.bind(('127.0.0.1'9999,))
    sk.listen(5)
     
    inputs=[sk,]
    while True:
        rlist,w,x = select.select(inputs,[],[],1)
        print(len(inputs),len(rlist))
        for in rlist:
            if == sk:
                conn,address = r.accept()
                inputs.append(conn)
                conn.sendall(bytes('hello',encoding='utf-8'))
            else:
                print('=====================')
                try:
                    ret = r.recv(1024)
                    r.sendall(ret)
                    if not ret:        #如果接收的数据Wie空的话,主动触发下面的raise错误
                        raise  Exception('断开连接!!!')
                except Exception as e:
                    inputs.remove(r)   #如果客户端断开的话,移除监听的连接

    下面我们就使用select来实现一下socketserver服务端的功能,具体代码如下:

     

     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    import socket
    import select
     
    sk = socket.socket()             #创建套接字
    sk.bind(('127.0.0.1'9999,))    #绑定套接字
    sk.listen(5)                     #等待连接队列长度
     
    inputs=[sk,]                     #初始化读取数据的监听列表,最开始时希望从sk这个套接字上读取数据
    outputs=[]                       #初始化写入数据的监听列表,最开始时并没有客户端连接进来,所以列表为空
    messages = {}                    #创建字典,用来记录发往客户端的数据
     
    while True:
        rlist,wlist,elist = select.select(inputs,outputs,[],1)    #调用select监听所有列表中的套接字,并将准备好的套接字加入到对应的列表中
        print(len(inputs),len(rlist),len(wlist),len(outputs))
        for in rlist:
            if == sk:
                conn,address = r.accept()
                inputs.append(conn)
                messages[conn] = []
                conn.sendall(bytes('hello',encoding='utf-8'))
            else:
                print('=====================')
                try:
                    ret = r.recv(1024)
                    if not ret:                           #如果接收的数据为空的话,主动触发下面的raise错误
                        raise  Exception('断开连接!!!')
                    else:
                        outputs.append(r)
                        messages[r].append(ret)
                except Exception as e:
                    inputs.remove(r)                      #如果客户端断开的话,移除监听的连接
                    del messages[r]
     
    #所有给我发过消息的人
        for in wlist:
            msg = messages[w].pop()
            resp = msg + bytes('response',encoding='utf-8')
            w.sendall(resp)
            outputs.remove(w)

    在上面的例子中监控文件句柄有某一处发生了变化,可写、可读、异常属于Linux中的网络编程,属于同步I/O操作,属于I/O复用模型的一种:

    • rlist-->等待到准备好读;

    • wlist-->等待到准备好写;

    • xlist-->等待到一种异常。

         如果sk这个套接字可读,则说明有新链接到来,此时在sk套接字上调用accept,生成一个与客户端通讯的套接字,并将与客户端通讯的套接字加入到inputs列表,下一次可以通过select检查链接是否可读,然后在发往客户端的缓冲加入一项,键名为:与客户端通讯的套接字,键值为空队列,select系统调用是用来让我们的程序监视多个文件句柄(file descriptor)的状态变化的。程序会停在select这里等待,知道被监视的文件句柄有某一个会多个发生了状态改变。

    若可读的套接字不是sk套接字,有两种情况:一种是有数据到来,另一种是链接断开。

        如果有数据到来,先接收数据,然后将收到的数据填入往客户端的缓存区中的对应位置,最后将于客户端通讯的套接字加入到写数据的监听列表;

        如果套接字可读,但没有接收到数据,则说明客户端已经断开,这时需要关闭与客户端链接的套接字,进行资源清理。

     select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理的,这样所带来的缺点是:

    • select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SIZE设置,默认值是1024。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看。32位的系统默认为1024,64位的系统默认为2048。

    • 对socket进行扫描是采用的轮询的方法,效率较低当套接字比较多的时候,不管哪个socket是活跃的,都要遍历一遍,这样会浪费CPU时间。

    •  需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    2、poll

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

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    import socket
    import select
    import Queue
       
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setblocking(False)                    #设置成非阻塞
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_address = ("127.0.0.1"9999)
    server.bind(server_address)
    server.listen(5)
    print "服务器启动成功,监听IP:" , server_address
    message_queues = {}
     #超时,毫秒
    timeout = 5000  
    #监听哪些事件
    READ_ONLY = ( select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR)
    READ_WRITE = (READ_ONLY|select.POLLOUT)
    #新建轮询事件对象
    poller = select.poll()
    #注册本机监听socket到等待可读事件事件集合
    poller.register(server,READ_ONLY)
    #文件描述符到socket映射
    fd_to_socket = {server.fileno():server,}
    while True:
        print "等待活动连接......"
        #轮询注册的事件集合
        events = poller.poll(timeout)
        if not events:
          print "poll超时,无活动连接,重新poll......"
          continue
        print "有" len(events), "个新事件,开始处理......"
        for fd ,flag in events:
            = fd_to_socket[fd]
            #可读事件
            if flag & (select.POLLIN | select.POLLPRI) :
                if is server :
                    #如果socket是监听的server代表有新连接
                    connection , client_address = s.accept()
                    print "新连接:" , client_address
                    connection.setblocking(False)
                       
                    fd_to_socket[connection.fileno()] = connection
                    #加入到等待读事件集合
                    poller.register(connection,READ_ONLY)
                    message_queues[connection] = Queue.Queue()
                else :
                    #接收客户端发送的数据
                    data = s.recv(1024)
                    if data:
                        print "收到数据:" , data , "客户端:" , s.getpeername()
                        message_queues[s].put(data)
                        #修改读取到消息的连接到等待写事件集合
                        poller.modify(s,READ_WRITE)
                    else :
                        # Close the connection
                        print " closing" , s.getpeername()
                        # Stop listening for input on the connection
                        poller.unregister(s)
                        s.close()
                        del message_queues[s]
            #连接关闭事件
            elif flag & select.POLLHUP :
                print " Closing ", s.getpeername() ,"(HUP)"
                poller.unregister(s)
                s.close()
            #可写事件
            elif flag & select.POLLOUT :
                try:
                    msg = message_queues[s].get_nowait()
                except Queue.Empty:
                    print s.getpeername() , " queue empty"
                    poller.modify(s,READ_ONLY)
                else :
                    print "发送数据:" , data , "客户端:" , s.getpeername()
                    s.send(msg)
            #异常事件
            elif flag & select.POLLERR:
                print " exception on" , s.getpeername()
                poller.unregister(s)
                s.close()
                del message_queues[s]

    3、epoll

       epoll是在2.6内核中提出的,是之前的select和poll的增强版本。先对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理过个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

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

    使用epoll的优点:

    • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);

    • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你"活跃"的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll;

    • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap减少

    epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认的模式,LT模式与ET模式的区别如下:

    • LT模式(缺省工作模式):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件;

    • ET模式(高速工作模式):当epoll_wait检测到描述符事件发生将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

    下面举一个epoll事件处理的方式,来监听socket套接字的变化,请看下面代码:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    import socket, select
    import Queue
      
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_address = ("127.0.0.1", 9999)
    serversocket.bind(server_address)
    serversocket.listen(1)
    print "服务器启动成功,监听IP:" , server_address
    serversocket.setblocking(0)
    timeout = 10
    #新建epoll事件对象,后续要监控的事件添加到其中
    epoll = select.epoll()
    #添加服务器监听fd到等待读事件集合
    epoll.register(serversocket.fileno(), select.EPOLLIN)
    message_queues = {}
      
    fd_to_socket = {serversocket.fileno():serversocket,}
    while True:
      print "等待活动连接......"
      #轮询注册的事件集合
      events = epoll.poll(timeout)
      if not events:
         print "epoll超时无活动连接,重新轮询......"
         continue
      print "有" , len(events), "个新事件,开始处理......"
      for fd, event in events:
         socket = fd_to_socket[fd]
         #可读事件
         if event & select.EPOLLIN:
             #如果活动socket为服务器所监听,有新连接
             if socket == serversocket:
                connection, address = serversocket.accept()
                print "新连接:" , address
                connection.setblocking(0)
                #注册新连接fd到待读事件集合
                epoll.register(connection.fileno(), select.EPOLLIN)
                fd_to_socket[connection.fileno()] = connection
                message_queues[connection] = Queue.Queue()
             #否则为客户端发送的数据
             else:
                data = socket.recv(1024)
                if data:
                   print "收到数据:" , data , "客户端:" , socket.getpeername()
                   message_queues[socket].put(data)
                   #修改读取到消息的连接到等待写事件集合
                   epoll.modify(fd, select.EPOLLOUT)
         #可写事件
         elif event & select.EPOLLOUT:
            try:
               msg = message_queues[socket].get_nowait()
            except Queue.Empty:
               print socket.getpeername() , " queue empty"
               epoll.modify(fd, select.EPOLLIN)
            else :
               print "发送数据:" , data , "客户端:" , socket.getpeername()
               socket.send(msg)
         #关闭事件
         elif event & select.EPOLLHUP:
            epoll.unregister(fd)
            fd_to_socket[fd].close()
            del fd_to_socket[fd]
    epoll.unregister(serversocket.fileno())
    epoll.close()
    serversocket.close()

    二、多线程

     

    多线程,多进程:
    1,一个应用程序,可以有多进程和多线程
    2,默认:单进程,单线程
    3,单进程,多线程下:
    python多线程:IO操作是不会占用CPU,多线程会提高并发
    计算性操作,占用CPU,多进程提高并发
        4,GIL,全局解释器锁

    首先我们先看一个多线程的例子,然后在详细介绍,请看代码:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #!/usr/bin/env python
    #-*- coding:utf-8 -*-
     
    import time                                 #多线程的程序
     
    def f1(args):
        time.sleep(5)
        print(args)
     
    import threading
     
    t= threading.Thread(target=f1,args=(123,))  #创建子线程
    t.setDaemon(True)                           #True表示主线程不等子线程
    t.start()                                   #不代表当前线程会被立即执行
    t.join(2)                                   #表示主线程到此,等待...直到子线程执行完毕,
                                                #参数,表示主线程在此最多等N秒
    print('end')

    上面这个例子是开启一个线程,主线程最多等子线程两分钟的时间然后执行。下面我们一起看下threading的更多方法:

    • start    线程准备就绪,等待CPU调度;

    • setName  为线程设置名称;

    • getName 获取线程名称;

    • setDaemon  设置为后台线程或前台线程(默认),是否等待子线程,值为True或False;

    • join   逐个执行每个线程,执行完毕后据需往下执行,该方法使得多线程变得无意义;

    • run  线程被CPU调度后自动执行线程对象的run方法。

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #!/usr/bin/env python
    #-*- coding:utf-8 -*-
     
    import threading
    import time
     
    class MyThread(threading.Thread):
        def __init__(self,num):
            threading.Thread.__init__(self)
            self.num = num
        def run(self):         #定义每个线程要运行的函数
            print('running on number:%s'%self.num)
            time.sleep(2)
     
    if __name__=='__main__':
        t1 = MyThread(1)
        t2 = MyThread(2)
        t1.start()
        t2.start()
     
    #结果:
    running on number:1
    running on number:2

    多线程先介绍到这里,下篇博客在详细介绍,多线程,多进程和协程的用法。

    三、知识点补充

    1,python作用域

    通过两个简单的代码我们在来补充一下python作用域的问题,请看代码:

     

    1
    2
    3
    4
    5
    6
    7
    if 1==1:
        name = 'jack1'   #一个代码块
    print(name)
     
    def func():
        name = 'eric'
    print(name)

     

    1
    2
    3
    4
    5
    6
    7
    8
    name= 'jack'
    def f1():
        print(name)
    def f2():
        name = 'eric'
        return f1
    ret = f2()
    ret()

        ​分析一下上面代码的执行结果,在第一个例子中函数中的eric是无法输出的,因为python中函数为作用域的,在Python中无块级作用域,python中以函数为作用域,而在Java或C#中存在块级作用域。

        python作用域链,由内向外找,直到找不到报错,并且python作用域在代码执行之前已经确定,原定义的那个作用域,就去那个作用域里找。

    python和JavaScript的作用域是类似的,武sir大神给我们总结了五句话,方便理解作用域,详细介绍请参考链接:

     

    http://www.cnblogs.com/wupeiqi/p/5649402.html

    2,XX公司面试题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    li = [lambda :x for x in range(10)]
    #li列表
    #li列表中的元素:[函数、函数、函数.....]
    #函数在没有执行前,内部代码不执行
    #li[0]是个函数
    #执行第一个函数()
    #返回值是???
    r = li[0]()
    print(r)

    我们一起分析一下这个程序的结果是什么,这用到了我们上面补充的作用域知识,我们可以先将lambda函数修改成正常函数的方式,在一步一步分析:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #!/usr/bin/env python
    #-*- coding:utf-8 -*-
     
    for x in range(10):
        def test():
            return x
     
    ret=test()
    print ret

         因为python的作用域为函数,在函数中为局部作用域,定义在函数外的为全局作用域,因为python作用域在代码执行之前已经确定,原定义的那个作用域,就去那个作用域里找,这里的结果为9。

     

        今天就介绍到这里,我们今天主要介绍了I/O多路复用的知识和多线程的定义,虽然I/O多路复用在我们平常写代码的时候用的比较少,但我们理解了后,方便我们以后去读懂源码。​





  • 相关阅读:
    EasyHook库系列使用教程之四钩子的启动与停止
    inputclean插件的使用方法
    机器学习(十三)——机器学习中的矩阵方法(3)病态矩阵、协同过滤的ALS算法(1)
    <LeetCode OJ> 204. Count Primes
    leetcode 235: Lowest Common Ancestor of a Binary Search Tree
    数据结构经常使用算法
    调侃物联网开源框架,我们什么时候也来开源一个?
    字符编码简单介绍
    PriorityQueue ,ArrayList , 数组排序
    从struts2.1开始Convention零配置
  • 原文地址:https://www.cnblogs.com/phennry/p/5673635.html
Copyright © 2020-2023  润新知