【epoll】epoll使用详解(精髓)--研读和修正

目录

epoll 和select

epoll的接口

如何来使用epoll

epoll程序框架

实例源码

相关知识

Socket的阻塞模式和非阻塞模式

如何动态的改变listen监听的个数呢?

队列已满的情况,如何处理?

SYN泛滥攻击

参考

实例代码二

多进程Epoll:

建立2000+个链接的测试代码

关于ET、LT两种工作模式

epoll中读写数据 的注意事项

多路复用

讨论


epoll 和select


相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。

并且,在linux/posix_types.h头文件有这样的声明:
#define __FD_SETSIZE    1024
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

 

一、IO多路复用的select

  IO多路复用相对于阻塞式和非阻塞式的好处就是它可以监听多个 socket ,并且不会消耗过多资源。当用户进程调用 select 时,它会监听其中所有 socket 直到有一个或多个 socket 数据已经准备好,否则就一直处于阻塞状态。select的缺点在于单个进程能够监视的文件描述符的数量存在最大限制,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的的开销也线性增长。同时,由于网络响应时间的延迟使得大量的tcp链接处于非常活跃状态,但调用select()会对所有的socket进行一次线性扫描,所以这也浪费了一定的开销。不过它的好处还有就是它的跨平台特性。

【epoll】epoll使用详解(精髓)--研读和修正_第1张图片

二、 异步IO的epoll

  epoll的优点就是完全的异步,你只需要对其中 poll 函数注册相应的 socket 和事件,就可以完全不管。当有事件发生时,数据已经从内核态拷贝到用户态,也就是完全没有阻塞。

【epoll】epoll使用详解(精髓)--研读和修正_第2张图片

 

 


 

epoll的接口

epoll的接口非常简单,一共就三个函数:
 

(1)epoll_create系统调用

epoll_create在C库中的原型如下。

int epoll_create(int size);

epoll_create返回一个句柄,之后 epoll的使用都将依靠这个句柄来标识。参数 size是告诉 epoll所要处理的大致事件数目。不再使用 epoll时,必须调用 close关闭这个句柄。

注意:size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,这个 size参数没有任何意义。

(2)epoll_ctl系统调用

epoll_ctl在C库中的原型如下。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。

参数:

epfd: epoll_create返回的句柄,

op:的意义见下表:
        EPOLL_CTL_ADD:注册新的fd到epfd中;
        EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
        EPOLL_CTL_DEL:从epfd中删除一个fd;

fd:需要监听的socket句柄fd,

event:告诉内核需要监听什么事,struct epoll_event结构如下:

epoll_data_t;

struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

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

data成员是一个epoll_data联合,其定义如下:

typedef union epoll_data {

void *ptr;

int fd;

uint32_t u32;

uint64_t u64;

} epoll_data_t;

可见,这个 data成员还与具体的使用方式相关。例如,ngx_epoll_module模块只使用了联合中的 ptr成员,
作为指向 ngx_connection_t连接的指针。我们在项目中一般使用的也是 ptr成员,因为它可以指向任意的结构
体地址。



3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_wait在C库中的原型如下:

int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);

收集在 epoll监控的事件中已经发生的事件,如果 epoll中没有任何一个事件发生,则最多等待timeout毫秒后返回。epoll_wait的返回值表示当前发生的事件个数,如果返回0,则表示本次调用中没有事件发生,如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。

epfd:epoll的描述符。

events:分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

maxevents:表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。

timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。

epoll有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。

默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。

水平触发LT:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写

边缘触发ET:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你

此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。)

 

如何来使用epoll


1、包含一个头文件#include  

2、create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

3、之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。

最后一个timeout:是epoll_wait的超时,

为0的时候表示马上返回,

为-1的时候表示一直等下去,直到有事件返回,

为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。

一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。

epoll_wait范围之后应该是一个循环,遍利所有的事件。

epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:

1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

3)调用epoll_wait收集发生的事件的连接


epoll程序框架


几乎所有的epoll程序都使用下面的框架:

 

伪代码:

关于epoll_wait返回值的一个简单测试

void test(int epollfd)
{
  struct epoll_event events[MAX_EVENT_NUMBER];
  int number;

  while (1)
  {
    number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
    printf("number : %2d\n\n", number);
    for (i = 0; i < number; i++)
    {
      sockfd = events[i].data.fd;

      if (sockfd == listenfd)
      {/*用户上线*/

      }
      else if (events[i].events & EPOLLIN)
      {/*有数据可读*/

      }
      else if (events[i].events & EPOLLOUT)
      {/*有数据可写*/

      }
      else
      {/*出错*/

      }
    }
  }
}

通过测试发现epoll_wait返回值number是不会大于MAX_EVENT_NUMBER的。

测试过程中,连接的客户端数远大于MAX_EVENT_NUMBER,由此可以推论:epoll_wait()每次返回的是活跃客户端的个数,每次并将这些活跃的客户端信息加入到events[MAX_EVENT_NUMBER]。

由此可见,活跃客户端的个数相同的情况下,events[MAX_EVENT_NUMBER]越大,epoll_wait()函数执行次数越少,但是events[MAX_EVENT_NUMBER]越大越消耗存储资源。

所以,MAX_EVENT_NUMBER的选择应该在效率和资源间取一个平衡点。

示例代码: 

for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;ifd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
            }
            else
            {
                //其他的处理
            }
        }
    }

大致流程

 

 struct epoll_event ev, event_list[EVENT_MAX_COUNT];//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
 
 listenfd = socket(AF_INET, SOCK_STREAM, 0);

  ev.data.fd = listenfd; //设置与要处理的事件相关的文件描述符
  ev.events = EPOLLIN | EPOLLET; //设置要处理的事件类型EPOLLIN :表示对应的文件描述符可以读,EPOLLET状态变化才通知
 

epfd = epoll_create(256);  //生成用于处理accept的epoll专用的文件描述符
  //注册epoll事件
  epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll实例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(监听listenfd)


  if(0 != bind(listenfd, (struct sockaddr *)
  if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定义了宏//#define LISTENQ   20  

  nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //等待epoll事件的发生

 

实例源码

原文有相当多的如错误:

需要增加到头文件和错误修改

//'/0'->'\0'
//bzero() 替换为memset (注意二者参数不一样,bzero将前n个字节设为0,memset将前n 个字节的值设为值 c)
//local_addr 由char* 改为 string
#include 
#include  //atoi
#include  //memset
#include    //std:cout  等

修正后的源码C++ lnux:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

//'/0'->'\0'
//bzero() 替换为memset (注意二者参数不一样,bzero将前n个字节设为0,memset将前n 个字节的值设为值 c)
//local_addr 由char* 改为 string
#include 
#include  //atoi
#include  //memset
#include    //std:cout  等



using namespace std;

#define MAXLINE   255              //读写缓冲
#define OPEN_MAX  100
#define LISTENQ   20             //listen的第二个参数  定义TCP链接未完成队列的大小(linux >2.6 则表示accpet之前的队列)
#define SERV_PORT 5000
#define INFTIM    1000

#define TIMEOUT_MS      500
#define EVENT_MAX_COUNT 20

void setnonblocking(int sock)
{
    int opts;
    opts = fcntl(sock, F_GETFL);
    if(opts < 0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts | O_NONBLOCK;
    if(fcntl(sock, F_SETFL, opts) < 0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main(int argc, char *argv[])
{
    int i, maxi, listenfd, connfd, sockfd, epfd, nfds, portnumber;
    ssize_t n;
    char line_buff[MAXLINE];
    


    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
            //fprintf()函数根据指定的format(格式)(格式)发送信息(参数)到由stream(流)指定的文件
            //printf 将内容发送到Default的输出设备,通常为本机的显示器,fprintf需要指定输出设备,可以为文件,设备。
            //stderr
            return 1;
        }
    }
    else
    {
        fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
        return 1;
    }

    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    struct epoll_event ev, event_list[EVENT_MAX_COUNT];

    //生成用于处理accept的epoll专用的文件描述符
    epfd = epoll_create(256); //生成epoll文件描述符,既在内核申请一空间,存放关注的socket fd上是否发生以及发生事件。size既epoll fd上能关注的最大socket fd数。随你定好了。只要你有空间。

    struct sockaddr_in clientaddr;
    socklen_t clilenaddrLen;
    struct sockaddr_in serveraddr;
    
    listenfd = socket(AF_INET, SOCK_STREAM, 0);//Unix/Linux“一切皆文件”,创建(套接字)文件,id=listenfd
    if (listenfd < 0)
    {
       printf("socket error,errno %d:%s\r\n",errno,strerror(errno));
    }
    //把socket设置为非阻塞方式
    //setnonblocking(listenfd);

    //设置与要处理的事件相关的文件描述符
    ev.data.fd = listenfd;

    //设置要处理的事件类型
    ev.events = EPOLLIN | EPOLLET; //EPOLLIN :表示对应的文件描述符可以读,EPOLLET状态变化才通知
    //ev.events=EPOLLIN;

    //注册epoll事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll实例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(监听listenfd)
   

    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family     = AF_INET;
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); /*IP,INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP*/
    serveraddr.sin_port       = htons(portnumber);
    
    if(0 != bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)))
     {
        printf("bind error,errno %d:%s\r\n",errno,strerror(errno));
     }

     if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定义了宏
     {
         printf("listen error,errno %d:%s\r\n",errno,strerror(errno));
     }

    maxi = 0;

    for ( ; ; )
    {

        //等待epoll事件的发生
        nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //epoll_wait(int epfd, struct epoll_event * event_list, int maxevents, int timeout),返回需要处理的事件数目

        //处理所发生的所有事件
        for(i = 0; i < nfds; ++i)
        {
            if(event_list[i].data.fd == listenfd) //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
            {
                clilenaddrLen = sizeof(struct sockaddr_in);//在调用accept()前,要给addrLen赋值,这样才不会出错,addrLen = sizeof(clientaddr);或addrLen = sizeof(struct sockaddr_in);
                connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilenaddrLen);//(accpet详解:https://blog.csdn.net/David_xtd/article/details/7087843)
                if(connfd < 0)
                {
                    //perror("connfd<0:connfd= %d",connfd);
                    printf("connfd<0,accept error,errno %d:%s\r\n",errno,strerror(errno));
                    exit(1);
                }

                //setnonblocking(connfd);

                char *str = inet_ntoa(clientaddr.sin_addr);//将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址

                cout << "accapt a connection from " << str << endl;

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

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

                //注册ev
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //将accpet的句柄添加进入(增加监听的对象)
            }
            else if(event_list[i].events & EPOLLIN) //如果是已经连接的用户,并且收到数据,那么进行读入。
            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = event_list[i].data.fd) < 0)
                    continue;

                
                if ( (n = read(sockfd, line_buff, MAXLINE)) < 0)  //read时fd中的数据如果小于要读取的数据,就会引起阻塞?
                {
                    if (errno == ECONNRESET)
                    {
                        close(sockfd);
                        event_list[i].data.fd = -1;
                    }
                    else
                        std::cout << "readline error" << std::endl;
                }
                else if (n == 0)
                {
                    close(sockfd);
                    event_list[i].data.fd = -1;
                }

                line_buff[n] = '\0';
                cout << "read " << line_buff << endl;

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

                //设置用于注测的写操作事件
                ev.events = EPOLLOUT | EPOLLET; //EPOLLOUT:表示对应的文件描述符可以写;

                //修改sockfd上要处理的事件为EPOLLOUT
                //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);

            }
            else if(event_list[i].events & EPOLLOUT) // 如果有数据发送
            {
                sockfd = event_list[i].data.fd;
                write(sockfd, line_buff, n);
               
                //设置用于读操作的文件描述符
                ev.data.fd = sockfd;
                
                //设置用于注测的读操作事件
                ev.events = EPOLLIN | EPOLLET;
                
                //修改sockfd上要处理的事件为EPOLIN
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
            }
        }
    }
    return 0;
}

 

编译命令

linux下编译:g++ epoll.cpp -o epoll

 

命令行简单测试

 curl  192.168.0.250:5000  -d   "phone=123456789&name=Hwei"

 

相关知识

Socket的阻塞模式和非阻塞模式

https://www.cnblogs.com/duhuo/articles/4982386.html

 

如何动态的改变listen监听的个数呢?

如果指定值在源代码中是一个常值,那么增长其大小需要重新编译服务器程序。那么,我们可以为它设定一个缺省值,不过允许通过命令行选项或者环境变量来覆写该值。

void Listen(int fd, int backlog)
{
    char *ptr;
    
    if((ptr = getenv("LISTENQ")) != NULL)
        backlog = atoi(ptr);

    if(listen(fd, backlog) < 0)
        printf("listen error\n");
}

队列已满的情况,如何处理?

 当一个客户SYN到达时,若这个队列是满的,TCP就忽略该分节,也就是不会发送RST。

  这么做的原因在于,队列已满的情况是暂时的,客户TCP如果没收收到RST,就会重发SYN,在队列有空闲的时候处理该请求。如果服务器TCP立即响应一个RST,客户的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不会再次重发SYN。而且客户端也不无区别该套接口的状态,是“队列已满”还是“该端口没有在监听”。

SYN泛滥攻击

向某一目标服务器发送大量的SYN,用以填满一个或多个TCP端口的未完成队列。每个SYN的源IP地址都置成随机数(IP欺骗),这样防止攻击服务器获悉黑客的真实IP地址。通过伪造的SYN装满未完成连接队列,使得合法的SYN不能排上队,导致针对合法用户的服务被拒绝。

 

  防御方法:

  1. 针对服务器主机的方法。增加连接缓冲队列长度和缩短连接请求占用缓冲队列的超时时间。该方式最简单,被很多操作系统采用,但防御性能也最弱。
  2. 针对路由器过滤的方法。由于DDoS攻击,包括SYN-Flood,都使用地址伪装技术,所以在路由器上使用规则过滤掉被认为地址伪装的包,会有效的遏制攻击流量。
  3. 针对防火墙的方法。在SYN请求连接到真正的服务器之前,使用基于防火墙的网关来测试其合法性。它是一种被普遍采用的专门针对SYN-Flood攻击的防御机制。

 

SYN:同步序列编号(Synchronize Sequence Numbers)

【epoll】epoll使用详解(精髓)--研读和修正_第4张图片

 它们的含义是:
SYN表示建立连接,
FIN表示关闭连接,
ACK表示响应,
PSH表示有 
DATA数据传输,
RST表示连接重置。

SYN(synchronous建立联机)

ACK(acknowledgement 确认)

PSH(push传送)

FIN(finish结束)

RST(reset重置)

URG(urgent紧急)

Sequence number(顺序号码)

Acknowledge number(确认号码)
 

三次握手:

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。完成三次握手,客户端与服务器开始传送数据.

第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;

第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
 

参考

Unix网络编程

基于主动网的SYN攻击防御

摘自:https://www.cnblogs.com/coder2012/archive/2013/12/13/3472677.html

 

《listen() 函数》

https://www.cnblogs.com/love-yh/p/7518552.html

code sample

https://blog.csdn.net/shenya1314/article/details/73691088

 

实例代码二

 

多进程Epoll:

#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#include   
#define PROCESS_NUM 10  
static int  
create_and_bind (char *port)  
{  
    int fd = socket(PF_INET, SOCK_STREAM, 0);  
    struct sockaddr_in serveraddr;  
    serveraddr.sin_family = AF_INET;  
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    serveraddr.sin_port = htons(atoi(port));  
    bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));  
    return fd;  
}  
    static int  
make_socket_non_blocking (int sfd)  
{  
    int flags, s;  
 
    flags = fcntl (sfd, F_GETFL, 0);  
    if (flags == -1)  
    {  
        perror ("fcntl");  
        return -1;  
    }  
 
    flags |= O_NONBLOCK;  
    s = fcntl (sfd, F_SETFL, flags);  
    if (s == -1)  
    {  
        perror ("fcntl");  
        return -1;  
    }  
 
    return 0;  
}  
  
#define MAXEVENTS 64  
 
int  
main (int argc, char *argv[])  
{  
    int sfd, s;  
    int efd;  
    struct epoll_event event;  
    struct epoll_event *events;  
 
    sfd = create_and_bind("1234");  
    if (sfd == -1)  
        abort ();  
 
    s = make_socket_non_blocking (sfd);  
    if (s == -1)  
        abort ();  
 
    s = listen(sfd, SOMAXCONN);  
    if (s == -1)  
    {  
        perror ("listen");  
        abort ();  
    }  
 
    efd = epoll_create(MAXEVENTS);  
    if (efd == -1)  
    {  
        perror("epoll_create");  
        abort();  
    }  
 
    event.data.fd = sfd;  
    //event.events = EPOLLIN | EPOLLET;  
    event.events = EPOLLIN;  
    s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);  
    if (s == -1)  
    {  
        perror("epoll_ctl");  
        abort();  
    }  
 
    /* Buffer where events are returned */  
    events = calloc(MAXEVENTS, sizeof event);  
            int k;  
    for(k = 0; k < PROCESS_NUM; k++)  
    {  
        int pid = fork();  
        if(pid == 0)  
        {  
 
            /* The event loop */  
            while (1)  
            {  
                int n, i;  
                n = epoll_wait(efd, events, MAXEVENTS, -1);  
                printf("process %d return from epoll_wait!\n", getpid());  
                                       /* sleep here is very important!*/  
                //sleep(2);  
                                       for (i = 0; i < n; i++)  
                {  
                    if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events &                                    EPOLLIN)))  
                    {  
                        /* An error has occured on this fd, or the socket is not  
                        ready for reading (why were we notified then?) */  
                        fprintf (stderr, "epoll error\n");  
                        close (events[i].data.fd);  
                        continue;  
                    }  
                    else if (sfd == events[i].data.fd)  
                    {  
                        /* We have a notification on the listening socket, which  
                        means one or more incoming connections. */  
                        struct sockaddr in_addr;  
                        socklen_t in_len;  
                        int infd;  
                        char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];  
 
                        in_len = sizeof in_addr;  
                        infd = accept(sfd, &in_addr, &in_len);  
                        if (infd == -1)  
                        {  
                            printf("process %d accept failed!\n", getpid());  
                            break;  
                        }  
                        printf("process %d accept successed!\n", getpid());  
 
                        /* Make the incoming socket non-blocking and add it to the  
                        list of fds to monitor. */  
                        close(infd); 
                    }  
                }  
            }  
        }  
    }  
    int status;  
    wait(&status);  
    free (events);  
    close (sfd);  
    return EXIT_SUCCESS;  
}  

 https://www.jianshu.com/p/362b56b573f4 

建立2000+个链接的测试代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include
#include 
#include 
 
const int MAXLINE = 5;
int count = 1;
 
static int make_socket_non_blocking(int fd)
{
  int flags, s;
 
  flags = fcntl (fd, F_GETFL, 0);
  if (flags == -1)
    {
      perror ("fcntl");
      return -1;
    }
 
  flags |= O_NONBLOCK;
  s = fcntl (fd, F_SETFL, flags);
  if (s == -1)
    {
      perror ("fcntl");
      return -1;
    }
 
  return 0;
}
 
void sockconn()
{
	int sockfd;
	struct sockaddr_in server_addr;
	struct hostent *host;
	char buf[100];
	unsigned int value = 1;
 
	host = gethostbyname("127.0.0.1");
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1) {
		perror("socket error\r\n");
		return;
	}
	
	//setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
	
	//make_socket_non_blocking(sockfd);
 
	bzero(&server_addr, sizeof(server_addr));
 
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(8080);
	server_addr.sin_addr = *((struct in_addr*) host->h_addr);
 
	int cn = connect(sockfd, (struct sockaddr *) &server_addr,
			sizeof(server_addr));
	if (cn == -1) {
		printf("connect error errno=%d\r\n", errno);
		return;
 
	}
//	char *buf = "h";
	sprintf(buf, "%d", count);
	count++;
	write(sockfd, buf, strlen(buf));
	close(sockfd);
	
	printf("client send %s\r\n", buf);
	
	return;
}
 
int main(void) {
 
 int i;
 for (i = 0; i < 2000; i++)
 {
 	  sockconn();
 }
 
 return 0;
}

 

关于ET、LT两种工作模式

 

水平触发LT:

其中LT就是与select和poll类似,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写

 

边缘触发ET:

当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你

 

由此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。

原文链接:https://blog.csdn.net/hahachenchen789/article/details/83276058


4、关于ET、LT两种工作模式
可以得出这样的结论:
ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.

 

epoll中读写数据 的注意事项

(https://blog.csdn.net/ctthuangcheng/article/details/9716715)

 

在一个非阻塞的socket上调用read/write函数,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。

       从字面上看,意思是:

 EAGAIN: 再试一次
 EWOULDBLOCK:如果这是一个阻塞socket, 操作将被block
 perror输出:Resource temporarily unavailable
总结:

       这个错误表示资源暂时不够,可能read时, 读缓冲区没有数据, 或者write时,写缓冲区满了。 

       遇到这种情况,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 时errno设置为EAGAIN。

       所以对于阻塞socket、 read/write返回-1代表网络出错了。但对于非阻塞socket、read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。

本文主要讲述epoll模型(不完全是针对epoll)下读写数据接口使用的注意事项

 

1、read  write
函数原型如下:

#include 
ssize_t read(int filedes, void* buf, size_t nbytes)
ssize_t write(int filedes, const void* buf, size_t nbytes)


其中,read返回实际读取到的字节数。但实际读取的字节很有可能少于指定要读取的字节数nbytes。因此会分为:


①返回值大于0。 读取正常,返回实际读取到的字节数
②返回值等于0。 读取异常,读取到文件filedes结尾处了。这里逻辑上要理解为read已经读取完数据
③返回值小于0(-1)。 读取出错,在处理网络请求时可能是网络异常。着重注意当返回-1,此时errno的值EAGAIN、EWOULLDBLOCK,表示内核对应的读缓冲区为空


而write返回的实际写入字节数正常情况是与制定写入的字节数nbytes相同的,不相等说明写入异常了,着重注意,此时errno的值EAGAIN、EWOULLDBLOCK,表示内核对应的写缓冲区为空。注,EAGAIN等同于EWOULLDBLOCK。

总之,这个错误表示资源暂时不够,可能read时读缓冲区没有数据, 或者write时写缓冲区满了。遇到这种情况,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同时errno设置为EAGAIN。

所以对于阻塞socket、 read/write返回-1代表网络出错了。但对于非阻塞socket、read/write返回-1不一定网络真的出错了。可能支持缓冲区空或者满,这时应该再试,直到Resource available。


综上,对于非阻塞的socket,正确的读写操作为:

LT模式

读: 忽略掉errno = EAGAIN的错误,下次继续读;

写:忽略掉errno = EAGAIN的错误,下次继续写。

对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。

下面来介绍下epoll事件的两种模式LT(水平触发)和ET(边沿触发),根据可以理解为,文件描述符的读写状态发生变化才会触发epoll事件,具体说来如下:二者的差异在于 level-trigger 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger 模式下只有某个 socket 从 unreadable 变为 readable,或从unwritable 变为writable时,epoll_wait 才会返回该 socket。如下两个示意图:

  从socket读数据:      

 往socket写数据:

             

 所以在epoll的ET模式下,正确的读写方式为:

 

读: 只要可读, 就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写, 就一直写,直到数据发送完,或者 errno = EAGAIN


这里的意思是,对于ET模式,相当于我们要自己重写read和write,使其像”原子操作“一样,保证一次read 或 write能够完整的读完缓冲区的数据或者写完要写入缓冲区的数据。因此,实现为用while包住read和write即可。但是对于select或者LT模式,我们可以只使用一次read和write,因为在主程序中会一直while,而事件再下一次select时还会被获取到。但也可以实现为用while包住read和write。从逻辑上讲,一次性把数据读取完整可以保证数据的完整性。

下面来说明这种”原子操作“read和write

int n = 0;
while(1)
{
    nread = read(fd, buf + n, BUFSIZ - 1); //读时,用户进程指定的接收数据缓冲区大小固定,一般要比数据大
    if(nread < 0)
    {
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
            continue;
        }
        else
        {
            break; //or return;
        }
    }
    else if(nread == 0)
    {
        break; //or return. because read the EOF
    }
    else
    {
        n += nread;
    }
}
int data_size = strlen(buf);
int n = 0;
while(1)
{
    nwrite = write(fd, buf + n, data_size);//写时,数据大小一直在变化
    if(nwrite < data_size)
    {
        if(errno == EAGAIN || errno == EWOULDBLOCK)
        {
            continue;
        }
        else
        {
            break;//or return;        
        }
 
    }
    else
    {
        n += nwrite;
        data_size -= nwrite;
    }
}

注参考文章:
http://blog.csdn.net/ctthuangcheng/article/details/9716715

 

正确的accept,accept 要考虑 2 个问题:参考<<UNIX网络编程——epoll的 et,lt关注点>>讲解的更加详细

       (1) LT模式下或ET模式下,阻塞的监听socket, accept 存在的问题

       accept每次都是从已经完成三次握手的tcp队列中取出一个连接,考虑这种情况: TCP 连接被客户端夭折,即在服务器调用 accept 之前,客户端主动发送 RST 终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在 accept 调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上,就绪队列中的其他描述符都得不到处理

 

       解决办法是:把监听套接口设置为非阻塞,当客户在服务器调用 accept 之前中止某个连接时,accept 调用可以立即返回 -1, 这时源自 Berkeley 的实现会在内核中处理该事件,并不会将该事件通知给 epool,而其他实现把 errno 设置为 ECONNABORTED 或者 EPROTO 错误,我们应该忽略这两个错误。

 

       (2) ET 模式下 accept 存在的问题

       考虑这种情况:多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理

 

       解决办法是将监听套接字设置为非阻塞模式,用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept  返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完

 

       综合以上两种情况,服务器应该使用非阻塞地 accept, accept 在 ET 模式下 的正确使用方式为:

 

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");  
}  

 

       一道腾讯后台开发的面试题:

       使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发 socket 可写的事件,如何处理?

 

  • 第一种最普遍的方式:

       需要向 socket 写数据的时候才把 socket 加入 epoll ,等待可写事件。接受到可写事件后,调用 write 或者 send 发送数据。当所有数据都写完后,把 socket 移出 epoll。

 

       这种方式的缺点是,即使发送很少的数据,也要把 socket 加入 epoll,写完后在移出 epoll,有一定操作代价。

 

  •  一种改进的方式:

       开始不把 socket 加入 epoll,需要向 socket 写数据的时候,直接调用 write 或者 send 发送数据。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驱动下写数据,全部数据发送完毕后,再移出 epoll。

 

       这种方式的优点是:数据不多的时候可以避免 epoll 的事件处理,提高效率。

  

 

多路复用

如文初的说明表示,这三者都是 I/O 多路复用机制,且简要介绍了多路复用的定义,那么如何更加直观地了解多路复用呢?这里有张图:

 

【epoll】epoll使用详解(精髓)--研读和修正_第5张图片

对于网页服务器 Nginx 来说,会有很多连接进来, epoll 会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

一般来说以下场合需要使用 I/O 多路复用:

  • 当客户处理多个描述字时(一般是交互式输入和网络套接口)
  • 如果一个服务器既要处理 TCP,又要处理 UDP,一般要使用 I/O 复用
  • 如果一个 TCP 服务器既要处理监听套接口,又要处理已连接套接口

 

讨论

 

#1楼   epoll 自身是基于 IO 多路复用模式的同步IO模型,可实现是基于事件驱动的异步非阻塞的进程模型。不要把进程模型的异步分阻塞看成异步IO。

#2楼  epoll只是非阻塞,不是异步,linux真正的异步是aio系列函数!

#3楼   

调用epoll是会进入等待的(取决设置timeout的值),而且在数据从内核态取到用户态也是阻塞的,严格意义上说,epoll实现的多路复用IO是阻塞同步IO。相对于普通阻塞同步IO,就是能够不开启多线程来处理多个IO请求。资料里说Linux下的真正的AIO还不成熟,不清楚。

https://www.cnblogs.com/zhouyang123200/p/6613302.html

你可能感兴趣的:(linux,网络编程)