• 网络编程socket 结合IO多路复用select; epool机制分别实现单线程并发TCP服务器


    select版-TCP服务器

    1. select 原理

    在多路复用的模型中,比较常用的有select模型和epoll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。

    网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。

    这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。

    2、select版本基于socket模块的TCP并发服务器代码示例

    说明:服务端采用socket.AF_INET socket.SOCK_STREAM ;即IP/TCP协议

        # 使用select ,阻塞等待监控哪些套接字有新数据
        # select模块的select()是个函数,接收四个参数rlist, wlist, xlist, timeout=None
        # 分别表示准备监控读套接字列表,准备监控写套接字列表,准备监控异常套接字列表,超时时间
        # 返回值为可读套接字列表,可写套接字列表, 异常套接字列表
        # 有任何套接字有变化,就会有返回值,否则就一直阻塞住
    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    #  @Time: 2020/7/4 16:56
    #  @Author:zhangmingda
    #  @File: socket_select_study.py
    #  @Software: PyCharm
    #  Description:select io多路复用实现单线程并发TCP服务器
    
    import socket
    import sys
    import select
    
    listenAddr = ('', 8080)
    tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcpServer.bind(listenAddr)
    tcpServer.listen(5) # 设置可以并发建立的连接数
    
    # 准备让select 模块监控是否有数据可以收的套接字列表
    inputs = [tcpServer, ] # sys.stdin linux下还可以加入sys.stdin作为监控键盘的输入
    # 准备一个装已建立连接的字典,键为连接实例,值为对应的客户端IP地址信息
    established = {}
    
    running = True
    while True:
        # 使用select ,阻塞等待监控哪些套接字有新数据
        # select模块的select()是个函数,接收四个参数rlist, wlist, xlist, timeout=None
        # 分别表示准备监控读套接字列表,准备监控写套接字列表,准备监控异常套接字列表,超时时间
        # 返回值为可读套接字列表,可写套接字列表, 异常套接字列表
        # 有任何套接字有变化,就会有返回值,否则就一直阻塞住
        print("使用select IO多路负用监控套接字状态")
        readable, writeable, exceptional = select.select(inputs, [], [])
        print("注意:被监控的套接字有变化")
    
        # 循环判断收数据的套接字是否有数据到达
        for sock in readable:
            # 当收到数据的套接字为tcpServer时,说明时新的一个客户端到了
            if sock == tcpServer:
                # 获取套接字中活动的连接对象和客户端地址
                conn, addr = tcpServer.accept()
                # 将活动的套接字对象添加到监控列表中
                inputs.append(conn)
                print("建立了一个新TCP连接:", addr)
                established[conn] = addr
    
            # 收到数据的时 标准输入,即键盘
            elif sock == sys.stdin:
                # 获取输入内容
                cmd = sys.stdin.readline()
                print("获取到键盘输入指令:%s 退出" % cmd)
                running = False
                break
    
    
            # 收到的数据不是上面两个,则肯定时已建立链接的套接字有新数据或者断开连接了
            else:
                # 读取客户端发来的数据
                data = sock.recv(1024)
                # 如果数据存在,就原封返回去:做个echo服务器
                if data:
                    print("收到并返回:",data.decode('gb2312'))
                    sock.send(data)
                # 如果不存在数据,说明连接状态异常了,断开连接并从监控列表移除连接对象
                else:
                    inputs.remove(sock)
                    sock.close()
                    # 移除记录的客户端地址信息
                    print(established.get(sock), "客户已断开")
                    established.pop(sock)
    
        # while 循环必须有个跳出循环的条件,否则while下面的代码有获取不到while上面变量的风险
        if not running:
            break
    
    tcpServer.close()

    客户端使用"网络调试助手.exe"发包

    windows 下pycharm服务端输出效果:

     

     Linux下监控键盘输入结果

     如上代码当处理逻辑中有sleep时while 循环仍会卡住。并没有实现并发处理

    可以参考之前threading.Thread() 多线程方式处理;每个线程处理一次套接字变化,参考代码如下:

    当一个请求卡住时,不影响第二个连接的处理逻辑

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    #  @Time: 2020/7/4 16:56
    #  @Author:zhangmingda
    #  @File: socket_select_study.py
    #  @Software: PyCharm
    #  Description:select io多路复用实现单线程并发TCP服务器
    
    import socket
    import sys
    import select
    import time
    from threading import Thread
    
    listenAddr = ('', 8080)
    tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcpServer.bind(listenAddr)
    tcpServer.listen(5) # 设置可以并发建立的连接数
    
    # 准备让select 模块监控是否有数据可以收的套接字列表
    inputs = [tcpServer, ] # sys.stdin linux下还可以加入sys.stdin作为监控键盘的输入
    # 准备一个装已建立连接的字典,键为连接实例,值为对应的客户端IP地址信息
    established = {}
    
    # 退出循环判断用变量
    running = True
    
    def sendTask(sock, data):
        print("收到并返回:", data.decode('gb2312'))
        sock.send(data)
        time.sleep(10)
    
    while True:
        # 使用select ,阻塞等待监控哪些套接字有新数据
        # select模块的select()是个函数,接收四个参数rlist, wlist, xlist, timeout=None
        # 分别表示准备监控读套接字列表,准备监控写套接字列表,准备监控异常套接字列表,超时时间
        # 返回值为可读套接字列表,可写套接字列表, 异常套接字列表
        # 有任何套接字有变化,就会有返回值,否则就一直阻塞住
        print("使用select IO多路负用监控套接字状态")
        readable, writeable, exceptional = select.select(inputs, [], [])
        print("注意:被监控的套接字有变化")
    
        # 循环判断收数据的套接字是否有数据到达
        for sock in readable:
            # 当收到数据的套接字为tcpServer时,说明时新的一个客户端到了
            if sock == tcpServer:
                # 获取套接字中活动的连接对象和客户端地址
                conn, addr = tcpServer.accept()
                # 将活动的套接字对象添加到监控列表中
                inputs.append(conn)
                print("建立了一个新TCP连接:", addr)
                established[conn] = addr
    
            # 收到数据的时 标准输入,即键盘
            elif sock == sys.stdin:
                # 获取输入内容
                cmd = sys.stdin.readline()
                print("获取到键盘输入指令:%s 退出" % cmd)
                running = False
                break
    
    
            # 收到的数据不是上面两个,则肯定时已建立链接的套接字有新数据或者断开连接了
            else:
                # 读取客户端发来的数据
                data = sock.recv(1024)
                # 如果数据存在,就原封返回去:做个echo服务器
                if data:
                    # print("收到并返回:",data.decode('gb2312'))
                    # sock.send(data)
                    # time.sleep(10)
                    # 可以考虑使用一个新线程处理新数据
                    t = Thread(target=sendTask, args=(sock, data))
                    t.start()
    
                # 如果不存在数据,说明连接状态异常了,断开连接并从监控列表移除连接对象
                else:
                    inputs.remove(sock)
                    sock.close()
                    # 移除记录的客户端地址信息
                    print(established.get(sock), "客户已断开")
                    established.pop(sock)
    
        # while 循环必须有个跳出循环的条件,否则while下面的代码有获取不到while上面变量的风险
        if not running:
            break
    
    tcpServer.close()

    并行处理请求效果

    epoll版-TCP服务器

    1. epoll的优点:

    1. 没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024

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

    2. epoll使用参考代码

     说明:epoll 监控的是被注册的套接字所对应的文件描述符。

          例如 sys.stdin文件描述符数字为0;

          sys.stdout文件描述符数字为1;

               sys.stderr文件描述符数字为2;

             其它套接字的文件描述符例如网络io socket的 通过 套接字实例的.fineno()获取具体对应的文件描述符对应数字。

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-
    #  @Time: 2020/7/5 11:32
    #  @Author:zhangmingda
    #  @File: socket_epoll_study.py
    #  @Software: PyCharm
    #  Description: 使用epoll 事件通知机制实现单线程并发服务器
    
    import socket
    import select
    import time
    
    
    # 创建一个套接字作为TCP服务端
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 设置套接字,服务端主动断开后快速回收,无需等待2MSL时间
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
    
    # 绑定本机监听地址和断开
    listenAddr = ('',7788)
    s.bind(listenAddr)
    
    # 开始监听端口
    s.listen(10)
    
    # 创建一个epoll对象,
    # 注意:windows下没有epoll
    epoll = select.epoll()
    
    # 将监听套接字的文件描述符注册到epoll中
    # select.EPOLLIN 为只监听是否有新数据可读,
    # select.EPOLLET 设置事件如果没有处理下次是否还进行通知
    # epoll对文件描述符的操作有两种模式:
    #   select.EPOLLET:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。
    #   select.EPOLLLT:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。
    epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET)
    
    # 准备已连接客户端的socket文件符和对应连接socket、客户端地址存储的字典
    conn_established = {}
    conn_client_addr = {}
    
    # 循环等待客户端来连接或者对方发送数据
    while True:
        # 反复检查已活动的文件描述符列表,有就返回,没有就阻塞
        # 文件描述符列表的元素为文件描述符和事件组成的元组
        epoll_list = epoll.poll()
        print('epoll_list:',epoll_list)
    
        # 循环所有文件描述符列表内容做判断
        for fd, events in epoll_list:
            print('fd:', fd)
            print('events:', events)
    
            # 如果是监听的套接字文件描述符活动,说明有新连接到了
            if fd == s.fileno():
                conn, client_addr = s.accept()
                print("新连接:", client_addr)
                # 将新连接的socket文件描述符注册到监控列表中
                epoll.register(conn.fileno(), select.EPOLLIN|select.EPOLLET)
                # 存储已连接的socket
                conn_established[conn.fileno()] = conn
                conn_client_addr[conn.fileno()] = client_addr
            # 如果活动的不是上面的监听套接字文件描述符,
            # 那么就是已连接的文件描述符有新数据
            # 此处使用fd判断或者events判断都行,
            # 如下用events判断之前fd对监听套接字的判断如果符和已经拦截单独处理
            elif events == select.EPOLLIN:
                # 到此肯定是新数据,获取新数据进行判断
                recvData = conn_established.get(fd).recv(1024)
                # 返回数据大于0 是新数据,否则一定是断开连接了
                if len(recvData) > 0:
                    print("from client: %s" % recvData.decode('gb2312'))
                    # 如果做一个Echo服务器,原封不动返回给客户端
                    conn_established.get(fd).send(recvData)
                else:
                    # 判断为客户端断开连接,从epoll中注销对应文件描述符
                    epoll.unregister(fd)
                    # 获取到服务端的socket进行关闭
                    conn_established[fd].close()
                    print("客户端 %s 已断开连接" % str(conn_client_addr[fd]))

    测试服务端输出效果

  • 相关阅读:
    制作Autorun的CD
    Sybase ASE MDA tables 装不上怎么办?
    对于TStringList.Find函数,我有话要说
    HH.exe CHM Operator Command.
    Delphi 7的一些常用的快捷键
    Explain Plan
    在Delphi中的Log
    subst windows下实用的磁盘映射工具
    Excel 2007 如何冻结多行&多列
    LinqToDataTable[转]
  • 原文地址:https://www.cnblogs.com/zhangmingda/p/13236032.html
Copyright © 2020-2023  润新知