Linux网络_高并发之epoll

前言

        本章主要对多路复用IO接口epoll进行详细的使用说明和其它多路复用IO接口(poll,select)与epoll的主要区别。建议在linux内核版本2.6以上使用。

区别

        在linux中有三种多路复用模型分别是select、poll、epoll。
        select能监听的文件描述符最大1024,单纯的改变进程的打开文件描述符个数并不能改变select监听文件的个数,如果要进行更改需要修改内和文件并重新编译内核。解决1024以下的客户端时使用select是很合适的,但如果链接的客户端过多,select采用轮询获取哪些文件描述符是可读或可写的,会大大降低服务器相应效率。
        poll与select类似,但突破了监听1024个文件描述符的限制。
        epoll是select和poll的增强版,epoll不需要对监听的文件描述符轮询判断是哪个文件描述符有IO事件,当有IO事件会被内核将发生事件的文件描述符通过epoll_wail返回。并且除了提供select/poll那种IO事件的水平触发外,还提供了边缘触发,给并发提供了更多的灵活性。

        补充:当epoll和select/poll都监听1024以下相同的客户端时,并且监听的客户端都是活跃链接,两者的性能相差不大。epoll主要是减少了事件的轮询。但select夸平台windows下也可以使用,epoll只有linux中可以使用。

API

        使用epoll离不开三和API系统调用分别是epoll_create、epoll_ctl、epoll_wait。它们都包含在sys/epoll.h头文件中。

epoll_create

        原型:int epoll_create(int size)
        功能:创建epoll实例。
        size:标识这个监听的数目最大有多大。
        返回值:成功,返回epoll文件描述符。错误,则返回-1,并且将errno设置为指示错误。
        注意:
        1 epoll_create()返回引用新epoll实例的文件描述符。该文件描述符用于随后的所有对epoll的调用接口。每创建一个epoll句柄,会占用一个fd,因此当不再需要时,应使用close关闭epoll_create()返回的文件描述符,否则可能导致fd被耗尽。当所有文件描述符引用已关闭的epoll实例,内核将销毁该实例并释放关联的资源以供重用。
        2 在最初的epoll_create()实现中,size参数将调用者希望添加到epoll实例文件描述符的数量告知内核。内核使用该信息作为内部数据结构初始分配空间的提示,事件。 (如果有必要,如果调用方的使用超出了大小提示,内核将分配更多空间。)如今,此提示不再必需(内核无需提示即可动态调整所需数据结构的大小),但是大小必须仍大于零,以便当新的epoll应用程序在较旧的内核上运行时,请确保向后兼容。

epoll_ctl

        原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
        功能:控制epoll对象epfd监控的文件描述符上的事件:注册、修改、删除。
        epfd:   epoll_create创建的句柄。
        op:     要执行的动作,用3个宏表示。
                  EPOLL_CTL_ADD:将新的文件描述符fd添加(注册)到epfd对象中。
                  EPOLL_CTL_MOD:修改已经注册在epfd中的fd文件描述符。并给出event的新设置。
                  EPOLL_CTL_DEL:从epfd中删除一个fd文件描述符。第三个event参数为NULL。
      event:  需要设置的事件。类型struct epoll_event。
       返回值:成功,0。失败,-1。
      结构说明:

struct epoll_event 
{
    __uint32_t events;     根据下方 events 宏值进行设置
    epoll_data_t data;     根据下方联合体,选择一种成员(或者方式)给监听的文件描述符携带数据
};

typedef union epoll_data 
{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

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

epoll_wait

        原型:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
        功能:等待epfd上所监控的文件描述符有事件的产生,并返回。
        epfd:   epoll_create创建的epoll实例
        events: 用来从内核获取产生事件的集合(结构中包含事件,和设置时携带的数据。)
        maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。如果事件数量大于这个值,剩余的事件将在下次调用wait时返回。
        timeout: 超时时间。-1,永久阻塞。0,立即返回,不阻塞。>0, 指定超时时间,如果等待超过这个时间将返回,单位:微妙。
        返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1。

示例代码

server

#include <iostream>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstdio>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/epoll.h>

using namespace std;

const unsigned int PORT = 9527;
const unsigned int MAXBUFLEN = 4096;
const unsigned int OPEN_EP_MAX = 4096;

int Listen();

int main(int argc, char* argv[])
{

    int sfd = Listen();
    if(sfd == -1)
    {
        perror("Listen");
        return -1;
    }

    int efd = epoll_create(1024);
    if(efd == -1)
    {
        perror("create epoll");
        return -1;
    }
    epoll_event epevent;
    epevent.events  = EPOLLIN;        // 默认水平触发,在这里可以设置sfd套接字为边缘触发。
    epevent.data.fd = sfd;

    if( epoll_ctl(efd,EPOLL_CTL_ADD, sfd, &epevent) == -1)
    {
        perror("epoll_ctl");
        return -1;
    }

    epoll_event resEvent[OPEN_EP_MAX];
    while(true)
    {
      int waitCount = epoll_wait(efd,resEvent,OPEN_EP_MAX,-1);
      if(waitCount <= 0)
      {
          continue;
      }
      for(int i = 0; i < waitCount; ++i)
      {
          if(resEvent[i].events & EPOLLIN)
          {
              if(resEvent[i].data.fd == sfd)
              {
                  sockaddr_in caddr;
                  socklen_t clen = sizeof(caddr);
                  int cfd = accept(sfd,(sockaddr*)&caddr,&clen);
                  if(cfd == -1)
                  {
                      perror("accpet");
                      continue;
                  }
                  epoll_event epinevent;
                  epinevent.events = EPOLLIN;        // 默认水平触发,在这里可以设置sfd套接字为边缘触发。
                  epinevent.data.fd = cfd;
                  epoll_ctl(efd, EPOLL_CTL_ADD,cfd, &epinevent);

                  char clientIP[INET_ADDRSTRLEN] = {0};
                  cout << "received from:  " << inet_ntop(AF_INET,&caddr.sin_addr, clientIP,sizeof(clientIP)) << " PROT: " << ntohs(caddr.sin_port) << flush;
              }
              else
              {
                  int cfd = resEvent[i].data.fd;
                  char buf[MAXBUFLEN] = {0};
                  int rcnt = read(cfd,buf, MAXBUFLEN );

                  if(rcnt >0 )
                  {
                      cout << "   MSG: " << buf << endl;
                      const char* sendMsg = "sender ok.";
                      write(cfd, sendMsg , strlen(sendMsg));
                      memset(buf,0,MAXBUFLEN);
                  }
                  else if( rcnt == 0)
                  {
                      // 客户端主动发起关闭,描述符可读数据为0. 此时将客户端关闭,为半链接状态。会不断的收到客户端发送数据包,服务器关闭对应套接字,完成四次分手,并从epoll中移除这个链接。
                      // 对于长时间与服务器链接但是没有数据传送的客户端,不活跃的客户端。可以通过时间戳设置套接字事件,当超过一定时间,服务器关闭这个套接字。
                      epoll_ctl(efd,EPOLL_CTL_DEL,cfd,NULL);
                      close(cfd);
                      cout << "client close." << endl;
                  }
              }

          }
      }
    }

    close(sfd);
    close(efd);
    return 0;
}

int Listen()
{
    int sfd = socket(AF_INET,SOCK_STREAM,0);
    if(sfd == -1)
    {
        perror("create socket");
        return -1;
    }

    sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons( PORT );

    if (bind(sfd, (sockaddr*) &saddr,sizeof(saddr)) == -1)
    {
        perror("bind error");
        return -1;
    }

    if(listen(sfd,128) == -1)
    {
        perror("listen");
        return -1;
    }
    return sfd;
}

client

        在测试与服务器联通时也可以用linux提供的命令进行测试,nc ip port
        执行:nc 127.0.0.1 9527

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 80
#define SERV_PORT 9527
int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    while (fgets(buf, MAXLINE, stdin) != NULL) 
    {
        write(sockfd, buf, strlen(buf));
        n = read(sockfd, buf, MAXLINE);
        if (n == 0)
            printf("the other side has been closed.\n");
        else
            write(STDOUT_FILENO, buf, n);
    }
    close(sockfd);
    return 0;
}

补充

事件触发模型

EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。用宏EPOLLET表示
Level Triggered (LT) 水平触发只要有数据都会触发。用宏EPOLLET表示。
两种宏通过在设置设置文件描述符事件时设置,如:

...
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET;    // ET 边沿触发,默认是水平触发,通过与其它事件按位或设置
...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_虚竹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值