TCP/IP网络编程_基于Linux的编程_第17章优于select的epoll

在这里插入图片描述

17.1 epoll 理解及应用

select 复用方法其实由来已久, 因此, 利用该技术后, 无论如何优化程序性能也无法同时接入上百个客户端(当然, 硬件性能不同, 差别也很大). 这种select 方式并不适合以Web服务端开发为主流开发环境, 所以要学习Linux平台下的epoll.

基于 select 的 I/O 复用技术速度慢的原因

第12章曾经实现基于select 的I/O 复用服务器端, 很容易从代码分析出不合理的设计, 最主要的两点如下.
在这里插入图片描述上述两点可以从第12章示例echo_selectserv.c 的第45, 49行以及第54行代码得到确认. 调用 select 函数后, 并不是把发送变化的文件描述符单独集中在一起, 而是通过观察作为监视对象的 fd_set 变量的变化, 找出发生变化的文件描述符(示例echo_selectserv.c 的第54行, 56行), 因此无法避免针对所有监视对象的循环语句, 而且, 作为监视对象的fd_set 变量会发生变化, 所以调用select 函数前复制并保存原有信息(参考echo_selectserv.c 的第45行), 并在每次调用 select 函数时传递新的监视对象.

各位认为那些因素是提高性能的更大障碍? 是调用 select 函数后常见的针对所有文件描述符对象的循环语句? 还是每次需要传递监视对象信息?

只看代码的话很容易认为是循环. 但相比于循环语句, 更大的障碍的每次传递监视对象信息. 因为传递监视对象信息具有如下含义:
在这里插入图片描述
应用程序向操作系统传递数据将对程序造成很大的负担, 而且无法通过优化代码解决, 因此将成为性能上的致命弱点.

在这里插入图片描述
有些函数不需要操作系统的帮助就能完成功能, 而有些则必须借助操作系统. 假设各位定义了四则运算符相关函数, 此时无需操作系统的帮助. 但select 函数与文件描述符有关, 更准确地说, 是监视套接字变化的函数. 而套接字是由操作系统管理的, 所以select 函数绝对需要借助于操作系统才能完成功能. select 函数的这一点缺点可以通过如下方式你补:
在这里插入图片描述这样就无需每次调用select 函数时都向操作系统传递监视对象信息, 但前提是操作系统支持这种处理方式(每种操作系统支持的程度和方式存在差异). Linux 的支持方式是 epoll, Windows 的支持方式是 IOCP.

select 也有优点

知道这些内容后, 有些人可能对 select 函数感到失望, 但大家应当掌握 select 函数. 本章的 epoll 方式只在Linux 下提供支持, 也就是说, 改进的I/O 复用模型不具有兼容性. 相反, 大部分操作系统都支持 select 函数. 只要满足或要求如下两个条件, 即便在Linux 平台也不应拘泥于 epoll.
在这里插入图片描述
实际并不存在适用于所有情况的模型. 各位应理解好各种模型的优缺点, 并具备合理运用这些模型的能力.

实现 epoll 时必要的函数和结构体

能够克服 select 函数缺点的 epoll 函数具有如下优点, 这些优点正好与之前的 select 含数缺点相反.
在这里插入图片描述下面介绍 epoll 服务器端实现中需要的3个函数, 希望各位结合 epoll 函数的优点理解这些函数的功能.
在这里插入图片描述
select 方式中为了保存监视对象文件描述符, 直接声明了 fd_set 变量. 但 epoll 方式下由操作系统负责保存监视对象文件描述符, 因此需要向操作系统请求创建保护文件描述符的空间, 此时使用的函数就是epoll_create.

此外, 为了添加和删除监视对象文件描述符, select 方式中需要FD_SET, FD_CLR函数. 但在 epoll 方式中, 通过epoll_ctl 函数请求操作系统完成. 最后, select 方式下调用 select 函数等待文件描述符的变化, 而 epoll 中调用epoll_wait 函数. 还有, select 方式中通过fd_set 变量查看对象的状态变化(事件发生与否), 而epoll 方式中通过如下结构体 epoll_event 将发生变化的(发生事件的)文件描述符单独集中到一起.
在这里插入图片描述声明足够大的 epoll_event 结构体数组后, 传递给 epoll_wait 函数时, 发生变化的文件描述符信息将被填入该数组. 因此, 无需像 select 函数那样针对所有文件描述符进行循环.

以上就是epoll 中需要的函数和结构体. 实际上, 只要有 select 程序的编写经验, epoll 程序的编写就并不难. 接下来给出这些函数的详细的说明.

epoll_create

epoll 是从 Linux 的2.5.44 版内核(操作系统的核心模块)开始引入的, 所以使用 epoll 前需要验证 Linux 内核版本. 但各位使用的 Linux 内核都是 2.6以上版本, 所以这部分可以忽略. 若有人怀疑自己的版本过低, 可以通过如下命令验证:
在这里插入图片描述
下面观察 epoll_create 函数
在这里插入图片描述
调用 epoll_create 函数时创建的文件描述符保存空间称为 “epoll例程”, 但有些情况名称不同, 需要稍加注意. 通过参数size传递的值决定epoll例程的大小, 但该值只是向操作系统提的建议. 换言之, size 并非决定epoll例程的大小, 而仅供操作系统参考.
在这里插入图片描述
epoll_create 函数创建的资源与套接字相同, 也由操作系统管理. 因此, 该函数和创建套接字的情况相同, 也会返回文件描述符. 也就是说, 该函数返回的文件描述符主要用于区分epoll 例程. 需要终止时, 与其他文件描述符相同, 也要调用 close 函数.

epoll_ctl

生成epoll 后, 应在其内部注册监视对象文件描述符, 此时使用 epoll_ctl 函数.
在这里插入图片描述
在这里插入图片描述

与其他 epoll 函数相比, 该函数多少有些复杂, 但通过调用语句就很容易理解. 假设按照如下形式调用 epoll_ctl 函数:
在这里插入图片描述
第二个参数 EPOLL_CTL_ADD 意味着 ''添加", 因此上述语句具有如下含义:
在这里插入图片描述
再介绍一个调用语句.
在这里插入图片描述
上述语句中第二个参数 EPOLL_CTL_DEL 指 “删除”, 因此该语句具有如下含义:
在这里插入图片描述

从上述调用语句中可以看到, 从监视对象中删除时, 不需要监视类型(事件信息), 因此向第四个参数传递NULL. 接下来介绍可以向 epoll_ctl 第二个参数传递的常量及含义.
在这里插入图片描述

关于 EPOLL_CTL_MOD 常量稍后讲解(即使我不讲大家也自然能明白). 如前所述, 向epoll_ctl 的第二个参数传递EPOLL_CTL_DEL时, 应同时向第四个参数传递NULL. 但 Linux 2.6.9 之前的内核不允许传递NULL, 虽然被忽略掉, 但也应传递 epoll_event 结构体变量的地址值(本书示例将传递NULL). 其实这是Bug, 但也没必要因此怀疑epoll_event的功能, 因为我们使用的标准函数中也存在Bug.

下面讲解各位不太熟悉的epoll_ctl 函数的第四参数, 其类型是之前讲过的 epoll_event 结构体指针.
在这里插入图片描述
当然! 如前所述, epoll_event 结构体用于保存发生事件的文件描述符集合. 但也可以在 epoll 例程中注册文件描述符时, 用于注册关注的事件. 函数中epoll_event 结构体的定义并不显眼, 因此通过调用语句该结构体在 epoll_ctl 函数中的应用.
在这里插入图片描述
上述代码将 sockfd 注册到 epoll 例程 epfd 中, 并在需要读取数据的情况下产生相应事件. 接下来给出 epoll_event 的成员 events 中的可以保存的常量及所指的事件类型.
在这里插入图片描述
可以通过位或运行同时传递多个上述参数. 关于 “边缘触发” 稍后将单独讲解, 目前只需记住 EPOLLIN 即可.

epoll_wait

最后介绍与 select 函数对应的epoll_wait 函数, epoll 相关函数中默认最后调用该函数.
在这里插入图片描述
该函数的调用方式如下. 需要注意的是, 第二个参数所指缓冲需要动态分配.
在这里插入图片描述
调用函数后, 返回发生事件的文件描述符数, 同时在第二个参数指向的缓存中保存发生事件的文件描述符集合. 因此, 无需像 select 那样插入针对所有文件描述符的循环.

基于 epoll 的回声服务器端

以上就是基于 epoll 技术实现服务器端的所有理论说明, 接下来给出基于 epoll 的回声服务器端实例. 我通过更改第12章的echo_selectserv.c 实现了该实例. 当然, 从头开始写也与下面给出的内容类似. 但通过更改 select 示例理解二者差异将更有利于学习.
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50

void error_hangling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;

    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_hangling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_hangling("listen() error");
    }

    epfd = epoll_create(EPOLL_SIZE);
    ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    event.events = EPOLLIN;
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    while (1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }

        for (i=0; i<event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock)
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d \n", clnt_sock);
            }
            else
            {
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0)
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d \n", ep_events[i].data.fd);
                }
                else
                {
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }

    close(serv_sock);
    close(epfd);
    return 0;
}

void error_hangling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

之前解析过关键代码, 而且程序结构与 select 方式没有区别, 故省略代码说明. 如果有些地方难以理解, 说明未掌握本章之前的内容和 select 模型, 建议复习. 结合我的说明和 select 示例 理解上述代码也是一种很好的学习方式. 上述示例可以结合任意回声客户端运行, 而且运行结果与其他回声服务器/客户端没有差别, 故省略.

17.2 条件触发和边缘触发

有些人学习 epoll 时往往无法正确区分条件触发(Level Trigger)和边缘触发(Edge Trigger),但只有理解了二者区别才算完整掌握 epoll.

条件触发和边缘触发的区别在于发生事件的时间点

首先给出示例帮助各位理解条件触发和边缘触发. 观察如下对话, 可以通过对话内容理解条件触发事件的特点.
在这里插入图片描述
在这里插入图片描述
从上述对话可以看出, 儿子从压岁钱开始一直向妈妈报告, 这是条件触发的原理. 如果将上述对话中的儿子(儿子的钱包)换成输入缓冲, 压岁钱换成输入数据, 儿子的报告换成事件, 则可以发现条件触发的特性. 我将其整理如下:
在这里插入图片描述
例如, 服务器端输入缓冲收到50字节的数据时, 服务器端操作系统将通知该事件(注册到发生的文件描述符). 但服务器端读取20字节后还剩30字节的情况下, 仍会注册事件. 也就是说, 条件触发方式中, 只要输入缓冲中还剩有的数据, 就将以事件方式再次注册. 接下来通过如下对话介绍边缘的事件特性.
在这里插入图片描述
从上述对话可以看出, 边缘触发中输入缓冲收到数据时仅注册1次改事件. 即使输入缓冲中还留有数据, 也不会再进行注册.

掌握条件触发的事件特性

接下来通过代码了解条件触发的事件注册方式. 下列代码是稍微修改之前的 echo_epollserv.c 示例得到的. epoll 默认以条件方式工作, 因此可以通过该示例验证条件触发的特性.
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4
#define EPOLL_SIZE 50

void error_hangling(char *buf);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;

    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_hangling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_hangling("listen() error");
    }

    epfd = epoll_create(EPOLL_SIZE);
    ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    event.events = EPOLLIN;
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    while (1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }

        puts("return epoll_wait");
        for (i=0; i<event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock)
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d \n", clnt_sock);
            }
            else
            {
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0)
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d \n", ep_events[i].data.fd);
                }
                else
                {
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }

    close(serv_sock);
    close(epfd);
    return 0;
}

void error_hangling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

上述示例与之前的 echo_epollserv.c 之间的差异如下.
在这里插入图片描述
减少缓冲大小是为了阻止服务器端一次性读取数据. 换言之, 调用 read 函数后, 输入缓冲中仍有数据需要读取. 而且会因此注册新的事件并从 epoll_wait 函数返回时将循环输出"return epoll_wati" 字符串. 前提是条件触发的工作方式与我的描述一致. 接下来观察运行结果. 该程序同样可以结合第4章的 echo_client.c 运行.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从运行结果中可以看出, 每当收到客户端数据时, 都会注册该事件, 并因此多次调用 epoll_wait 函数. 下面将上述示例改成边缘触发方式, 需要做一些额外的工作. 但我希望通过最小的改动验证边缘触发模型的事件注册方式. 将上述示例的第57行改如下形式运行服务器端和客户端(不会单独提供这方面的源码, 需要各位自行更改):
在这里插入图片描述
更改后可以验证如下事实:
在这里插入图片描述
虽然可以验证上述事实, 但客户端运行时将发生错误. 大家是否遇到了这种问题? 能否自行分析原因? 虽然目前不必对此感到困惑, 但如果理解了边缘触发的特性, 应该可以分析出错误原因.
在这里插入图片描述

边缘触发的服务器端实现中必知的两点

下面讲解边缘触发服务器端的实现方法. 在此之前, 我希望说明如下2点, 这些是实现边缘触发的必知内容.
在这里插入图片描述
Linux 的套接字相关函数一般通过返回-1通知发生错误. 虽然知道发生了错误, 但仅凭这些内容无法得知产生错误的原因. 因此, 为了在发生错误时提供额外的信息, Linux声明如下全局变量:
在这里插入图片描述
为了访问该变量, 需要引入error.h 头文件, 因为此头文件中有上述变量的 extern 声明. 另外, 每种函数发生错误时, 保存到 errno 变量中的值都不同, 没必要记住所有可能的值. 学习每种函数的过程中逐一掌握, 并能在必要时参考即可. 本节只介绍如下类型的错误:
在这里插入图片描述
稍后通过示例给出 errno 的使用方法. 下面讲解将套接字改为非阻塞的方法. Linux提供更改或读取文件属性的如下方法(曾在第13章使用过).
在这里插入图片描述
从上述声明中可以看到, fcntl 具有可变参数的形式, 如果向第二个参数传递 F_GETFL, 可以获得第一个参数所指的文件描述符属性(int 型). 反之, 如果传递 F_SETFL, 可以更改文件描述符属性. 若希望将文件(套接字)改为非阻塞模式, 需要如下2条语句.
在这里插入图片描述
通过第一条语句获得之前设置的属性信息, 通过第二条语句在此基础上添加非阻塞 O_NONBLOCK 标志. 调用read & write 函数时, 无论是否存在数据, 都会形成非阻塞文件(套接字). fcntl 函数的适用范围很广, 各位既可以在学习系统编程时一次性总结所有适用情况, 也可以每次需要时逐一掌握.

实现边缘触发的回声服务器端

之前介绍读取错误原因的方法和非阻塞模式的套接字创建方法, 原因在于二者都与边缘触发的服务器端实现有密切联系. 首先说明为何需要通过 errno 确认错误原因.
在这里插入图片描述
就因为这种特点, 一旦发生输入相关事件, 就应该读取输入缓冲中的全部数据. 因此需要验证输入缓冲是否为空.
在这里插入图片描述
既然如此, 为何还需要将套接字变成非阻塞模式? 边缘触发方式下, 以阻塞方式工作的 read & write 函数有可能引起服务器端的长时间停顿. 因此, 边缘触发方式中一定采用非阻塞 read & write 函数. 接下来给出边缘方式工作的回声服务器端实例.
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

#define BUF_SIZE 4 /* 为了验证边缘的工作方式, 将缓冲设置为4字节 */
#define EPOLL_SIZE 50

void setnonblockingmode(int fd);
void error_handling(char *buf);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    int epfd, event_cnt;
    if (argc != 2)
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
    {
        error_handling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    epfd = epoll_create(EPOLL_SIZE);
    ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    setnonblockingmode(serv_sock);
    event.events = EPOLLIN;
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    while (1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait() error");
            break;
        }

        puts("return epoll_wait"); /* 67: 为观察事件发生数而添加的输出字符串的语句 */
        for (i=0; i<event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock)
            {
                adr_sz = sizeof(clnt_adr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                setnonblockingmode(clnt_sock); /* 74 75: 第74行将 eccept函数创建的套接字改为非阻塞模式. 第75行向EPOLLIN 添加 EPOLLET 标志, 将套接字事件注册方式改为边缘触发. */
                event.events = EPOLLIN|EPOLLET;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d \n", clnt_sock);
            }
            else
            {
                while (1)
                {
                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0)
                    {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                        close(ep_events[i].data.fd);
                        printf("close client: %d \n", ep_events[i].data.fd);
                        break;
                    }
                    else if (str_len < 0)
                    {
                        if (errno == EAGAIN)
                        {
                            break;
                        }
                    }
                    else
                    {
                        write(ep_events[i].data.fd, buf, str_len);
                    }
                }
            }
        }
    }

    close(serv_sock);
    close(epfd);

    return 0;
}

void setnonblockingmode(int fd)
{
    int flag = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}

void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

这里有bug, 等我以后功力到之后, 再回来, 书上的运行结果.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上述运行结果中需要注意的是, 客户端发送消息次数和服务器端epoll_wait 函数次数. 客户端从请求连接到断开共发送5次数据, 服务器端也相应产生5个事件.

条件触发和边缘触发 优 劣

我们理解从理论和代码的角度充分理解了条件触发和边缘触发, 但仅凭这些还无法理解边缘触发相对于条件触发的优点. 边缘触发方式下可以做到如下这点:
在这里插入图片描述
虽然比较简单, 但非常准而有力地说明了边缘触发的优点. 关于这句话的含义, 大家以后开发不同类型的程序是会会有深入的理解. 现阶段给出如下情景帮助大家理解, 如图17-1所示.
在这里插入图片描述
图 17-1 的运行流程如下.
在这里插入图片描述
为完成该过程, 若能按如下流程运行程序, 服务器端的实现并不难.
在这里插入图片描述
但现实中可能频繁出现如下这些情况, 换言之, 如下情况更符合实际.

在这里插入图片描述
在这里插入图片描述
因此, 即使输入缓冲数据(注册相应事件), 服务器端也能决定读取和处理这些数据的时间点, 这样就给服务器端的实现带来巨大的灵活性.
在这里插入图片描述
并非不可能. 但在输入缓冲收到数据的情况下, 如果不读取(延迟处理), 则每次调用epoll_wait 函数 时都会产生相应事件. 而且事件数也会累加, 服务器端能承受吗? 这在现实中是不可能的(本身并不合理, 因此是更本不可能的事).

条件触发和边缘触发的区别主要应该从服务器端实现模型的角度谈论, 因此希望各位不要提下面这种问题. 如果理解了之前的讲解, 应该有更好的提问.
在这里插入图片描述
从实现模型的角度看, 边缘触发更有可能带来高性能, 但不能简单地认为 “只要使用边缘触发就一点能提高速度”.

结语:

你可以下面这个网站下载这本书<TCP/IP网络编程>
https://www.jiumodiary.com/

时间: 2020-06-09

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值