TCP/IP网络编程 第十七章:优于select的epoll

这篇具有很好参考价值的文章主要介绍了TCP/IP网络编程 第十七章:优于select的epoll。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

epoll理解及应用

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

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

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


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

在代码层面上思考,很容易认为是循环。但相比于循环语句,更大的障碍是每次传递监视对象信息。因为传递对象信息具有如下含义:"每次调用select函数时向操作系统传递监视对象信息。"

应用程序向操作系统传递数据将对程序造成很大负担,而且无法通过优化代码解决,因此将成为性能上的致命缺点。

select函数的这一缺点可以通过如下方式弥补:“仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”

这样就无需每次调用select函数时都向操作系统传递监视对象信息,但前提是操作系统支持这种处理方式(每种操作系统支持的程度和方式存在差异)。Linux的支持方式是epoll,Windows的支持方式是IOCP。

select也有优点

知道这些内容后,有些人可能对select函数感到失望,但大家应当掌握select函数。本章的epoll方式只在Limux下提供支持,也就是说,改进的IO复用模型不具有兼容性。相反,大部分操作系统都支持select函数。只要满足或要求如下两个条件,即使在Linux平台也不应拘泥于epoll。
□服务器端接入者少
□程序应具有兼容性
实际并不存在适用于所有情况的模型。各位应理解好各种模型的优缺点。

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

能够克服select函数缺点的epoll函数具有如下优点,这些优点正好与之前的select函数缺点相反。
□无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。
□调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。

下面介绍epoll服务器端实现中需要的三个函数,希望各位结合epoll函数的优点理解这些函数的功能。

□epoll_create:创建保存epoll文件描述符的空间。

□epoll_ctl:像空间注册或注销文件描述符。

□epoll_wait:于select函数类似,等待文件描述符发生变化。

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将发生变化的(发生事件的)文件描述符单独集中到一起。

struct epoll_event{
     __uint32_t  events;
     epoll_data_t data;
}

     typedef union epoll_data{
           void *ptr;
           int fd;
           __uint32_t u32;
           __uint64_t u64;
     }epoll_data_t;

声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息被填入该数组。因此无需向select函数那样针对所有文件描述符进行循环。

epoll_create

epoll是从Linux2.5.44版内核开始引入的,所以使用epoll前需要验证Linux内核版本。下面仔细观察epoll_create函数。

#include<sys/epoll.h>
int epoll_create(int size);//成功时返回epoll文件描述符,失败时返回-1
    size   //epoll示例的大小

调用epoll_create函数时创建的文件描述符保存空间称为"epoll例程"。还有一个注意点,这个传入的size大小实际上并非用来决定epoll例程的大小,而仅供操作系统参考。

epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此,该函数和创建套接字的情况相同,也会返回文件描述符。需要终止时,与其他文件描述符相同,也要调用close函数。

epoll_ctl

#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//成功时返回0,失败时返回-1
     epfd    //用于注册监视对象epoll例程的文件描述符
     op      //用于指定监视对象的添加,删除或更改等操作 
     fd      //需要注册监视对象文件描述符
     event   //监视对象的事件类型

接下来是两个简单示例

epoll_ctl(A,EPOLL_CTL_ADD,B,C);
epoll_ctl(A,EPOLL_CTL_DEL,B,NULL);

第一句含义是:"epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件"

第二句含义是:"从epoll例程A中删除文件描述符B"

从上述调用语句中可以看到,从监视对象中删除时,不需要监视类型(事件信息),因此向第四个参数传递NULL。

接下来介绍可以向epoll_ctl第二个参数传递的常量及含义。
□ EPOLL_CTL_ADD:将文件描述符注册到epoll例程。

□ EPOLL_CTL DEL:从epoll例程中删除文件描述符。

□ EPOLL _CTL_MOD:更改注册的文件描述符的关注事件发生情况。

下面讲解各位不太熟悉的epoll_ctl函数的第四个参数,其类型是之前讲过的epoll_event结构体指针。如前所述,epoll_event结构体用于保存发生事件的文件描述符集合。但也可以在epoll例程中注册文件描述符时,用于注册关注的事件!(有两个功能)函数中epoll_event结构体的定义并不显眼,因此通过调用语句说明该结构体在epoll_ctl函数中的应用。

struct epoll_event event;
....
event.events=EPOLLIN;//事件种类
event.data.fd=sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
....

上述代码将sockfd注册到epoll例程epfd中,并在需要读取数据的情况下产生相应事件。接下来给出epoll_event的成员events中可以保存的常量及所指的事件类型。
□ EPOLLIN:需要读取数据的情况。
□ EPOLLOUT:输出缓冲为空,可以立即发送数据的情况。
□ EPOLLPRI:收到OOB数据的情况。
□ EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用。
□ EPOLLERR:发生错误的情况。
□ EPOLLET:以边缘触发的方式得到事件通知。
□ EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向
epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。

可以通过位或运算同时传递多个上述参数。关于“边缘触发”稍后将单独讲解,目前只需记住EPOLLIN即可。

epoll_wait

最后介绍与select函数对应的epoll_wait函数,epoll相关函数中默认最后调用该函数。

#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event*events,int maxevent,int timeout);
    epfd      //表示事件发生监视范围的epoll例程的文件描述符
    events    //保存发生事件的文件描述符集合的结构体地址
    maxevents //表示第二个参数中可以保存的最大事件数
    timeout   //以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件

该函数的调用方式如下。需要注意的是,第二个参数所指缓冲需要动态分配。

int event_cnt;
struct epoll_event* ep_events;
....
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
....
event_cnt=epoll_wait(epfd,ep_event,EPOLL_SIZE,-1);
....

调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。

基于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 100
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc,char *argv[]){
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    socklen_t addr_sz;
    int str_len,i;
    char buf[BUF_SIZE];

    struct epoll_event *ep_events;
    struct epoll_event event;
    in 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_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(argv[1]);

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-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)*EOLL_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){
                addr_sz=sizeof(clnt_addr);
                clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,sizeof(addr_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){  //传输完成
                    printf("close client: %d \n",ep_events[i].data.fd);
                    close(ep_events[i].data.fd);
                    epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
                 }
                 else{
                    write(ep_events[i].data.fd,buf,str_len);//回声
                 }
             }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}

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

上述代码的演示和之前章节中的select函数代码相似,可以结合之前章节中的代码思路进行理解。

条件触发和边缘触发

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

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

首先给出示例帮助各位理解条件触发和边缘触发。观察如下对话,可以通过对话内容理解条件触发事件的特点。

儿子:"妈妈,这次期末考试我全部都是A。"

妈妈:"好棒!"

儿子:"我的总分排全班第一。"

妈妈:"做的好!"

儿子:"但我的总分在年级中只排第十名"

妈妈:"不要灰心,以及很好了!"

从上述对话可以看出,儿子从说期末考开始一直向妈妈报告,这就是条件触发的原理。我将其整理如下:“条件触发方式中,只要输入缓冲有数据就会一直通知该事。"

例如,服务器端输入缓冲收到50字节的数据时,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务器端读取20字节后还剩30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。接下来通过如下对话介绍边缘触发的事件特性。

儿子:"妈妈,我期末考了。"

妈妈:“考的怎么样。”

儿子:"........."

妈妈:"说话啊!是考差了吗?"

从上述对话可以看出,边缘触发中输人缓冲收到数据时仅注册1次该事件。即使输入缓冲中
还留有数据,也不会再进行注册。

掌握条件触发的事件特性

接下来通过代码来了解条件触发的事件注册方式。epoll默认是以条件触发的方式工作,因此可以通过该实例验证条件触发的特性。

#include <"与之前示例的头文件声明一致,故省略。">
#define BUF_SIZE 4
#define EPOLL_SIZE 50
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);

    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){ //关闭套接字
                       printf("closed client: %d \n", ep_events[i].data.fd);
                       close(ep_events[i].data.fd);
                       epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                   }
                   else{
                       write(ep_events[i].data.fd,buf,str_len);//回声
                   }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}

void error_handling(char *buf){
     //与之前实例相同,故省略
}

上述示例与之前的差异如下。

□将调用read函数时使用的缓冲大小缩减为4个字节(第2行)

□插入验证epoll_wait函数调用次数的语句(第50行)
减少缓冲大小是为了阻止服务器端一次性读取接收的数据。换言之,调用read函数后,输入缓冲中仍有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出”return epoll_wait”字符串。

从运行结果中可以看出,每当收到客户端数据时,都会注册该事件,并因此多次调用epoll_ wait函数。下面将上述示例改成边缘触发方式,需要做一些额外的工作。但我希望通过最小的改动验证边缘触发模型的事件注册方式。将上述示例的第57行改成如下形式运行服务器端和客户端:

event.events = EPOLLIN|EPOLLET;


更改后可以验证如下事实:“从客户端接收数据时,仅输出1次'return epoll_wait'字符串,这意味着仅注册1次事件。”虽然可以验证上述事实,但客户端运行时将发生错误。大家是否遇到了这种问题?能否自行分析原因?虽然目前不必对此感到困惑,但如果理解了边缘触发的特性,应该可以分析出错误原因。

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

如下两点是实现边缘触发的必知内容。

□通过errno变量验证错误原因。
□为了完成非阻塞(Non-blocking)I/O,更改套接字特性。

Linux的套接字相关函数一般通过返回-1通知发生了错误。虽然知道发生了错误,但仅凭这
些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息,Linux声明了如下
全局变量:

int errno;

为了访问该变量,需要引入error.h头文件。另外每种函数发生错误时,保存到errno变量中的值都不同,没必要记住所有可能的值。学习每种函数的过程中逐一掌握,并能在必要时参考即可。本节只介绍如下类型的错误:"read函数发现输入缓冲中没有数据可读时返回-1,同时在errno中保存EAGAIN常量。”

稍后通过示例给出errno的使用方法。下面讲解将套接字改为非阻塞方式的方法。Linux提供更改或读取文件属性的如下方法(曾在第13章使用过)。

#include<fcntl.h>
int fcntl(int filedes,int cmd,...);
//成功是返回cmd参数相关值,失败时返回-1
    filedes  //目标的文件描述符。
    cmd      //表示函数调用的目的。

从上述声明中可以看到,fcntl具有可变参数的形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性(int型)。反之,如果传递F_SETFL,可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下2条语句。

int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd,F_SETFL,flag|O_NONBLOCK);

通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read&write函数时,无论是否存在数据,都会形成非阻塞文件。fcntl函数的适用范围很广,各位既可以在学习系统编程时一次性总结所有适用情况,也可以每次需要时逐一掌握。

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

之所以介绍读取错误原因的方法和非阻塞模式的套接字创建方法,原因在于二者都与边缘触发的服务器端实现有密切联系。

首先说明为何需要通过errno确认错误原因:“边缘触发方式中,接收数据时仅注册1次该事件。”
就因为这种特点,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据。因此需要验
证输入缓冲是否为空。(不然套接字将无法注销)

既然如此,为何还需要将套接字变成非阻塞模式?边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿(等待数据到来)。因此,边缘触发方式中一定要采用非阻塞read&write函数。接下来给出以边缘触发方式工作的回声服务器端示例。

#include<“添加fcntl.h、errno.h,其他与之前示例的头文件声明一致。“>
#include <fcntl.h>
#include <errno.h>
#define BUF SIZE 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");
        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);
                  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){ // 客户端关闭连接
                          printf("closed client: %d \n", ep_events[i].data.fd);
                          close(ep_events[i].data.fd);
                          epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                          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){
     //示例和之前的示例相同,故省略
}

条件触发和边缘触发孰强孰弱

我们从理论和代码的角度充分理解了条件触发和边缘触发,但仅凭这些还无法理解边缘触发
相对于条件触发的优点。边缘触发方式下可以做到如下这点:“可以分离接收数据和处理数据的时间点!”

虽然比较简单,但非常准确有力地说明了边缘触发的优点。关于这句话的含义,大家以后开
不同类型的程序时会有更深入的理解。现阶段给出如下情景帮助大家理解:有一个服务器端和三个客户端,这三个客户端分别是A,B,C,它们分别向服务器发送它们对应部分的数据,服务器需要将这些数据组合,以A,B,C的正向顺序排列发送给任意主机。

那么对应服务端的运行过程如下:

□服务器端分别从客户端A、B、C接收数据。
□服务器端按照A、B、C的顺序重新组合收到的数据。
□组合的数据将发送给任意主机。

为了完成该过程,若能按如下流程运行程序,服务器端的实现并不难。
□客户端按照A、B、C的顺序连接服务器端,并依序向服务器端发送数据。

□需要接收数据的客户端应在客户端A、B、C之前连接到服务器端并等待。

但现实中可能频繁出现如下这些情况,换言之,如下情况出现更符合实际。

□客户端C和B正向服务器端发送数据,但A尚未连接到服务器端。

□客户端A、B、C乱序发送数据。
□服务器端已收到数据,但要接收数据的目标客户端还未连接到服务器端。

因此,即使输入缓冲收到数据(注册相应事件),服务器端也能决定读取和处理这些数据的
时间点,这样就给服务器端的实现带来巨大的灵活性。相比于条件触发,如果尝试分离接受数据和处理数据的时间的话,则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加,服务端能够接受吗?文章来源地址https://www.toymoban.com/news/detail-601758.html

到了这里,关于TCP/IP网络编程 第十七章:优于select的epoll的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《TCP IP网络编程》第十八章

    线程背景:         第 10 章介绍了多进程服务端的实现方法。多进程模型与 select 和 epoll 相比的确有自身的优点,但同时也有问题。如前所述, 创建(复制)进程的工作本身会给操作系统带来相当沉重的负担。而且,每个进程都具有独立的内存空间,所以进程间通信的实

    2024年02月12日
    浏览(53)
  • 《TCP IP 网络编程》第十五章

     标准 I/O 函数的两个优点:         除了使用 read 和 write 函数收发数据外,还能使用标准 I/O 函数收发数据。下面是标准 I/O 函数的两个优点: 标准 I/O 函数具有良好的移植性 标准 I/O 函数可以利用缓冲提高性能         创建套接字时,操作系统会准备 I/O 缓冲。 此缓

    2024年02月14日
    浏览(50)
  • 《TCP IP网络编程》第十三章

    Linux 中的 send recv:          send 函数定义:         recv 函数的定义:         send 和 recv 函数的最后一个参数是收发数据的可选项,该选项可以用位或(bit OR)运算符(| 运算符)同时传递多个信息。send recv 函数的可选项意义: MSG_OOB:发送紧急消息 :     

    2024年02月15日
    浏览(43)
  • TCP/IP网络编程 第十九章:Windows平台下线程的使用

    要想掌握Windows平台下的线程,应首先理解“内核对象”(Kernel Objects)的概念。如果仅介绍Windows平台下的线程使用技巧,则可以省略相对陌生的内核对象相关内容。但这并不能使各位深入理解Windows平台下的线程。 内核对象的定义 操作系统创建的资源有很多种,如进程、线程

    2024年02月16日
    浏览(54)
  • TCP/IP网络编程 第十六章:关于IO流分离的其他内容

    两次I/O流分离 我们之前通过2种方法分离过IO流,第一种是第十章的“TCPI/O过程(Routine)分离”。这种方法通过调用fork函数复制出1个文件描述符,以区分输入和输出中使用的文件描述符。虽然文件描述符本身不会根据输入和输出进行区分,但我们分开了2个文件描述符的用途

    2024年02月16日
    浏览(43)
  • TCP/IP网络编程 第十五章:套接字和标准I/O

    标准I/O函数的两个优点 将标准I/O函数用于数据通信并非难事。但仅掌握函数使用方法并没有太大意义,至少应该 了解这些函数具有的优点。下面列出的是标准I/O函数的两大优点: □标准I/O函数具有良好的移植性(Portability) □标准I/O函数可以利用缓冲提高性能。 关于移植性无需

    2024年02月16日
    浏览(54)
  • 《TCP IP网络编程》

            2023.6.28 正式开始学习网络编程。 每一章每一节的笔记都会记录在博客中以便复习。         网络编程又叫套接字编程。所谓网络编程,就是编写程序使两台连网的计算机相互交换数据。 为什么叫套接字编程? 我们平常将插头插入插座上就能从电网中获取电力,同

    2024年02月11日
    浏览(48)
  • 【Java基础教程】(四十七)网络编程篇:网络通讯概念,TCP、UDP协议,Socket与ServerSocket类使用实践与应用场景~

    了解多线程与网络编程的操作关系; 了解网络程序开发的主要模式; 了解 TCP 程序的基本实现; 在Java中,网络编程的核心意义是实现不同电脑主机之间的数据交互。Java采用了一种简化的概念,将这个过程进一步抽象为JVM(Java虚拟机)进程之间的通信。可以在同一台电脑上

    2024年02月15日
    浏览(67)
  • JAVAEE初阶相关内容第十七弹--网络原理之TCP_IP

    目录 1. TCP-IP五层模型 2. UDP协议 2.1 特点 2.2 UDP协议端格式 2.3 校验和 3. TCP协议 3.1 特点 3.2 TCP协议段格式 3.2.1 首部长度 3.2.2 选项 3.2.3 保留6位 3.3 TCP内部的工作机制 3.3.1 确认应答 (1)应答报文ack (2)小结 3.3.2 超时重传 3.3.3 连接管理 3.3.4 滑动窗口 窗口大小 3.3.5流量控制 3

    2024年01月18日
    浏览(45)
  • TCP/IP网络编程(一)

    1.1.1 构建打电话套接字 以电话机打电话的方式来理解套接字。 **调用 socket 函数(安装电话机)时进行的对话:**有了电话机才能安装电话,于是就要准备一个电话机,下面函数相当于电话机的套接字。 **调用 bind 函数(分配电话号码)时进行的对话:**套接字同样如此。就想

    2024年02月03日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包