zoukankan      html  css  js  c++  java
  • Python—IO多路复用之epoll总结

    一、epoll接口

    epoll操作过程需要三个接口,分别如下:

    1. int epoll_create(int size);

    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

    需要注意的是,当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

    2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    epoll的事件注册函数,该函数是对指定描述符fd执行op操作。它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

    第一个参数是epoll_create()的返回值。

    第二个参数表示动作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。

    • EPOLL_CTL_ADD:注册新的fd到epfd中;
    • EPOLL_CTL_DEL:从epfd中删除一个fd;
    • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

    第三个参数是需要监听的fd(文件描述符)。

    第四个参数是告诉内核需要监听什么事件。

    3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    等待事件的产生,类似于select()调用。等待epfd上的io事件,最多返回maxevents个事件。

    参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

    二、epoll事件

    struct epoll_event结构如下:

    struct epoll_event {
        __uint32_t events;  /* Epoll events */
        epoll_data_t data;  /* User data variable */
    };
    

    events可以是以下几个宏的集合:

    • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    • EPOLLOUT:表示对应的文件描述符可以写;
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    • EPOLLERR:表示对应的文件描述符发生错误;
    • EPOLLHUP:表示对应的文件描述符被挂断;
    • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

    三、工作模式

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

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

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

    ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

    三、代码演示

    服务端代码:

    import socket
    import select, errno
    
    # 1.创建流式套接字实例
    server_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
    server_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)   
    server_fd.bind(("127.0.0.1", 8832))
    server_fd.listen(1024)
    server_fd.setblocking(0)
    
    try:
    	# 创建 epoll 句柄
        epoll_fd = select.epoll()
        # 向 epoll 句柄中注册 监听 socket 的 可读 事件
        epoll_fd.register(server_fd.fileno(), select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP)
        print("listen_fd: %s" % server_fd.fileno())
    except Exception as e:
        traceback.print_exc()
    
    connections = {}
    addresses = {}
    datalist = {}
    
    while True:
        # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
        epoll_list = epoll_fd.poll()
        for fd, events in epoll_list:
            print(fd,"===>",events)
            # 若为监听 fd 被激活
            if fd == server_fd.fileno():
                # 进行 accept -- 获得连接上来 client 的 ip 和 port,以及 socket 句柄
                conn, addr = server_fd.accept()
                # conn.send(("连接成功,服务端端口:%s, 本机客户端端口:%s" % (8832, addr[1])).encode())
                # 将连接 socket 设置为 非阻塞
                conn.setblocking(0)
                # 向 epoll 句柄中注册 连接 socket 的 可读 事件
                # epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
                epoll_fd.register(conn.fileno(), select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP)
                # 将 conn 和 addr 信息分别保存起来
                connections[conn.fileno()] = conn
                addresses[conn.fileno()] = addr
            elif select.EPOLLIN & events:
                # 有 可读 事件激活
                datas = ''
                while True:
                    try:
                        # 从激活 fd 上 recv 10 字节数据
                        data = connections[fd].recv(1024)
                        # 若当前没有接收到数据,并且之前的累计数据也没有
                        if not data and not datas:
                            # 从 epoll 句柄中移除该 连接 fd
                            epoll_fd.unregister(fd)
                            # server 侧主动关闭该 连接 fd
                            connections[fd].close()
                            # logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
                            break
                        else:
                            # 将接收到的数据拼接保存在 datas 中
                            datas += data.decode()
                    except socket.error as msg:
                        print("读穿了===>>>")	
                        # 在 非阻塞 socket 上进行 recv 需要处理 读穿 的情况。这里实际上是利用 读穿 出 异常 的方式跳到这里进行后续处理
                        if msg.errno == errno.EAGAIN:
                            # logger.debug("%s receive %s" % (fd, datas))
                            # 将已接收数据保存起来
                            datalist[fd] = datas
                            # 更新 epoll 句柄中连接d 注册事件为 可写
                            epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLOUT| select.EPOLLERR | select.EPOLLHUP)
                            # epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP)
                            break
                        else:
                            # 出错处理
                            epoll_fd.unregister(fd)
                            connections[fd].close()
                            # logger.error(msg)
                            break
            elif select.EPOLLOUT & events:
                # 有 可写 事件激活
                sendLen = 0
                # 通过 while 循环确保将 buf 中的数据全部发送出去        
                while True:
                    print(datalist[fd])
                	# 将之前收到的数据发回 client -- 通过 sendLen 来控制发送位置
                    sendLen += connections[fd].send((datalist[fd][sendLen:]).encode())
                    # 在全部发送完毕后退出 while 循环
                    if sendLen == len(datalist[fd].encode()):
                        break
                # 更新 epoll 句柄中连接 fd 注册事件为 可读
                # epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET)
                epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET | select.EPOLLERR | select.EPOLLHUP)
            elif select.EPOLLHUP & events:
                # 有 HUP 事件激活
                epoll_fd.unregister(fd)
                connections[fd].close()
                # logger.debug("%s, %d closed" % (addresses[fd][0], addresses[fd][1]))
            else:
                # 其他 epoll 事件不进行处理
                continue
    

    客户端代码:

    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    client.connect(("127.0.0.1", 8832))
    
    while True:
        msg = input("请输入要发送的信息...")
        client.send(msg.encode())
        if not msg or msg == "exit":
            break
        data = client.recv(1024)  # 接收服务端发送的信息
        print(data.decode())
    	
    client.close()

    https://segmentfault.com/a/1190000003063859

    https://www.sooele.com/1410.html

    https://www.cnblogs.com/Anker/p/3263780.html

    https://www.jb51.net/article/111899.htm

    https://my.oschina.net/moooofly/blog/147297

  • 相关阅读:
    二分练习题4 查找最接近的元素 题解
    二分练习题5 二分法求函数的零点 题解
    二分练习题3 查找小于x的最大元素 题解
    二分练习题2 查找大于等于x的最小元素 题解
    二分练习题1 查找元素 题解
    code forces 1176 D. Recover it!
    code forces 1173 B. Nauuo and Chess
    code forces 1173 C. Nauuo and Cards
    吴恩达深度学习课程笔记-15
    吴恩达深度学习课程笔记-14
  • 原文地址:https://www.cnblogs.com/liuhaidon/p/12195828.html
Copyright © 2011-2022 走看看