• Linux网络编程6——poll和epoll


    学习视频链接

    04-poll函数实现服务器_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1iJ411S7UA?p=68&spm_id_from=333.1007.top_right_bar_window_history.content.click

    目录

    一、poll 函数

    1.1 poll函数原型

    1.2 流程

    1.3 实现

    1.4 优点和缺点

    二、epell 函数

    2.1 简介

    2.2 文件描述符上限

    2.3 API 

    2.4 代码

    三、epoll 事件模型

    3.1 分类

    3.2 查看他们之间的区别

    3.3 网络中的两种模式


    select 出来的比较早,其缺点是监听散乱的文件描述符效率会低一点。

    所以使用 poll,但是 poll 效率没改进多少,所以又改进为 epoll

    一、poll 函数

    1.1 poll函数原型

    9f351eee71034ec79031273d394e2e5d.png

    1.2 流程

    1da650b0366d4d6eb05b460d2a82c01f.png

    循环里面执行的就是 poll 监听 lfd 和所有的 cfd,如果是 lfd 就执行 accept,如果是 cfd 就执行 read/write

    1.3 实现

    1、代码讲解

    进入死循环前,数组是一个这样的状态

    016a3a75096e49138f3b789e86f9abf4.png

    if(client[0].revents & POLLIN) 是用来处理 listenfd

    411dfbad08be4ff2a0ec47c36386590c.png

    如果有新的连接,就会去在数组里面找空闲的位置 for(i=1;i

    5d59d6fa043646ea856b82bc24e16859.png

    设置完成后就跳出循环,或者遍历到 1024 后跳出循环。如果遍历到 1024 会报错,没有便利到 1024 就会在刚刚修改 fd 的位置,再设置 events 等于 POLLIN 就完成了

    for 循环是用来处理 cfd

    2586ebb77e354351849a44484ed55a1d.png

    上来先做一个异常的处理,保证代码健壮性 

    if(Client[].revents & POLLIN) 为真,表示读事件满足了,就判断读的返回值

    read 的返回值有以下这些 

    6a56ccea98484ffd9e1402e40d58db31.png

    如果返回值等于 -1,其实应该依次处理这些内容,但是函数中只对 ECONNRESET 这种情况进行了处理,其他的直接打印出错

    如果返回值等于 0,就停用对应的文件描述符

    如果返回值大于 0,就说明读到了数据,就进行处理

    2、代码(代码有BUG,后面有机会再改)

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #define MAXLINE 80
    14. #define SERV_PORT 8000
    15. #define OPEN_MAX 1024
    16. void perr_exit(const char* str)
    17. {
    18. perror(str);
    19. exit(1);
    20. }
    21. int main(void)
    22. {
    23. int i, j, maxi, listenfd, connfd, sockfd, nready, opt;
    24. ssize_t n; // 接受poll返回值,记录满足监听事件的fd个数
    25. char buf[MAXLINE], str[INET_ADDRSTRLEN];
    26. socklen_t clilen;
    27. struct pollfd client[OPEN_MAX];
    28. struct sockaddr_in cliaddr, servaddr;
    29. listenfd = socket(AF_INET, SOCK_STREAM, 0);
    30. opt = 1;
    31. setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    32. bzero(&servaddr, sizeof(servaddr));
    33. servaddr.sin_family = AF_INET;
    34. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    35. servaddr.sin_port = htons(SERV_PORT);
    36. bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    37. listen(listenfd, 128);
    38. client[0].fd = listenfd; // 要监听的第一个文件描述符存入client[0]
    39. client[0].events = POLLIN; // listenfd监听普通读事件
    40. for (i = 1; i < OPEN_MAX; i++) {
    41. client[i].fd = -1; // 用-1初始化client[]里剩下的元素
    42. }
    43. maxi = 0;
    44. for ( ; ; ) {
    45. nready = poll(client, maxi + 1, -1); // 阻塞监听是否有客户端链接请求
    46. if (client[0].revents & POLLIN) { // listenfd有读事件就绪
    47. clilen = sizeof(cliaddr);
    48. connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); // 接受客户端请求accept不会阻塞
    49. printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
    50. for (i = 1; i < OPEN_MAX; i++) {
    51. if (client[i].fd < 0) {
    52. client[i].fd = connfd; // 找到client[]中空闲的位置,存放accept返回的connfd
    53. break;
    54. }
    55. }
    56. if (i = OPEN_MAX) { // 达到了最大客户端数
    57. perr_exit("too many clients");
    58. }
    59. client[i].events = POLLIN; // 设置刚刚返回的connfd,监控读事件
    60. if (i > maxi) {
    61. maxi = i; // 更新client[]中最大元素下标
    62. }
    63. if (--nready <= 0) {
    64. continue; // 没有更多就绪事件时,继续回到poll阻塞
    65. }
    66. }
    67. for (i = 1; i <= maxi; i++) { // 前面的if没满足,说明没有listenfd满足,检测client[]看是哪个connfd就绪
    68. if ((sockfd = client[i].fd) < 0) {
    69. continue;
    70. }
    71. if (client[i].revents & POLLIN) {
    72. if ((n = read(sockfd, buf, MAXLINE)) < 0) {
    73. if(errno == ECONNRESET) { // 收到RET标志
    74. printf("client[%d] aborted connection\n", i);
    75. close(sockfd);
    76. client[i].fd = -1; // poll中不监控该文件描述符,直接置为-1即可,不用像select中那样移除
    77. }
    78. else {
    79. perr_exit("read error");
    80. }
    81. }
    82. else if (n == 0) { // 说明客户端先关闭链接
    83. printf("client[%d] closed connection\n", i);
    84. close(sockfd);
    85. client[i].fd = -1;
    86. }
    87. else {
    88. for (j = 0; j < n; j++) {
    89. buf[j] = toupper(buf[j]);
    90. }
    91. write(sockfd, buf, n);
    92. }
    93. if (--nready <= 0) {
    94. break;
    95. }
    96. }
    97. }
    98. }
    99. return 0;
    100. }

    1.4 优点和缺点

    d2cce72edd694893ad66ed6104ca34a9.png

    二、epell 函数

    2.1 简介

    epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了

    目前 epoll 是 linux 大规模并发网络程序中的热门首选模型。

    epoll 除了提供 select/poll 那种 IO 事件的电平触发 (Level Triggered) 外,还提供了边沿触发 (Edge Triggered),这就使得用户空间程序有可能缓存 lO 状态,减少 epoll_wait/epoll_pwait 的调用,提高应用程序效率。

    2.2 文件描述符上限

    1、可以使用 cat 命令查看一个进程可以打开的 socket 描述符上限。

    cat /proc/sys/fs/file-max (当前计算机所能打开的最大文件个数,受硬件影响)

    415bb479afca473f92aa95a57ccc7ea9.png

    ulimit -a (当前用户下的进程,默认打开文件描述符个数)

    e440fc98566a40759eb0bc34e2eb68d4.png

    2、如有需要,可以通过修改配置文件的方式修改该上限值。

    sudo vi /etc/security/limits.conf

    在文件尾部写入以下配置,soft 软限制,hard 硬限制。如下图所示。

    * soft nofile 65536 

    * hard nofile 100000

    d0bcfde358694175b41488fa580dfe9c.png

    在这里修改不能超过 hard

     a04800774ae54c79871253e970d42a3a.png

    2.3 API 

    前面只要一个函数就可以了,epoll 需要三个函数

    第一个函数是 poll_create

    6d560e34beba4a5cb15caf8ce662ad58.png

    第二个函数是 epoll_ctl 

    b793756024a7489486001e0944fa86f9.png

    第三个函数是 epoll_wait

    a8a4ccc7748f462685f607dc2e8b46eb.png

    数组中存储就是连接上的内容

    cf832e9b29144717925646502b154b60.png

    2.4 代码

    首先代码创建一个根结点,再创建 lfd 结点,用于监听,把 lfd 插入到根结点的子节点上。根据epoll_wait 返回的数字,循环遍历数组,处理读事件。判断读事件是否是请求联立连接,如果是请求连接的,就使用 epoll_ctl 插入结点,其他的就是数据读写事件。具体代码如下

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #include
    14. #define MAXLINE 8192
    15. #define SERV_PORT 8000
    16. #define OPEN_MAX 5000
    17. void perr_exit(const char* str)
    18. {
    19. perror(str);
    20. exit(1);
    21. }
    22. int main(void)
    23. {
    24. int i, listenfd, connfd, sockfd, n;
    25. int num = 0;
    26. ssize_t nready, efd, res;
    27. char buf[MAXLINE], str[INET_ADDRSTRLEN];
    28. socklen_t clilen;
    29. struct sockaddr_in cliaddr, servaddr;
    30. struct epoll_event tep, ep[OPEN_MAX];
    31. listenfd = socket(AF_INET, SOCK_STREAM, 0);
    32. int opt = 1;
    33. setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 端口复用
    34. bzero(&servaddr, sizeof(servaddr));
    35. servaddr.sin_family = AF_INET;
    36. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    37. servaddr.sin_port = htons(SERV_PORT);
    38. bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    39. listen(listenfd, 20);
    40. efd = epoll_create(OPEN_MAX); // 创建epoll模型,efd指向红黑书根节点
    41. if (efd == -1) {
    42. perr_exit("epoll_create error");
    43. }
    44. // 指定lfd的监听事件为 读
    45. tep.events = EPOLLIN;
    46. tep.data.fd = listenfd;
    47. res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); // 将lfd及对应的结构体设置到树上,efd可找到该树
    48. if(res == -1) {
    49. perr_exit("epoll_ctl error");
    50. }
    51. for ( ; ; ) {
    52. // epoll为server阻塞监听事件,ep为struct epoll_event类型数组,OPEN_MAX为数组容量,-1表永久阻塞
    53. nready = epoll_wait(efd, ep, OPEN_MAX, -1);
    54. if (nready == -1) {
    55. perr_exit("epoll_wait error");
    56. }
    57. for (i = 0; i < nready; i++) {
    58. if (!(ep[i].events & EPOLLIN)) { // 如果并不是 读 事件,就继续循环
    59. continue;
    60. }
    61. if (ep[i].data.fd == listenfd) { // 判断满足事件的fd是不是lfd
    62. clilen = sizeof(cliaddr);
    63. connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); // 接受连接
    64. printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
    65. printf("cdf %d--client %d\n", connfd, ++num);
    66. tep.events = EPOLLIN;
    67. tep.data.fd = connfd;
    68. res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); // 加入红黑树
    69. if (res == -1) {
    70. perr_exit("epoll_ctl error");
    71. }
    72. }
    73. else { // 不是lfd
    74. sockfd = ep[i].data.fd;
    75. n = read(sockfd, buf, MAXLINE);
    76. if (n == 0) { // 读到0,说明客户端关闭连接
    77. res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); // 将该文件描述符从红黑树摘除
    78. if (res == -1) {
    79. perr_exit("epoll_stl error");
    80. }
    81. close(sockfd); // 关闭与该客户端的连接
    82. printf("Client[%d] closed connection\n", sockfd);
    83. }
    84. else if (n < 0) { // 出错
    85. perror("read n < 0 error");
    86. res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
    87. close(sockfd);
    88. }
    89. else {
    90. for (i = 0; i < n; i++) { // 实际读到的字节数
    91. buf[i] = toupper(buf[i]); // 转大写,写会给客户端
    92. }
    93. write(STDOUT_FILENO, buf, n);
    94. write(sockfd, buf, n);
    95. }
    96. }
    97. }
    98. }
    99. close(listenfd);
    100. close(efd);
    101. return 0;
    102. }

    三、epoll 事件模型

    3.1 分类

    ET模式:边沿触发

    缓冲区剩余未读尽的数据不会导致 epoll_wait 返回

    event.events = EPOLLIN | EPOLLET;

    LT模式:水平触发 —— 默认采用模式

    缓冲区剩余未读尽的数据会导致 epoll_wait 返回

    event.events = EPOLLIN

    3.2 查看他们之间的区别

    1、ET模式每 5 秒写一次,缓冲区中还有数据,但是不会去读数据,只有等到下次有10个字符数据来的时候才会继续读缓冲区后面 5 个字符的数据

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #define MAXLINE 10
    7. int main(void)
    8. {
    9. int efd, i;
    10. int pfd[2];
    11. pid_t pid;
    12. char buf[MAXLINE], ch = 'a';
    13. pipe(pfd);
    14. pid = fork();
    15. if(pid == 0) { // 子进程 写
    16. close(pfd[0]);
    17. while (1) {
    18. for (i = 0; i < MAXLINE / 2; i++) {
    19. buf[i] = ch;
    20. }
    21. buf[i-1] = '\n';
    22. ch++;
    23. for ( ; i < MAXLINE; i++) {
    24. buf[i] = ch;
    25. }
    26. buf[i-1] = '\n';
    27. ch++;
    28. write(pfd[1], buf, sizeof(buf));
    29. sleep(5);
    30. }
    31. close(pfd[1]);
    32. }
    33. else if (pid > 0) { // 父进程 读
    34. struct epoll_event event;
    35. struct epoll_event resevent[10]; // epoll_wait就绪返回event
    36. int res, len;
    37. close(pfd[1]);
    38. efd = epoll_create(10);
    39. event.events = EPOLLIN | EPOLLET; // ET边沿触发
    40. //event.events = EPOLLIN; // LT水平触发(默认)
    41. event.data.fd = pfd[0];
    42. epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
    43. while (1) {
    44. res = epoll_wait(efd, resevent, 10, -1);
    45. printf("res %d\n", res);
    46. if (resevent[0].data.fd == pfd[0]) {
    47. len = read(pfd[0], buf, MAXLINE / 2);
    48. write(STDOUT_FILENO, buf, len);
    49. }
    50. }
    51. close(pfd[0]);
    52. }
    53. return 0;
    54. }

    2、LT模式只要缓冲区中还有数据,就回去读数据,每次读 5 个字节的数据,一次性连续读两次。

    1. //event.events = EPOLLIN | EPOLLET; // ET边沿触发
    2. event.events = EPOLLIN; // LT水平触发(默认)

    3.3 网络中的两种模式

    1. #include
    2. #include
    3. #include
    4. #include
    5. #define MAXLINE 10
    6. #define SERV_PORT 9000
    7. int main(void)
    8. {
    9. struct sockaddr_in servaddr;
    10. char buf[MAXLINE];
    11. int sockfd, i;
    12. char ch = 'a';
    13. sockfd = socket(AF_INET, SOCK_STREAM, 0);
    14. bzero(&servaddr, sizeof(servaddr));
    15. servaddr.sin_family = AF_INET;
    16. inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    17. servaddr.sin_port = htons(SERV_PORT);
    18. connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    19. while (1) {
    20. for (i = 0; i < MAXLINE / 2; i++) {
    21. buf[i] = ch;
    22. }
    23. buf[i-1] = '\n';
    24. ch++;
    25. for ( ; i < MAXLINE; i++) {
    26. buf[i] = ch;
    27. }
    28. buf[i-1] = '\n';
    29. ch++;
    30. write(sockfd, buf, sizeof(buf));
    31. sleep(5);
    32. }
    33. return 0;
    34. }
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #define MAXLINE 10
    11. #define SERV_PORT 9000
    12. int main(void)
    13. {
    14. struct sockaddr_in servaddr, cliaddr;
    15. socklen_t cliaddr_len;
    16. int listenfd, connfd;
    17. char buf[MAXLINE];
    18. char str[INET_ADDRSTRLEN];
    19. int efd;
    20. listenfd = socket(AF_INET, SOCK_STREAM, 0);
    21. bzero(&servaddr, sizeof(servaddr));
    22. servaddr.sin_family = AF_INET;
    23. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    24. servaddr.sin_port = htons(SERV_PORT);
    25. bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    26. listen(listenfd, 20);
    27. struct epoll_event event;
    28. struct epoll_event resevent[10];
    29. int res, len;
    30. efd = epoll_create(10);
    31. event.events = EPOLLIN | EPOLLET; // ET边沿触发
    32. //event.events = EPOLLIN; // 默认LT水平触发
    33. printf("Aceepting connections ...\n");
    34. cliaddr_len = sizeof(cliaddr);
    35. connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    36. printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
    37. event.data.fd = connfd;
    38. epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
    39. while (1) {
    40. res = epoll_wait(efd, resevent, 10, -1);
    41. printf("res %d\n", res);
    42. if (resevent[0].data.fd == connfd) {
    43. len = read(connfd, buf, MAXLINE / 2);
    44. write(STDOUT_FILENO, buf, len);
    45. }
    46. }
    47. return 0;
    48. }

    如下图,在 ET 模式中,也是每次客户端发送过来信息,服务端才执行一次写操作

    改成 LT 模式后,每次发送消息后,服务端先打印出 5 个字符,然后服务端再次触发 epoll_wait 返回,又会再次打印剩下的 5 个字符

    1. // event.events = EPOLLIN | EPOLLET; // ET边沿触发
    2. event.events = EPOLLIN; // 默认LT水平触发

    3.4 结论:

    epoll 的 ET 模式,高效模式,但是只支持非阻塞模式。

    struct epoll_event event;

    event.events = EPOLLIN | EPOLLET;

    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);

    int flg = fcntl(cfd, F_GETFL);

    flg |= O_ NONBLOCK;

    fcntl(cfd, F_SETFL, flg);

    优点:

    高效。突破1024文件描述符。

    缺点:

    不能跨平台。Linux.

  • 相关阅读:
    R语言Sys.Date函数获取当前日期、获取指定日期自1970年1月1日以来经过的天数(Julian Date)
    groovy在SpringBoot中的使用
    如何用个人电脑搭建一台本地服务器,并部署项目到服务器详细教程(Ubuntu镜像)
    经纬度坐标转换为工程坐标
    Java、Spring、Dubbo三者SPI机制原理与区别
    Spring和SpringBoot的区别
    Integer缓存到底有啥问题?
    .Net 对象生命周期由浅入深2(GC)
    Mysql 的分布式策略
    利器 | TestNG 与 Junit 对比,测试框架如何选择?
  • 原文地址:https://blog.csdn.net/HuanBianCheng27/article/details/126166492