Linux socket通信– poll和epoll

Linux socket通信-- poll和epoll

1 poll 函数

1.1 poll函数用法

poll函数用于检测一组文件描述符(File Descroptor, 简称 fd)上的可读可写和出错事件,其函数签名如下:

#include <poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

参数解析如下:

  1. fds:指向一个结构体数组首个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定检测某个指定的fd的条件。
    struct pollfd结构体如下:

    struct pollfd {
        int fd;         // 待检测事件的fd
        short events;   // 关心的事件组合
        short revents;  // 检测后得到的事件类型
    }
    

    struct pollfdevents字段是由开发者设置的,告诉内核我们关注什么事件,而revents 字段是poll函数返回时内核设置的,说明该fd发生了什么事件。取值如下:

    事件宏 事件描述 是否可以作为输入(events) 是否可以作为输出(revents)
    POLLIN 数据可读(普通数据&优先数据)
    POLLOUT 数据可写(普通数据&优先数据)
    POLLRDNORM 等同于POLLIN
    POLLRDBAND 优先级带数据可读(一般用于Linux)
    POLLPRI 高优先级数据可读,列如TCP带外数据
    POLLWRNORM 等同于POLLOUT
    POLLWRBAND 优先级带数据可写
    POLLRDHUP TCP连接被对端关闭,或者关闭了写操作由GUN引入
    POPPHUP 挂起
    POLLERR 错误
    POLLNVAR 文件描述符没有打开
  2. nfds:参数fds结构体数组的长度。nfds_t 在本质上是unsigned long int,其定义如下: typedef unsigned long int nfds_t;

  3. timeout:表示poll的函数的超时时间,单位为毫秒。

1.2 poll和select比较

  1. poll不要求开发者计算最大文件描述符加1的大小;
  2. 与select相比,poll在处理大数量的文件描述符时速度更快;
  3. poll没有最大连接数量的限制,因为其存储fd的数组没有长度限制;
  4. 在调用poll函数时,只需对参数进行一次设置就好了。

1.3 poll实例

poll server实例
/**
 * @file poll_server.cpp
 * @author your name (you@domain.com)
 * @brief 演示poll 函数的用法,poll_server.cpp
 * @version 0.1
 * @date 2021-12-16
 *
 * @copyright Copyright (c) 2021
 *
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>

#define INVALID_FD -1

int main()
{
    // 创建一个监听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if ( listenfd == INVALID_FD ) {
        std::cout << "Socket create error: " << strerror(errno) << std::endl;
        return -1;
    }

    // 将监听socket设置为非阻塞的
    int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) {
        close(listenfd);
        std::cout << "set listenfd to nonblock error:" << strerror(errno) << std::endl;
        return -1;
    }

    // 复用地址和端口号
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*) &on, sizeof(on));
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*) &on, sizeof(on));

    // 初始化服务器的地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if(bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1) {
        std::cout << "bind listen socket error." << std::endl;
        close(listenfd);
        return -1;
    }

    // 启动监听
    if (listen(listenfd, SOMAXCONN) == -1) {
        std::cout << "listen socket error." << std::endl;
        close(listenfd);
        return -1;
    }

    struct sockaddr_in listendAddr;
    unsigned int listendAddrLen = sizeof(listendAddr);
    //获取监听的地址和端口
    if(getsockname(listenfd, (struct sockaddr *)&listendAddr, &listendAddrLen) == -1){
        std::cout<< "getsockname error." << std::endl;
        return -1;
    }
    std::cout << "listen address = " << inet_ntoa(listendAddr.sin_addr) << ":" << ntohs(listendAddr.sin_port) << std::endl;

    std::vector<pollfd> fds;
    pollfd listen_fd_info;
    listen_fd_info.fd = listenfd;
    listen_fd_info.events = POLLIN;
    listen_fd_info.revents = 0;

    fds.push_back(listen_fd_info);

    // 是否存在无效的fd标志
    bool exist_invalid_fd;
    int n;
    while(true) {
        exist_invalid_fd = false;
        n = poll(&fds[0], fds.size(), 1000);
        if (n < 0) {
            // 被信号中断
            if (errno == EINTR)
                continue;

            // 出错,退出
            break;
        } else if (n == 0) {
            // 超时
            continue;
        }

        for(size_t i = 0; i < fds.size(); i++) {
            // 事件可读
            if (fds[i].revents & POLLIN) {
                if (fds[i].fd == listenfd) {
                    // 监听 socket,接受新连接
                    struct sockaddr_in clientaddr;
                    socklen_t clientaddrlen = sizeof(clientaddr);
                    // 接受客户端连接并将产生的clientfd加入fds的集合中
                    int clientfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clientaddrlen);
                    if (clientfd != -1) {
                        // 将客户端socket 设置为非阻塞的
                        int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
                        int newSocketFlag = oldSocketFlag | O_NONBLOCK;
                        if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) {
                            close(clientfd);
                            std::cout << "set clientfd to nonblock error." << std::endl;
                        } else {
                            struct pollfd client_fd_info;
                            client_fd_info.fd = clientfd;
                            client_fd_info.events = POLLIN;
                            client_fd_info.revents = 0;
                            fds.push_back(client_fd_info);
                            std::cout << "new client accepted, clientfd:" << clientfd << std::endl;
                        }
                    }
                } else {
                    // 普通clientfd,收取数据
                    char buf[64] = {0};
                    int m = recv(fds[i].fd, buf, 64, 0);
                    if (m <= 0) {
                        if (errno != EINTR && errno != EWOULDBLOCK) {
                            // 出错或对端关闭了连接,关闭对应的clientfd,并设置了无效标志位
                            for(std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); iter++) {
                                if (iter->fd == fds[i].fd) {
                                    std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl;
                                    close(fds[i].fd);
                                    iter->fd = INVALID_FD;
                                    exist_invalid_fd = true;
                                    break;
                                }
                            }
                        }
                    } else {
                        std::cout << "recv from client:" << buf << " , clientfd:" << fds[i].fd << std::endl;
                    }
                }

            } else if (fds[i].revents & POLLERR) {
                // TODO :暂且不处理
                std::cout << "error: << " << fds[i].revents << std::endl;
            }
        }

        if (exist_invalid_fd) {
            // 统一清理无效的fd
            for (auto iter = fds.begin(); iter != fds.end(); iter++) {
                if (iter->fd == INVALID_FD) {
                    iter = fds.erase(iter);
                } else {
                    iter++;
                }
            }
        }

    }

    // 关闭所有socket
    for (auto iter = fds.begin(); iter != fds.end();iter++) {
        close(iter->fd);
    }

    return 0;
}

2 epoll 函数

2.1 epoll 接口

头文件:

#include <sys/epoll.h>

2.1.1 epoll_create

获取epoll 的fd:

int epoll_create(int size);

创建一个epoll句柄,size用来告诉内核这个监听的数目多大;注意:当创建好epoll句柄后,返回fd值,可在linux下通过/proc/进程/fd/,可以看到这个fd。

  1. 参数说明:
    • size:监听数目多少
  2. 函数返回:
    • epollfd:epoll_create 返回fd;

2.1.2 epoll_ctl

epoll_ctl epoll的事件注册函数,它不同与select() 是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数。

int epoll_ctl(int epollfd, int op, int fd, struct epoll_event *event);
  1. 参数说明:
    • epollfd: epoll_create 返回值
    • op: 表示监听动作
      + EPOLL_CTL_ADD:注册新的fd到epfd中;
      + EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
      + EPOLL_CTL_DEL:从epfd中删除一个fd;
    • fd:需要监听的fd
    • event:监听内容:
        typedef union epoll_data {
              void *ptr;
              int fd;
              __uint32_t u32;
              __uint64_t u64;
          } epoll_data_t;
      
          struct epoll_event {
              __uint32_t events; /* Epoll events */
              epoll_data_t data; /* User data variable */
          };
      

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

  2. 参数返回:
    • 返回运行结果,0未成功,非0 失败

2.1.3 epoll_wait

等待epoll事件从epoll实例中发生, 并返回事件以及对应文件描述符

 int epoll_wait(int epollfd, struct epoll_event * events, int maxevents, int timeout)
  1. 参数说明:

    • epollfd: epoll的描述符。
    • events:events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)
    • maxevents:本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的
    • timeout:timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。
  2. 函数返回:

    • epoll_wait若调用成功,则会返回有事件的fd数量;若返回0,则表示超时;若调用失败,则返回-1。
  3. 使用示例

    while (true) {
        epoll_event epoll_events[1024];
        int n = epoll_wait(epollfd, epoll_events, 1024, 10000);
        if (n < 0) {
            // 被信号中断
            if (errno == EINTR) {
                continue;
            }
    
            // 出错,退出
            break;
        } else if (n == 0) {
            //超时,继续
        }
    
        for(size_t i = 0; i < n; i++) {
            if (epoll_events[i].events & EPOLLIN) {
                // 处理可读事件
            }else if (epoll_events[i].events & EPOLLOUT) {
                // 处理可写事件
            }else if (epoll_events[i].events & EPOLLERR) {
                // 处理出错事件
            }
        }
    }
    

2.2 epoll_wait 与 poll函数的区别

通过对 poll 与 epoll_wait 函数的介绍可以发现:我们在 epoll_wait 函数调用完成后,可以通过参数 event 拿到所有有事件就绪的 fd(参数 event 仅仅是个输出参数);而 poll函数的事件集合参数(poll函数的第1个参数)在调用前后数量都不会改变,只不过调用前通过pollfd结构体的events字段设置待检测的事件,调用后通过pollfd结构体的revents字段检测就绪的事件(参数fds既是入参也是出参)。

2.3 LT 模式和 ET模式

与poll模式的事件宏相比,epoll模式新增了一个事件宏EPOLLET,即边缘触发模式(Edge Trigger,ET),我们称默认的模式为水平触发模式(Level Trigger,LT)。这两种模式的区别在于:

  1. LT:对于水平触发模式,一个事件只要有,就会一直触发;
  2. ET:对于边缘触发模式,在一个事件从无到有时才会触发。

理解:fd 上有数据的状态认为是高电平状态,将没有数据的状态认为是低电平状态,将fd可写状态认为是高电平状态,将fd不可写状态认为是低电平状态。

  1. LT:水平模式的触发条件:①低电平→高电平;②处于高电平状态。
  2. ET:边缘模式的触发条件:低电平→高电平。

2.4 epoll 实例

2.4.1 server 实例

server epoll实例
/**
 * @file testepoll.cpp
 * @author your name (you@domain.com)
 * @brief
 * @version 0.1
 * @date 2022-03-23
 *
 * @copyright Copyright (c) 2022
 *
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>

int main()
{
    // 创建一个监听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
        std::cout << "create listen socket error" << std::endl;
        return -1;
    }

    // 设置重用IP地址和端口号
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));

    // 将监听socket 设置为非阻塞的
    int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) {
        close(listenfd);
        std::cout << "set listenfd to nonblock error" << std::endl;
        return -1;
    }

    // 初始化服务器的地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);

    if (bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1) {
        std::cout << "bind listen socket error." << std::endl;
        close(listenfd);
        return -1;
    }

    // 启动监听
    if (listen(listenfd, SOMAXCONN) == -1) {
        std::cout << "listen error." << std::endl;
        close(listenfd);
        return -1;
    }

    // 创建 epollfd
    int epollfd = epoll_create(1);
    if (epollfd == -1) {
        std::cout << "create epollfd error." << std::endl;
        close(listenfd);
        return -1;
    }

    epoll_event listen_fd_event;
    listen_fd_event.data.fd = listenfd;
    listen_fd_event.events = EPOLLIN;
    // 若取消注释掉这一行,则使用ET模式
    // listen_fd_event.events |= EPOLLET;

    // 将监听socket绑定到epollfd上
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1) {
        std::cout << "epoll_ctl error" << std::endl;
        close(listenfd);
        return -1;
    }

    int n;
    while(true) {
        epoll_event epoll_events[1024];
        n = epoll_wait(epollfd, epoll_events, 1024, 1000);
        if (n < 0) {
            // 被信号中断
            if(errno == EINTR) {
                continue;
            }
            // 出错,退出
            break;
        } else if (n == 0) {
            // 超时, 继续
            continue;
        }

        for(size_t i = 0; i < n; i++) {
            // 事件可读
            if (epoll_events[i].events & EPOLLIN) {
                if (epoll_events[i].data.fd == listenfd) {
                    // 监听socket,接受新连接
                    struct sockaddr_in clientaddr;
                    socklen_t clientaddrlen = sizeof(clientaddr);
                    int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen);
                    if (clientfd != -1) {
                        int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
                        int newSocketFlag = oldSocketFlag | O_NONBLOCK;
                        if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) {
                            std::cout << "set clientfd to nonblocking error." << std::endl;
                            close(clientfd);
                        } else {
                            epoll_event client_fd_event;
                            client_fd_event.data.fd = clientfd;
                            client_fd_event.events = EPOLLIN;
                            // 若取消注释掉这一行,则使用ET模式
                            // listen_fd_event.events |= EPOLLET;
                            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1) {
                                std::cout << "new client accepted, clientfd:" << clientfd << std::endl;
                            } else {
                                std::cout << "add client fd to epollfd error." << std::endl;
                                close(clientfd);
                            }
                        }
                    }
                } else {
                    std::cout << "client fd:" << epoll_events[i].data.fd << " recv data." << std::endl;
                    // 普通clientfd
                    char ch;
                    // 每次只接收一个字符
                    int m = recv(epoll_events[i].data.fd, &ch, 1, 0);
                    if (m == 0) {
                        // 对端关闭了连接,从epollfd上移除clientfd
                        if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) {
                            std::cout << "client disconnected, clientfd:" << epoll_events[i].data.fd << std::endl;
                        }
                        close(epoll_events[i].data.fd);
                    } else if (m < 0) {
                        // 出错
                        if( errno != EWOULDBLOCK && errno != EINTR) {
                            if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) {
                                std::cout << "client disconnected, clientfd:" << epoll_events[i].data.fd << std::endl;
                            }
                            close(epoll_events[i].data.fd);
                        }
                    } else {
                        // 正常收到数据
                        std::cout << "recv from client:" << epoll_events[i].data.fd << " , " << ch <<std::endl;
                    }
                }
            } else if (epoll_events[i].events & EPOLLERR) {
                // TODO: 暂不处理
            }
        }
    }
    close(listenfd);
    return 0;
}

3 socket对端关闭

socket对端关闭判断条件:

  • recv 收到对端的数据为0
  • recv 收到对端的数据小于0:判断 errno != EWOULDBLOCK && erno != EINTR 为true

注意如果发送0数据,实际对端是收不到任何数据,即不触发recv

原文链接: https://www.cnblogs.com/lihaihui1991/p/15685985.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    Linux socket通信-- poll和epoll

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/400315

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月14日 下午2:04
下一篇 2023年4月14日 下午2:04

相关推荐