分析epoll当中的边缘触发以及水平触发模式与阻塞和非阻塞模式的多种组合

一:连接套接字的 “ET+阻塞模式”

(1)采用循环的方式读取数据

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

/* 最大缓存区大小 */
#define MAX_BUFFER_SIZE 5
/* epoll最大监听数 */
#define MAX_EPOLL_EVENTS 20
/* LT模式 */
#define EPOLL_LT 0
/* ET模式 */
#define EPOLL_ET 1
/* 文件描述符设置阻塞 */
#define FD_BLOCK 0
/* 文件描述符设置非阻塞 */
#define FD_NONBLOCK 1

/* 出错处理 */
void err_exit(char *msg)
{
    perror(msg);
     exit(1);
}


/* 设置文件为非阻塞 */
int set_nonblock(int fd)
{
    int old_flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
    return old_flags;
}

/* 创建socket */
int create_socket(const char *ip, const int port_number)
{
     struct sockaddr_in server_addr;
     int sockfd, reuse = 1;

     memset(&server_addr, 0, sizeof(server_addr));
     server_addr.sin_family = AF_INET;
     server_addr.sin_port = htons(port_number);

     if (inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1)
         err_exit("inet_pton() error");
 
     if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
         err_exit("socket() error");

    /* 设置复用socket地址 */
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
         err_exit("setsockopt() error");

     if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
        err_exit("bind() error");

     if (listen(sockfd, 5) == -1)
       err_exit("listen() error");

   return sockfd;
}


/* 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) */
void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type)
{
     struct epoll_event ep_event;
     ep_event.data.fd = fd;
     ep_event.events = EPOLLIN;

     /* 如果是ET模式,设置EPOLLET */
     if (epoll_type == EPOLL_ET)
         ep_event.events |= EPOLLET;

     /* 设置是否阻塞 */
    if (block_type == FD_NONBLOCK)
         set_nonblock(fd);

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event);
}

/* 带循环的ET处理流程 */
void epoll_et_loop(int sockfd)
{
    char buffer[MAX_BUFFER_SIZE];
    int ret;

    printf("带循环的ET读取数据开始...\n");
    while (1)
     {
        memset(buffer, 0, MAX_BUFFER_SIZE);
        ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
        if (ret == -1)
        {
             if (errno == EAGAIN || errno == EWOULDBLOCK)
             {
                 printf("循环读完所有数据!!!\n");
                 break;
             }
             close(sockfd);
             break;
         }
         else if (ret == 0)
         {
              printf("客户端主动关闭请求!!!\n");
             close(sockfd);
             break;
         }
         else
             printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     }
     printf("带循环的ET处理结束!!!\n");
}


/* 不带循环的ET处理流程,比epoll_et_loop少了一个while循环 */
void epoll_et_nonloop(int sockfd)
{
    char buffer[MAX_BUFFER_SIZE];
     int ret;
     printf("不带循环的ET模式开始读取数据...\n");
     memset(buffer, 0, MAX_BUFFER_SIZE);
     ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
     if (ret > 0)
     {
        printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     }
     else
     {
         if (ret == 0)
             printf("客户端主动关闭连接!!!\n");
        close(sockfd);
     }
    printf("不带循环的ET模式处理结束!!!\n");
}
/* 处理epoll的返回结果 */
void epoll_process(int epollfd, struct epoll_event *events, int number, int sockfd, int epoll_type, int block_type)
{
     struct sockaddr_in client_addr;
     socklen_t client_addrlen;
     int newfd, connfd;
     int i;
     for(i = 0; i < number; i++)
     {
         newfd = events[i].data.fd;
         if (newfd == sockfd)
         {
             printf("accept()开始...\n");
             /* 休眠3秒,模拟一个繁忙的服务器,不能立即处理accept连接 */
             printf("开始休眠3秒...\n");
             sleep(3);
             printf("休眠3秒结束!!!\n");
 
            client_addrlen = sizeof(client_addr);
            connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
            printf("connfd = %d\n", connfd);

             /* 注册已链接的socket到epoll,并设置是LT还是ET,是阻塞还是非阻塞 */
             addfd_to_epoll(epollfd, connfd, epoll_type, block_type);
             printf("accept()结束!!!\n");
        }
        else if (events[i].events & EPOLLIN)
        {
            if (epoll_type == EPOLL_ET)
            {
                printf("============================>边缘触发开始...\n");
                /* 带循环的ET模式 */
                epoll_et_loop(newfd);
                 /* 不带循环的ET模式 */
                 //epoll_et_nonloop(newfd);
             }
        else
            printf("其他事件发生...\n");
        }
     }
}
/* main函数 */
int main(int argc, const char *argv[])
{
    int sockfd, epollfd, number;
    sockfd = create_socket(argv[1], atoi(argv[2]));
    struct epoll_event events[MAX_EPOLL_EVENTS];
    if (argc < 3)
    {
         fprintf(stderr, "usage:%s ip_address port_number\n", argv[0]);
         exit(1);
     }
 
    /*linux内核2.6.27版的新函数,和epoll_create(int size)一样的功能,并去掉了无用的size参数*/
     if ((epollfd = epoll_create1(0)) == -1)
         err_exit("epoll_create1() error");

 
     /*把监听套接字设置成 “LT+非阻塞模式”,在epoll中,
       因为epoll_wait返回时,肯定是有事件发生的,所以
      设置成阻塞模式无意义。*/


     addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);

     while (1)
     {
         number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
         if (number == -1)
         {
             err_exit("epoll_wait() error");
         }
         else
         {
             /*针对客户端连接的套接字使用的是 “ET+阻塞模式"*/
            epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);
         }
     }
     close(sockfd);
     return 0;
}

客户端连接服务端,并发送123456789。
在这里插入图片描述
在服务端可以看到,接收数据时,即使可以将数据全部接收完毕,但是会比较混乱。
在这里插入图片描述
这个时候,再次使用客户端连接服务端,发现连接不上了,因为,卡在recv函数这里了。
在这里插入图片描述

(2)采用非循环的方式读取数据

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

/* 最大缓存区大小 */
#define MAX_BUFFER_SIZE 5
/* epoll最大监听数 */
#define MAX_EPOLL_EVENTS 20
/* LT模式 */
#define EPOLL_LT 0
/* ET模式 */
#define EPOLL_ET 1
/* 文件描述符设置阻塞 */
#define FD_BLOCK 0
/* 文件描述符设置非阻塞 */
#define FD_NONBLOCK 1

/* 出错处理 */
void err_exit(char *msg)
{
    perror(msg);
     exit(1);
}


/* 设置文件为非阻塞 */
int set_nonblock(int fd)
{
    int old_flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
    return old_flags;
}

/* 创建socket */
int create_socket(const char *ip, const int port_number)
{
     struct sockaddr_in server_addr;
     int sockfd, reuse = 1;

     memset(&server_addr, 0, sizeof(server_addr));
     server_addr.sin_family = AF_INET;
     server_addr.sin_port = htons(port_number);

     if (inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1)
         err_exit("inet_pton() error");
 
     if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
         err_exit("socket() error");

    /* 设置复用socket地址 */
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
         err_exit("setsockopt() error");

     if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
        err_exit("bind() error");

     if (listen(sockfd, 5) == -1)
       err_exit("listen() error");

   return sockfd;
}

/* 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) */
void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type)
{
     struct epoll_event ep_event;
     ep_event.data.fd = fd;
     ep_event.events = EPOLLIN;

     /* 如果是ET模式,设置EPOLLET */
     if (epoll_type == EPOLL_ET)
         ep_event.events |= EPOLLET;

     /* 设置是否阻塞 */
    if (block_type == FD_NONBLOCK)
         set_nonblock(fd);

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event);
}

/* 带循环的ET处理流程 */
void epoll_et_loop(int sockfd)
{
    char buffer[MAX_BUFFER_SIZE];
    int ret;

    printf("带循环的ET读取数据开始...\n");
    while (1)
     {
        memset(buffer, 0, MAX_BUFFER_SIZE);
        ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
        if (ret == -1)
        {
             if (errno == EAGAIN || errno == EWOULDBLOCK)
             {
                 printf("循环读完所有数据!!!\n");
                 break;
             }
             close(sockfd);
             break;
         }
         else if (ret == 0)
         {
              printf("客户端主动关闭请求!!!\n");
             close(sockfd);
             break;
         }
         else
             printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     }
     printf("带循环的ET处理结束!!!\n");
}


/* 不带循环的ET处理流程,比epoll_et_loop少了一个while循环 */
void epoll_et_nonloop(int sockfd)
{
    char buffer[MAX_BUFFER_SIZE];
     int ret;
     printf("不带循环的ET模式开始读取数据...\n");
     memset(buffer, 0, MAX_BUFFER_SIZE);
     ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
     if (ret > 0)
     {
        printf("收到消息:%s, 共%d个字节\n", buffer, ret);
     }
     else
     {
         if (ret == 0)
             printf("客户端主动关闭连接!!!\n");
        close(sockfd);
     }
    printf("不带循环的ET模式处理结束!!!\n");
}

/* 处理epoll的返回结果 */
void epoll_process(int epollfd, struct epoll_event *events, int number, int sockfd, int epoll_type, int block_type)
{
     struct sockaddr_in client_addr;
     socklen_t client_addrlen;
     int newfd, connfd;
     int i;
     for(i = 0; i < number; i++)
     {
         newfd = events[i].data.fd;
         if (newfd == sockfd)
         {
             printf("accept()开始...\n");
             /* 休眠3秒,模拟一个繁忙的服务器,不能立即处理accept连接 */
             printf("开始休眠3秒...\n");
             sleep(3);
             printf("休眠3秒结束!!!\n");
 
            client_addrlen = sizeof(client_addr);
            connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
            printf("connfd = %d\n", connfd);

             /* 注册已链接的socket到epoll,并设置是LT还是ET,是阻塞还是非阻塞 */
             addfd_to_epoll(epollfd, connfd, epoll_type, block_type);
             printf("accept()结束!!!\n");
        }
        else if (events[i].events & EPOLLIN)
        {
            if (epoll_type == EPOLL_ET)
            {
                printf("============================>边缘触发开始...\n");
                /* 带循环的ET模式 */
                //epoll_et_loop(newfd);
                 /* 不带循环的ET模式 */
                 epoll_et_nonloop(newfd);
             }
        else
            printf("其他事件发生...\n");
        }
     }
}

/* main函数 */
int main(int argc, const char *argv[])
{
    int sockfd, epollfd, number;
    sockfd = create_socket(argv[1], atoi(argv[2]));
    struct epoll_event events[MAX_EPOLL_EVENTS];
    if (argc < 3)
    {
         fprintf(stderr, "usage:%s ip_address port_number\n", argv[0]);
         exit(1);
     }
 
    /*linux内核2.6.27版的新函数,和epoll_create(int size)一样的功能,并去掉了无用的size参数*/
     if ((epollfd = epoll_create1(0)) == -1)
         err_exit("epoll_create1() error");

 
     /*把监听套接字设置成 “LT+非阻塞模式”,在epoll中,
       因为epoll_wait返回时,肯定是有事件发生的,所以
      设置成阻塞模式无意义。*/


     addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);

     while (1)
     {
         number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
         if (number == -1)
         {
             err_exit("epoll_wait() error");
         }
         else
         {
             /*针对客户端连接的套接字使用的是 “ET+阻塞模式"*/
            epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);
         }
     }
     close(sockfd);
     return 0;
}

虽然可以跳出循环回到epoll_wait函数中继续等待新事件,但是输出的数据比较混乱。
在这里插入图片描述
在这里插入图片描述
二:连接套接字的 “ET+非阻塞模式”
(1)不带循环的方式处理数据,结果和上面是一样的,可以回到epoll_wait当中去,继续等待新的事件,但就是输出数据时比较混乱。

(2)带循环的方式处理数据,不会卡在recv函数这里,因为是非阻塞,所以会break回到epoll_wait当中去,继续等待新的事件,

三:连接套接字的 “LT+阻塞模式” 和连接套接字的 “LT+非阻塞模式”
在这两种情况下,效果都差不多,可以正常收到数据以及回到epoll_wait函数当中去,但是还是建议采用“LT+非阻塞模式”的方式,效率上会高一些。

四:监听套接字的LT模式
对于对监听套接字来说,一般采用LT模式,因为,如果多个客户端接入时,我们没有及时利用accept函数进行处理的话,它还可以返回的触发这些事件,以提醒服务端进行处理。需要注意的是,对于监听套接字的LT模式来说,采用阻塞还是非阻塞意义不大(在epoll当中)。

五:监听套接字的ET模式
对对监听套接字来说,如果采用ET模式,在多个客户端连入时,因为是ET模式,系统只会提醒你一次,所以可能有遗漏的情况,会照成不能对客户端进行及时的接入,同样需要注意的是,对于监听套接字的ET模式来说,采用阻塞还是非阻塞意义不大(在epoll当中)。

六:总结
1:对于监听套接字建议采用LT+非阻塞模式
2:对于连接套接字建议采用LT+非阻塞模式或者ET+非阻塞模式。
参考:https://zhuanlan.zhihu.com/p/174544112

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值