一、条件触发和边缘触发

        在《 网络编程(21)—— 使用epoll进行IO复用》一文中,我们介绍了在linux平台下使用epoll进行IO复用的方法。本文主要介绍epoll中进行IO复用的两种触发方式:条件触发和边缘触发。

1.1 什么是条件触发和边缘触发?

       epoll进行IO复用的基本原理我们可以概括如下:

1、 使用epoll_create()创建用来保存epoll文件的内存空间。

2、 使用epoll_ctl()在上文的内存空间中注册需要监视的文件描述符。

3、 使用epoll_wait()监视已经注册好的文件描述符,只要有描述符触发要读或写的epool_event,epoll_wait()就会返回,我们就可以进行后续的读写操作了。

        我们所说的条件触发和边缘触发就发生在第三步,是两种不同的触发epoll_event的方式。以触发读的epool_event为例,解释下条件触发和边缘触发的区别:

当服务端的socket接收到客户端传过来的数据时,数据先被缓存到socket的输入缓冲区。如果在条件触发的方式下只要数据缓冲区有数据,操作系统就会一直触发读的

epool_event,直到我们把所有数据都读上来,缓冲区的数据为空为止。而在边缘触发的方式下,在socket的输入缓冲区的数据从0到有的这个时机才会触发一次读的epoll_event(或者说是每接收到客户端一次数据就触发一次),这就要求我们需要把输入缓冲区的所有数据一次性读完,直到下一次缓冲区数据从0到有再次触发epoll_event。

1.2 条件触发和边缘触发的设置方法

        我们使用epoll_ctl()进行socket的注册前,需要创建一个epoll_event结构体变量,通过设置这个变量的events字段值,可以选择使用条件触发方式还是边缘触发方式。
struct epoll_event event;
……
event.events=EPOLLIN;//默认是设置条件触发方式
//event.events=EPOLLIN|EPOLLET;增加了EPOLLET可选项后是设置了边缘触发方式。
event.data=socket_fd;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,socket_fd,&event);

二、编写条件触发方式的服务端

        我们在《网络编程(21)—— 使用epoll进行IO复用》的基础之上,编写如下基于epoll条件触发的服务端。与不同的是,请注意第55行代码,我们每次只获取输入缓冲区中一个字节的数据,然后打印到屏幕上。如果epoll的条件触发特性真如我们在前面解释的一样,那么我们发送5个字节的数据,epoll_wait()将会返回6次(包含发送的最后一个’\0’),而且会在屏幕上打印6次” epoll_wait() called,recv:X”的字符串。

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<string.h>

#define BUF_SIZE 30
#define EPOLL_SIZE 30

int main()
{
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    int clnt_addr_sz,str_len;
    char buf[BUF_SIZE];
    
    int ep_fd,event_cnt,i;
    struct epoll_event event;
    struct epoll_event* pevents;

    serv_sock=socket(AF_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(atoi("8888"));

    bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));

    listen(serv_sock,5);
    
    ep_fd=epoll_create(EPOLL_SIZE);
    pevents=malloc(sizeof(event)*EPOLL_SIZE);

    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    epoll_ctl(ep_fd,EPOLL_CTL_ADD,serv_sock,&event);
    
    while(1)
    {
        event_cnt=epoll_wait(ep_fd,pevents,EPOLL_SIZE,-1);
        for(i=0;i<event_cnt;i++)
        {
            if(serv_sock==pevents[i].data.fd)
            {
                clnt_addr_sz=sizeof(clnt_addr);
                clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
                event.events=EPOLLIN;
                event.data.fd=clnt_sock;
                epoll_ctl(ep_fd,EPOLL_CTL_ADD,clnt_sock,&event);
            }
            else
            {
                str_len = read(pevents[i].data.fd,buf,1);
                if(str_len<=0)
                {
                    close(pevents[i].data.fd);
                    epoll_ctl(ep_fd,EPOLL_CTL_DEL,pevents[i].data.fd,NULL);
                }
                buf[str_len]=0;
                printf("epoll_wait() called,recv:%s\n",buf);
                write(pevents[i].data.fd,buf,str_len);
            }
        }
    }

    close(serv_sock);
    close(ep_fd);
    return 0;
}
        是不是和我们预期的一致呢,请看下面的运行结果。我们在客户端输入“hello”:
[Hyman@Hyman-PC csdn]$ ./clnt 127.0.0.1 8888
Hello
        然后在服务器上观察服务器的响应状态:
[Hyman@Hyman-PC csdn]$ ./serv
epoll_wait() called,recv:h
epoll_wait() called,recv:e
epoll_wait() called,recv:l
epoll_wait() called,recv:l
epoll_wait() called,recv:o
epoll_wait() called,recv:
        果然和我们预期的一致。也就是说我们对epoll条件触发的解释完全正确。

三、编写边缘触发方式的服务端

        在编写边缘触发的epoll服务器之前,我们先来做个试验。在上文中的epoll服务器中设置触发方式为边缘触发,查看运行程序会出现什么效果。改动很简单,只要将第49行的代码增加一个EPOLLET选项。
event.events=EPOLLIN|EPOLLET;
        我们可以对结果也进行一个预期,因为边缘触发方式是在输入缓冲区从无到有数据时的那个时间点触发一次epoll_event,所以结果只会输出一个字符’h’。是不是这样呢?请看下面的执行结果:
[Hyman@Hyman-PC csdn]$ ./serv
epoll_wait() called,recv:h
        当我们在客户端输入hello的字符串时,服务端果然只打印出了一次” epoll_wait() called,recv:h”的字符串,只输出一个’h’字符,这也就证明了,只有在接收到客户端的一次数据后才触发了一次epoll_event。考虑到epoll边缘触发的特性,我们编写服务端时必须一次性把输入缓冲区的数据读完。我们将之前条件触发的服务端改成下面的样子。

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<string.h>

#define BUF_SIZE 30
#define EPOLL_SIZE 30

int main()
{
    int serv_sock,clnt_sock;
    struct sockaddr_in serv_addr,clnt_addr;
    int clnt_addr_sz,str_len,recv_cnt,recv_sum;
    char buf[BUF_SIZE];
    
    int ep_fd,event_cnt,i;
    struct epoll_event event;
    struct epoll_event* pevents;

    serv_sock=socket(AF_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(atoi("8888"));

    bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));

    listen(serv_sock,5);
    
    ep_fd=epoll_create(EPOLL_SIZE);
    pevents=malloc(sizeof(event)*EPOLL_SIZE);

    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    epoll_ctl(ep_fd,EPOLL_CTL_ADD,serv_sock,&event);
    
    while(1)
    {
        event_cnt=epoll_wait(ep_fd,pevents,EPOLL_SIZE,-1);
        for(i=0;i<event_cnt;i++)
        {
            if(serv_sock==pevents[i].data.fd)
            {
                clnt_addr_sz=sizeof(clnt_addr);
                clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
                event.events=EPOLLIN|EPOLLET;
                event.data.fd=clnt_sock;
                epoll_ctl(ep_fd,EPOLL_CTL_ADD,clnt_sock,&event);
            }
            else
            {
                recv_sum=0;
                recv_cnt=recv(pevents[i].data.fd,buf,BUF_SIZE,MSG_DONTWAIT|MSG_PEEK);
                while(1)
                {
                    str_len=read(pevents[i].data.fd,buf,BUF_SIZE);
                    if(str_len<=0)
                    {
                        close(pevents[i].data.fd);
                        epoll_ctl(ep_fd,EPOLL_CTL_DEL,pevents[i].data.fd,NULL);
                        break;
                    }
                    recv_sum+=str_len;
                    printf("epoll_wait() called,recv:%s\n",buf);
                    write(pevents[i].data.fd,buf,str_len);
                    if(recv_sum>=recv_cnt)
                        break;
                }
            }
        }
    }

    close(serv_sock);
    close(ep_fd);
    return 0;
}

        请注意第56行,我们利用recv函数检查输入缓冲区是否有数据,并获取数据的字节数。紧接着在57~71行,循环读取输入缓冲区中的数据,直到读的数据数累加到我们探测到的字节数,这样就保证了每次读取都会将输入缓冲区中的数据一次性全部读完。
执行结果如下:
客户端:
[Hyman@Hyman-PC csdn]$ ./clnt 127.0.0.1 8888
hello
the message from server:hello

服务端:
[Hyman@Hyman-PC csdn]$ ./serv 
epoll_wait() called,recv:hello



Github位置:
https://github.com/HymanLiuTS/NetDevelopment
克隆本项目:
git clone git@github.com:HymanLiuTS/NetDevelopment.git
获取本文源代码:
git checkout NL49







Logo

更多推荐