zoukankan      html  css  js  c++  java
  • TCP/IP网络编程之优于select的epoll(二)

    基于epoll的回声服务端

    TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例。

    echo_epollserv.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUF_SIZE 100
    #define EPOLL_SIZE 50
    void error_handling(char *buf);
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        struct sockaddr_in serv_adr, clnt_adr;
        socklen_t adr_sz;
        int str_len, i;
        char buf[BUF_SIZE];
    
        struct epoll_event *ep_events;
        struct epoll_event event;
        int epfd, event_cnt;
    
        if (argc != 2) {
            printf("Usage : %s <port>
    ", argv[0]);
            exit(1);
        }
    
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
            error_handling("bind() error");
        if (listen(serv_sock, 5) == -1)
            error_handling("listen() error");
    
        epfd = epoll_create(EPOLL_SIZE);
        ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    
        event.events = EPOLLIN;
        event.data.fd = serv_sock;
        epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
        while (1)
        {
            event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
            if (event_cnt == -1)
            {
                puts("epoll_wait() error");
                break;
            }
    
            for (i = 0; i < event_cnt; i++)
            {
                if (ep_events[i].data.fd == serv_sock)
                {
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock =
                        accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    event.events = EPOLLIN;
                    event.data.fd = clnt_sock;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                    printf("connected client: %d 
    ", clnt_sock);
                }
                else
                {
                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0) // close request!
                    {
                        epoll_ctl(
                            epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                        close(ep_events[i].data.fd);
                        printf("closed client: %d 
    ", ep_events[i].data.fd);
                    }
                    else
                    {
                        write(ep_events[i].data.fd, buf, str_len); // echo!
                    }
                }
            }
        }
        close(serv_sock);
        close(epfd);
        return 0;
    }
    
    void error_handling(char *buf)
    {
        fputs(buf, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    之前解释过关键代码,而且程序结构与select方式没有区别,故省略代码说明。

    条件触发和边缘触发

    条件触发的方式中,只要输入缓冲有数据就会一直通知该事件。例如,服务端输入缓冲收到50字节的数据时,服务端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务端读取20字节后还剩30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。而边缘触发中输入缓冲收到数据时仅注册一次事件,即使输入缓冲中还留有数据,也不会再进行注册

    接下来通过代码了解条件触发的事件注册方式

    echo_EPLTserv.c 

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUF_SIZE 4
    #define EPOLL_SIZE 50
    void error_handling(char *buf);
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        struct sockaddr_in serv_adr, clnt_adr;
        socklen_t adr_sz;
        int str_len, i;
        char buf[BUF_SIZE];
    
        struct epoll_event *ep_events;
        struct epoll_event event;
        int epfd, event_cnt;
    
        if (argc != 2) {
            printf("Usage : %s <port>
    ", argv[0]);
            exit(1);
        }
    
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
            error_handling("bind() error");
        if (listen(serv_sock, 5) == -1)
            error_handling("listen() error");
    
        epfd = epoll_create(EPOLL_SIZE);
        ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    
        event.events = EPOLLIN;
        event.data.fd = serv_sock;
        epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
        while (1)
        {
            event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
            if (event_cnt == -1)
            {
                puts("epoll_wait() error");
                break;
            }
    
            puts("return epoll_wait");
            for (i = 0; i < event_cnt; i++)
            {
                if (ep_events[i].data.fd == serv_sock)
                {
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    event.events = EPOLLIN;
                    event.data.fd = clnt_sock;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                    printf("connected client: %d 
    ", clnt_sock);
                }
                else
                {
                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0) // close request!
                    {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                        close(ep_events[i].data.fd);
                        printf("closed client: %d 
    ", ep_events[i].data.fd);
                    }
                    else
                    {
                        write(ep_events[i].data.fd, buf, str_len); // echo!
                    }
                }
            }
        }
        close(serv_sock);
        close(epfd);
        return 0;
    }
    
    void error_handling(char *buf)
    {
        fputs(buf, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    上述示例与之前的echo_epollserv.c之间的差异如下:

    •  第10行:将调用read函数时使用的缓冲大小缩减为4字节
    • 第58行:插入验证epoll_wait函数调用次数的语句

    减少缓冲大小是为了阻止服务端一次性读取接收的数据。换言之,调用read函数后,输入缓冲中仍然有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出“return epoll_wait”字符串。前提是条件触发的工作方式与之前描述一致。接下来观察运行结果

    编译echo_EPLTserv.c 并运行

    # gcc echo_EPLTserv.c -o echo_EPLTserv
    # ./echo_EPLTserv 8500
    return epoll_wait
    connected client: 5 
    return epoll_wait
    connected client: 6 
    return epoll_wait
    return epoll_wait
    return epoll_wait
    return epoll_wait
    return epoll_wait
    closed client: 5 
    return epoll_wait
    return epoll_wait
    return epoll_wait
    return epoll_wait
    return epoll_wait
    closed client: 6 
    

      

    运行echo_client ONE:

    # ./echo_client 127.0.0.1 8500
    Connected...........
    Input message(Q to quit): It's my life 
    Message from server: It's my life 
    Input message(Q to quit): q
    

      

    运行echo_client TWO:

    # ./echo_client 127.0.0.1 8500
    Connected...........
    Input message(Q to quit): It's your life
    Message from server: It's your life
    Input message(Q to quit): q
    

      

    从运行结果可以看出,每当接收到客户端数据时,都会注册该事件,并且多次调用epoll_wait函数

    边缘触发的服务端实现中必知的两点:

    • 通过error变量验证错误原因
    • 为了完成非阻塞I/O,更改套接字特性

    Linux套接字相关函数一般通过返回-1通知发生错误,虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息,Linux声明了全局变量:int errno;为了访问该变量,需要引入error.h头文件,因为此头文件中有上述变量的extern声明。另外,每种函数发生错误时,保存到error变量的值都不同。

    下面讲解将套接字改为非阻塞方式的方法,Linux提供更改或读取文件属性的如下方法:

    #include <fcntl.h>
    int fcntl(int filedes, int cmd, ...); //成功时返回cmd参数相关值,失败时返回-1
    

      

    • filedes:属性更改目标的文件描述符
    • cmd:表示函数调用的目的

    从上述声明中可以看到,fcntl具有可变参数形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性(int型)。反之,如果传递F_SETFL,可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下两条语句 

    int flag = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
    

      

    通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read和write函数时,无论是否存在数据,都会形成非阻塞文件(套接字) 

    echo_EPETserv.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUF_SIZE 4
    #define EPOLL_SIZE 50
    void setnonblockingmode(int fd);
    void error_handling(char *buf);
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        struct sockaddr_in serv_adr, clnt_adr;
        socklen_t adr_sz;
        int str_len, i;
        char buf[BUF_SIZE];
    
        struct epoll_event *ep_events;
        struct epoll_event event;
        int epfd, event_cnt;
    
        if (argc != 2) {
            printf("Usage : %s <port>
    ", argv[0]);
            exit(1);
        }
    
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
            error_handling("bind() error");
        if (listen(serv_sock, 5) == -1)
            error_handling("listen() error");
    
        epfd = epoll_create(EPOLL_SIZE);
        ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    
        setnonblockingmode(serv_sock);
        event.events = EPOLLIN;
        event.data.fd = serv_sock;
        epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
        while (1)
        {
            event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
            if (event_cnt == -1)
            {
                puts("epoll_wait() error");
                break;
            }
    
            puts("return epoll_wait");
            for (i = 0; i < event_cnt; i++)
            {
                if (ep_events[i].data.fd == serv_sock)
                {
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    setnonblockingmode(clnt_sock);
                    event.events = EPOLLIN | EPOLLET;
                    event.data.fd = clnt_sock;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                    printf("connected client: %d 
    ", clnt_sock);
                }
                else
                {
                    while (1)
                    {
                        str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                        if (str_len == 0) // close request!
                        {
                            epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                            close(ep_events[i].data.fd);
                            printf("closed client: %d 
    ", ep_events[i].data.fd);
                            break;
                        }
                        else if (str_len < 0)
                        {
                            if (errno == EAGAIN)
                                break;
                        }
                        else
                        {
                            write(ep_events[i].data.fd, buf, str_len); // echo!
                        }
                    }
                }
            }
        }
        close(serv_sock);
        close(epfd);
        return 0;
    }
    
    void setnonblockingmode(int fd)
    {
        int flag = fcntl(fd, F_GETFL, 0);
        fcntl(fd, F_SETFL, flag | O_NONBLOCK);
    }
    
    void error_handling(char *buf)
    {
        fputs(buf, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    • 第11行:为了验证边缘触发的工作方式,将缓冲设置为4个字节
    • 第61行:为观察事件发生数而添加的输出字符串的语句
    • 第68、69行:第68行将accept函数创建的套接字改为非阻塞模式,第69行向EPOLLIN添加EPOLLET标志,将套接字事件注册方式改为边缘触发
    • 第76、78行:之前的条件触发回声服务端中没有while循环,边缘触发方式中,发生事件时需要读取输入缓冲中的所有数据,因此需要循环调用read函数,如第78行所示
    • 第86行:read函数返回-1且errno值为EAGAIN时,意味着读取了输入缓冲中的全部数据,因此需要通过break语句跳出第76行的循环

    编译echo_EPETserv.c运行

    # ./echo_EPETserv 8500
    return epoll_wait
    connected client: 5 
    return epoll_wait
    return epoll_wait
    return epoll_wait
    return epoll_wait
    closed client: 5 
    

      

    运行客户端程序

    # ./echo_client 127.0.0.1 8500
    Connected...........
    Input message(Q to quit): I like computer programming
    Message from server: I like computer programming
    Input message(Q to quit): Do you like computer programming?
    Message from server: Do you like computer programming?
    Input message(Q to quit): Good bye
    Message from server: Good bye
    Input message(Q to quit): q
    

      

    上述运行结果中需要注意的是,客户端发送消息次数和服务端epoll_wait函数调用次数。客户端从请求连接到断开连接共发送数据5次,服务端也相应产生5个事件

    条件触发和边缘触发孰优孰劣

    我们从理论和代码的角度充分理解了条件触发和边缘触发,但仅凭这些还无法理解边缘触发相对于条件触发的优点。边缘触发可以做到分离接收数据和处理时间的时间点。虽然比较简单,但非常准确有力地说明了边缘触发的优点,现在我们来看图1-1

     

    图1-1   理解边缘触发

    图1-1的运行流程如下:

    • 服务端分别从客户端A、B、C接收数据
    • 服务端按照A、B、C的顺序重新组合收到的数据
    • 组合的数据将发送给任意主机

    为了完成该过程,若能按如下流程运行程序,服务端的实现并不难

    • 客户端按照A、B、C的顺序连接服务端,并依序向服务端发送数据
    • 需要接收数据的客户端应在客户端A、B、C之前连接到服务端并等待

    但现实中可能频繁出现如下这些情况,换言之,如下情况更符合实际

    • 客户端C和B向服务端发送数据,但A尚未连接到服务端
    • 客户端A、B、C乱序发送数据
    • 服务端已收到数据,但要接收数据的目标客户端还未连接到服务端

    因此,即使输入缓冲收到数据(注册相应事件),服务端也能决定读取和处理这些数据的时间点,这样就给服务端的实现带来巨大的灵活性。

    那么,条件触发中无法区分数据接收和处理吗?并非不可能。但在输入缓冲收到数据的情况下,如果不读取(延迟处理),则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加,服务端能承受吗?这在现实中是不合理的

    条件触发和边缘触发的区别主要应从服务端实现模型的角度讨论,从实现模型来看,边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”

  • 相关阅读:
    有没有可能两个不相等的对象有有相同的 hashcode?
    适配器模式和装饰器模式有什么区别?
    JRE、JDK、JVM 及 JIT 之间有什么不同?
    我们能在 Switch 中使用 String 吗?
    解释对象/关系映射集成模块?
    解释 Java 堆空间及 GC?
    List、Set、Map 和 Queue 之间的区别?
    Java 中,Comparator 与 Comparable 有什么不同?
    两个相同的对象会有不同的的 hash code 吗?
    Java 中的编译期常量是什么?使用它又什么风险?
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9696641.html
Copyright © 2011-2022 走看看