关于c++:彻底学会使用epoll二ET的读写操作实例分析

相干视频举荐
面试中正经“八股文”网络原理tcp/udp,网络编程epoll/reactor
epoll 原理分析 以及 reactor 模型利用
epoll原理分析以及三握四挥的解决
LinuxC++后盾服务器开发架构师收费学习地址
彻底学会应用epoll(一)——ET模式实现剖析
接上一篇
首先,看程序四的例子。
l 程序四

#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
    int epfd,nfds;
    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要解决的事件
    epfd=epoll_create(1);//只须要监听一个描述符——规范输入
    ev.data.fd=STDOUT_FILENO;
    ev.events=EPOLLOUT|EPOLLET;//监听读状态同时设置ET模式
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);//注册epoll事件
    for(;;)
   {
      nfds=epoll_wait(epfd,events,5,-1);
      for(int i=0;i<nfds;i++)
     {
         if(events[i].data.fd==STDOUT_FILENO)
             cout<<"hello world!"<<endl;
     }
   }
};

这个程序的性能是只有规范输入写就绪,就输入“hello world!”。
运行后果:

咱们发现这将是一个死循环。上面具体分析一下这个程序的执行过程:
(1) 首先初始buffer为空,buffer中有空间可写,这时无论是ET还是LT都会将对应的epitem退出rdlist(对应第一节图中的红线),导致epoll_wait就返回写就绪。
(2) 程序想规范输入输入”hello world!”和换行符,因为规范输入为控制台的时候缓冲是“行缓冲”,所以换行符导致buffer中的内容清空,这就对应第二节中ET模式下写就绪的第二种状况——当有旧数据被发送走时,即buffer中待写的内容变少得时候会触发fd状态的扭转。所以下次epoll_wait会返回写就绪。之后反复这个过程始终循环上来。
咱们再看程序五。
程序五
绝对程序四这里仅仅去掉了输入的换行操作。即:
cout<<“hello world!”;
运行后果如下:

咱们看到程序成挂起状态。因为第一次epoll_wait返回写就绪后,程序向规范输入的buffer中写入“hello world!”,然而因为没有输入换行,所以buffer中的内容始终存在,下次epoll_wait的时候,尽管有写空间然而ET模式下不再返回写就绪。回顾第一节对于ET的实现,这种状况起因就是第一次buffer为空,导致epitem退出rdlist,返回一次就绪后移除此epitem,之后尽管buffer依然可写,然而因为对应epitem曾经不再rdlist中,就不会对其就绪fd的events的在检测了。
程序六
int main(void)
{

int epfd,nfds;
struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要解决的事件
epfd=epoll_create(1);//只须要监听一个描述符——规范输入
ev.data.fd=STDOUT_FILENO;
ev.events=EPOLLOUT;//应用默认LT模式
epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);//注册epoll事件
for(;;)

{

 nfds=epoll_wait(epfd,events,5,-1);
 for(int i=0;i<nfds;i++)
{
  if(events[i].data.fd==STDOUT_FILENO)
     cout<<"hello world!";
}

}
};

程序六绝对程序五仅仅是批改ET模式为默认的LT模式,咱们发现程序再次死循环。这时候起因曾经很分明了,因为当向buffer写入”hello world!”后,尽管buffer没有输入清空,然而LT模式下只有buffer有写空间就返回写就绪,所以会始终输入”hello world!”,当buffer满的时候,buffer会主动刷清输入,同样会造成epoll_wait返回写就绪。
程序七
int main(void)

{

int epfd,nfds;

struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要解决的事件

epfd=epoll_create(1);//只须要监听一个描述符——规范输入

ev.data.fd=STDOUT_FILENO;

ev.events=EPOLLOUT|EPOLLET;//监听读状态同时设置ET模式

epoll_ctl(epfd,EPOLL_CTL_ADD,STDOUT_FILENO,&ev);//注册epoll事件

for(;;)

{

 nfds=epoll_wait(epfd,events,5,-1);

 for(int i=0;i<nfds;i++)

{

   if(events[i].data.fd==STDOUT_FILENO)

       cout<<"hello world!";

   ev.data.fd=STDOUT_FILENO;

   ev.events=EPOLLOUT|EPOLLET;

   epoll_ctl(epfd,EPOLL_CTL_MOD,STDOUT_FILENO,&ev); //从新MOD事件(ADD有效)

}

}

};

程序七绝对于程序五在每次向规范输入的buffer输入”hello world!”后,从新MOD OUT事件。所以相当于每次从新进行第一节中红线形容的路径返回就绪,导致程序循环输入。
ET模式下的读写
通过后面几节剖析,咱们能够晓得,当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的告诉,造成buffer中已有的数据无机会读出,除非有新的数据再次达到。对于写操作,次要是因为ET模式下fd通常为非阻塞造成的一个问题——如何保障将用户要求写的数据写完。
要解决上述两个ET模式下的读写问题,咱们必须实现:
a. 对于读,只有buffer中还有数据就始终读;
b. 对于写,只有buffer还有空间且用户申请写的数据还未写完,就始终写。
要实现上述a、b两个成果,咱们有两种办法解决。
办法一
(1) 每次读入操作后(read,recv),用户被动epoll_mod IN事件,此时只有该fd的缓冲还有数据能够读,则epoll_wait会返回读就绪。
(2) 每次输入操作后(write,send),用户被动epoll_mod OUT事件,此时只有该该fd的缓冲能够发送数据(发送buffer不满),则epoll_wait就会返回写就绪(有时候采纳该机制告诉epoll_wai醒过来)。
这个办法的原理咱们在之前探讨过:当buffer中有数据可读(即buffer不空)且用户对相应fd进行epoll_mod IN事件时ET模式返回读就绪,当buffer中有可写空间(即buffer不满)且用户对相应fd进行epoll_mod OUT事件时返回写就绪。
所以失去如下解决形式:
if(events[i].events&EPOLLIN)//如果收到数据,那么进行读入

{

cout << "EPOLLIN" << endl;

sockfd = events[i].data.fd;

if ( (n = read(sockfd, line, MAXLINE))>0) 

{

line[n] = ‘/0’;

    cout << "read " << line << endl;

if(n==MAXLINE)

{

ev.data.fd=sockfd;

ev.events=EPOLLIN|EPOLLET;

epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //数据还没读完,从新MOD IN事件

}

else

{

ev.data.fd=sockfd;

ev.events=EPOLLIN|EPOLLET;

epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //buffer中的数据曾经读取结束MOD OUT事件

}

}

else if (n == 0)

{

close(sockfd);

}


}

else if(events[i].events&EPOLLOUT) // 如果有数据发送

{

sockfd = events[i].data.fd;

write(sockfd, line, n);

ev.data.fd=sockfd; //设置用于读操作的文件描述符

ev.events=EPOLLIN|EPOLLET; //设置用于注测的读操作事件

epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  //批改sockfd上要解决的事件为EPOLIN

}
注:对于write操作,因为sockfd是工作在阻塞模式下的,所以没有必要进行非凡解决,和LT应用一样。
剖析:这种办法存在几个问题:
(1) 对于read操作后的判断——if(n==MAXLINE),不能阐明这种状况buffer就肯定还有没有读完的数据,试想万一buffer中一共就有MAXLINE字节数据呢?这样持续 MOD IN就不再失去告诉,而也就没有机会对相应sockfd MOD OUT。
(2) 那么如果服务端用其余形式可能在适当机会对相应的sockfd MOD OUT,是否这种办法就可取呢?咱们首先思考一下为什么要用ET模式,因为ET模式可能缩小epoll_wait等零碎调用,而咱们在这里每次read后都要MOD IN,之后又要epoll_wait,势必造成效率升高,这不是事与愿违吗?
综上,此形式不应该应用。
l 办法二
读: 只有可读, 就始终读, 直到返回 0, 或者 errno = EAGAIN
写: 只有可写, 就始终写, 直到数据发送完, 或者 errno = EAGAIN
if (events[i].events & EPOLLIN)

{

n = 0;

  while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) 

{

n += nread;

  }

if (nread == -1 && errno != EAGAIN)

{

perror(“read error”);

  }

  ev.data.fd = fd;

  ev.events = events[i].events | EPOLLOUT;

  epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);

}

if (events[i].events & EPOLLOUT)

{

int nwrite, data_size = strlen(buf);

  n = data_size;

  while (n > 0) 

{

nwrite = write(fd, buf + data_size – n, n);

      if (nwrite < n) 

{

         if (nwrite == -1 && errno != EAGAIN) 

{

perror(“write error”);

         }

         break;

       }

      n -= nwrite;

    }

ev.data.fd=fd; 

ev.events=EPOLLIN|EPOLLET; 

epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);  //批改sockfd上要解决的事件为EPOLIN

}
注:应用这种形式肯定要使每个连贯的套接字工作于非阻塞模式,因为读写须要始终读或写直到出错(对于读,当读到的理论字节数小于申请字节数时就能够进行),而如果你的文件描述符如果不是非阻塞的,那这个始终读或始终写势必会在最初一次阻塞。这样就不能在阻塞在epoll_wait上了,造成其余文件描述符的工作饿死。
综上:办法一不适宜应用,咱们只能应用办法二,所以也就常说“ET须要工作在非阻塞模式”,当然这并不能阐明ET不能工作在阻塞模式,而是工作在阻塞模式可能在运行中会呈现一些问题。
l 办法三
仔细分析办法二的写操作,咱们发现这种形式并不很完满,因为写操作返回EAGAIN就终止写,然而返回EAGAIN只能说名以后buffer已满不可写,并不能保障用户(或服务端)要求写的数据曾经写完。那么如何保障对非阻塞的套接字写够申请的字节数才返回呢(阻塞的套接字直到将申请写的字节数写完才返回)?
咱们须要封装socket_write()的函数用来解决这种状况,该函数会尽量将数据写完再返回,返回-1示意出错。在socket_write()外部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会期待后再重试.
ssize_t socket_write(int sockfd, const char* buffer, size_t buflen)

{

ssize_t tmp;

size_t total = buflen;

const char* p = buffer;

while(1)

{

tmp = write(sockfd, p, total);

if(tmp < 0)

{

  // 当send收到信号时,能够持续写,但这里返回-1.

  if(errno == EINTR)

    return -1;

  // 当socket是非阻塞时,如返回此谬误,示意写缓冲队列已满,

  // 在这里做延时后再重试.

  if(errno == EAGAIN)

  {

    usleep(1000);

    continue;

  }

  return -1;

}

if((size_t)tmp == total)

    return buflen;

 total -= tmp;

 p += tmp;

}

return tmp;//返回已写字节数

}
剖析:这种形式也存在问题,因为在实践上可能会长工夫的阻塞在socket_write()外部(buffer中的数据得不到发送,始终返回EAGAIN),但暂没有更好的方法。
不过看到这种形式时,我在想在socket_write中将sockfd改为阻塞模式应该一样可行,等再次epoll_wait之前再将其改为非阻塞。

【文章福利】:小编整顿了一些集体感觉比拟好的学习书籍、视频材料共享在群文件外面,有须要的能够自行添加哦!~点击退出(须要自取)

5.2 ET模式下的accept

思考这种状况:多个连贯同时达到,服务器的 TCP 就绪队列霎时积攒多个就绪

连贯,因为是边缘触发模式,epoll 只会告诉一次,accept 只解决一个连贯,导致 TCP 就绪队列中剩下的连贯都得不到解决。
解决办法是用 while 循环抱住 accept 调用,解决完 TCP 就绪队列中的所有连贯后再退出循环。如何晓得是否解决完就绪队列中的所有连贯呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就示意所有连贯都解决完。
的正确应用形式为:
while ((conn_sock = accept(listenfd,(struct sockaddr ) &remote, (size_t )&addrlen)) > 0) {

handle_client(conn_sock);   

}

if (conn_sock == -1) {

 if (errno != EAGAIN && errno != ECONNABORTED    

        && errno != EPROTO && errno != EINTR)    

    perror("accept");   

}
起因:如果accept工作在阻塞模式,思考这种状况: TCP 连贯被客户端夭折,即在服务器调用 accept 之前(此时select等曾经返回连贯达到读就绪),客户端被动发送 RST 终止连贯,导致刚刚建设的连贯从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会始终阻塞在 accept 调用上,直到其余某个客户建设一个新的连贯为止。然而在此期间,服务器单纯地阻塞在accept 调用上(理论应该阻塞在select上),就绪队列中的其余描述符都得不到解决。

解决办法是把监听套接口设置为非阻塞, 当客户在服务器调用 accept 之前停止

某个连贯时,accept 调用能够立刻返回 -1, 这时源自 Berkeley 的实现会在内核中解决该事件,并不会将该事件告诉给 epoll,而其余实现把 errno 设置为 ECONNABORTED 或者 EPROTO 谬误,咱们应该疏忽这两个谬误。(具体可参看UNP v1 p363)
6.1 ET模式为什么要设置在非阻塞模式下工作
因为ET模式下的读写须要始终读或写直到出错(对于读,当读到的理论字节数小于申请字节数时就能够进行),而如果你的文件描述符如果不是非阻塞的,那这个始终读或始终写势必会在最初一次阻塞。这样就不能在阻塞在epoll_wait上了,造成其余文件描述符的工作饿死。
6.2 应用ET和LT的区别
LT:程度触发,效率会低于ET触发,尤其在大并发,大流量的状况下。然而LT对代码编写要求比拟低,不容易呈现问题。LT模式服务编写上的体现是:只有有数据没有被获取,内核就一直告诉你,因而不必放心事件失落的状况。
ET:边缘触发,效率十分高,在并发,大流量的状况下,会比LT少很多epoll的零碎调用,因而效率高。然而对编程要求高,须要粗疏的解决每个申请,否则容易产生失落事件的状况。

上面举一个列子来阐明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采纳LT模式下,如果accept调用有返回就能够马上建设以后这个连贯了,再epoll_wait期待下次告诉,和select一样。
然而对于ET而言,如果accpet调用有返回,除了建设以后这个连贯外,不能马上就epoll_wait还须要持续循环accpet,直到返回-1,且errno==EAGAIN,
从实质上讲:与LT相比,ET模型是通过缩小零碎调用来达到进步并行效率的。
6.3 一道腾讯后盾开发的面试题

应用Linux epoll模型,程度(LT)触发模式,当socket可写时,会不停的触发socket可写的事件,如何解决?
第一种最广泛的形式:
须要向socket写数据的时候才把socket退出epoll,期待可写事件。承受到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移出epoll。
这种形式的毛病是,即便发送很少的数据,也要把socket退出epoll,写完后在移出epoll,有肯定操作代价。
一种改良的形式:
开始不把socket退出epoll,须要向socket写数据的时候,间接调用write或者send发送数据。如果返回EAGAIN,把socket退出epoll,在epoll的驱动下写数据,全副数据发送结束后,再移出epoll。
这种形式的长处是:数据不多的时候能够防止epoll的事件处理,提高效率。
6.4什么状况下用ET
很简略,当你想进步程序效率的时候。

最初附一个epoll实例:


#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_EVENTS 10
#define PORT 8080

//设置socket连贯为非阻塞模式
void setnonblocking(int sockfd) {
    int opts;
    opts = fcntl(sockfd, F_GETFL);
    if(opts < 0) {
        perror("fcntl(F_GETFL)\n");
        exit(1);
    }
    opts = (opts | O_NONBLOCK);
    if(fcntl(sockfd, F_SETFL, opts) < 0) {
        perror("fcntl(F_SETFL)\n");
        exit(1);
    }
}

int main(){
    struct epoll_event ev, events[MAX_EVENTS]; //ev负责增加事件,events接管返回事件
    int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
    struct sockaddr_in local, remote;
    char buf[BUFSIZ];

    //创立listen socket
    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("sockfd\n");
        exit(1);
    }
    setnonblocking(listenfd);//listenfd设置为非阻塞[1]
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);;
    local.sin_port = htons(PORT);
    if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) {
        perror("bind\n");
        exit(1);
    }
    listen(listenfd, 20);

    epfd = epoll_create(MAX_EVENTS);
    if (epfd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {//监听listenfd
        perror("epoll_ctl: listen_sock");
        exit(EXIT_FAILURE);
    }

    for (;;) {
        nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_pwait");
            exit(EXIT_FAILURE);
        }

        for (i = 0; i < nfds; ++i) {
            fd = events[i].data.fd;
            if (fd == listenfd) {
                while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
                                (size_t *)&addrlen)) > 0) {
                    setnonblocking(conn_sock);//上面设置ET模式,所以要设置非阻塞
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = conn_sock;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {//读监听
                        perror("epoll_ctl: add"); //连贯套接字
                        exit(EXIT_FAILURE);
                    }
                }
                if (conn_sock == -1) {
                    if (errno != EAGAIN && errno != ECONNABORTED
                            && errno != EPROTO && errno != EINTR)
                        perror("accept");
                }
                continue;
            }
            if (events[i].events & EPOLLIN) {
                n = 0;
                while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {//ET下能够读就始终读
                    n += nread;
                }
                if (nread == -1 && errno != EAGAIN) {
                    perror("read error");
                }
                ev.data.fd = fd;
                ev.events = events[i].events | EPOLLOUT; //MOD OUT
                if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {
                    perror("epoll_ctl: mod");
                }
            }
            if (events[i].events & EPOLLOUT) {
              sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
                int nwrite, data_size = strlen(buf);
                n = data_size;
                while (n > 0) {
                    nwrite = write(fd, buf + data_size - n, n);//ET下始终将要写数据写完
                    if (nwrite < n) {
                        if (nwrite == -1 && errno != EAGAIN) {
                            perror("write error");
                        }
                        break;
                    }
                    n -= nwrite;
                }
                close(fd);
            }
        }
    }
    return 0;
}

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据