[Linux网络]高级I/O与多路转接详解

一、IO效率的本质

        以read && write在网络层面为例,这两个函数本质是进行OS和用户层之间的数据拷贝工作。当成功建立了一个连接之后,对于接收方来说如果发送方一直不发送数据,那么接收方的接收缓冲区就没有数据,read就会一直阻塞式等待,对于发送方而言也一样,如果发送缓冲区满了write也要阻塞式的等待。所以IO不仅仅是进行拷贝的操作,更多时间是花在了等待上。

        综上所述:IO = 等待 + 拷贝

        要想进行read && write的拷贝工作,就必须等待条件的就绪,这里的条件称:读写事件 

        所以什么叫做高效的IO呢?单位时间内,IO过程中等待的比重越小,IO效率就越高。所以提高IO效率的策略基本上都是在减少IO过程中等待的时间比重。 

二、 五种IO模型

1. 阻塞式IO

        IO操作会阻塞进程的执行,直到IO操作结束后,才运行后续代码。0c765c66a56843cbb6c6f6fa979a284a.png

2. 非阻塞式IO

        IO操作不会阻塞进程,调用IO函数后没有数据会立即返回EWOULDBLOCK错误码,进程可以执行后续代码。一般配合着循环进行轮询检测读写事件是否就绪。

078609868cb44426b73ae1a64eceac7a.png

3. 信号驱动式IO

        在信号驱动 I/O 模型中,进程可以提供一个信号处理程序,当 I/O 操作准备好时,操作系统会向进程发送一个信号,进程可以在信号处理程序中处理 I/O 操作。

        避免了不必要的轮询,提高了效率。

4. IO多路复用(IO多路转接)   

        将等待和拷贝两个操作分开,等待的只负责等待,有IO事件就绪后会通知拷贝函数实现用户层和内核的数据拷贝工作。

5. 异步IO

        异步 I/O 模型中,进程发起 I/O 操作后可以继续执行其他任务,I/O 操作的完成由操作系统通知进程,通常通过回调函数或通知机制。

6. 同步IO和异步IO

        同步IO和异步IO的主要区别是在进行IO的时候,有没有进行等待。对于异步IO来说只是发起了IO操作,但不参与IO的等待,只是在最后拿到结果而已。

        这里的同步IO和线程同步是两个概念,线程同步是访问临界资源的时候,通过信号量、条件变量等方式规定多线程先后访问的顺序。

三、 非阻塞IO的实现

1. fcntl函数

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,

                                        struct sockaddr *src_addr, socklen_t *addrlen); 

        如图所示,以recv、recvfrom为例,如果我们想给一个IO函数操作设置为非阻塞,这些读写函数提供了一个flag标志位,当我们赋值为MSG_DONTWAIT时,就可以设置为非阻塞等待了。同时也可以在open文件打开时进行选项的设置。

        但归根结底是对于文件描述符的设置,而且用IO函数进行的设置只是对于该函数生效,如果想任何函数对该文件进行IO时都是非阻塞的话。可以用fcntl函数,该函数可以对文件描述符的属性做各种操作。

        所以设置IO操作为非阻塞很多方法,但fcntl函数也是最通用的。

#include <unistd.h>   

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

 2. 实现非阻塞IO

        (1)阻塞式IO代码:

#include <iostream>
#include <cstdio>

int main()
{
    while(true)
    {
        std::cout << "Please Enter# ";
        //上面的输出内容,没有换行式的刷新缓冲区,要手动刷新缓冲区,让Please Enter显示到屏幕上
        fflush(0);  
        char buffer[1024];
        ssize_t n = read(0, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "echo# " <<buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "read done" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
    return 0;
}

        (2)非阻塞IO代码

        设置文件描述符为非阻塞:

void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0)
    {
        std::cerr << "fctnl error" << std::endl;
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

        对于IO操作函数,在非阻塞情况下,读错出分为两种情况,一种是没有数据,直接返回-1,并设置错误码为EWODLBLOCK,另一种则是真的出错了,所以我们在判断IO函数返回值为负数时,还要判断一下是不是真的出错了。 

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>

int main()
{
    while(true)
    {
        SetNonBlock(0);
        std::cout << "Please Enter# ";
        fflush(0);
        char buffer[1024];
        ssize_t n = read(0, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "echo# " <<buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "read done" << std::endl;
            break;
        }
        else
        {
            if(errno == EWOULDBLOCK)
            {
                //do other thing
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
    }
}

四、 IO多路转接-select

1. select接口介绍

        IO操作:等待 + 拷贝,select只负责IO操作中的等待,同时一次可以等待多个读写事件(多个文件描述符)        

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
                                  fd_set *exceptfds, struct timeval *timeout); 

(1)返回值介绍:

        返回值:n>0: 有n个fd就绪了;n==0: 超时等待,没有错误但也没有fd就绪;n== -1: 等待出错,例如,等待的文件描述符已经关闭等错误。

(2) struct timeval结构体

#include <sys/time.h>

struct timeval {

        time_t      tv_sec;         /* seconds */

        suseconds_t tv_usec;        /* microseconds */

};

        tv_sec:是以秒为单位的时间戳
        tv_usec:微秒 

(3) 参数介绍

        nfds:传递的最大文件描述符 + 1(位图的大小)
        timeout:给select设置等待方式,如果设置为{5, 0},则是select每隔5s,返回一次,如果中间有文件描述符就绪了,会立即返回。 如果设置为{0, 0},相当于非阻塞的一种方式。如果设置为NULL,则是阻塞等待。同时他是个输出输出型参数,他会返回剩下没有用的时间数。
        readfds:读文件位图
        writefds:写文件位图
        exceptfds:异常位图

        这三个位图也是输入输出型参数,以readfds位图为例,输入时告诉内核,要帮我关系哪些文件描述符的读事件,输出时内核告诉用户,让内核关心的文件描述符中有哪些已经就绪了,可以进行读取操作了。

(4) fd_set位图

        内核提供的一种位图数据类型。对位图进行赋值以及位图的检测,都需要进行位运算操作,但是用户对fd_set的结构不了解,所以系统提供了一批对位图操作的接口。

void FD_CLR(int fd, fd_set *set);    // fd比特位置为0
int  FD_ISSET(int fd, fd_set *set);   // 判断fd在不在fd_set位图
void FD_SET(int fd, fd_set *set);    // fd比特位置为1
void FD_ZERO(fd_set *set);           // 清空位图

        fd_set位图比特位的个数最多是sizeof(fd_set) * 8个,不同的编译器下不相同,vscode是1024个比特位,也就是对一种事件的检测最多可以监测1024个文件描述符 

2. select服务器代码

        主要就是把等的工作交给select,服务器在read、write、accept等操作时就不会阻塞住了。如果不这样的话,一个链接到来了,会进行accept,只会对链接进行读写数据处理,等到链接关闭时才会进行下一次的accept,解决办法是使用多线程,accept后,建立新的线程去处理读写事件,主线程继续accept获取新连接。

        而使用select解决了该问题,当获取一个新连接的时候,不是傻傻的等待该链接读写事件就绪,而是会把新连接放入到select进行事件监控,继续处理已经就绪的事件。

        accept、read、write会一直在处理事件,不会阻塞住,除非一个就绪事件都没有。

(1)select函数的传参

        传参时,对于4个输入输出型参数,一般都要进行重复设置,以timeout为例,如果不进行重复设置的话,select函数返回都会对timeout进行修改成剩余超时时间,在传入时就是修改后的,而并非我们真正想要等待的事件,等到timeout时间为0时,还会变成非阻塞等待,无限的立即返回。

        其他三个位图也是一样的,以readfds为例,如果我们想监控10号文件描述符,传进入之后,内核会帮我们在timeout时间内检测,如果10号文件描述符就绪了,会直接返回,如果不重新设置的话,返回的readfds位图只有第10位是1,其他全是0,也不符合我们要检测的文件描述符的要求。

(2) 连接到来不等于读写事件就绪

        当listensockfd套接字读事件就绪时,也就是有新连接时,进行读取新连接,读取之后需要将新连接放回到select位图中,因为建立连接不一定有读写事件就绪。这样也就可以让read和write不需要傻傻等待读写事件就绪,而是处理那些已经就绪的读写事件。

(3) 代码细节

  • accept函数不会在阻塞了:因为等待的操作交给了select去做了,如果说listensockfd读事件就绪了,那么就说明一定有新连接到来了,才会去调用accept函数。
  • 为了能够记录我们要监听的文件描述符都有哪些,以及再次调用select能够正确的初始化位图的哪些比特位,还要知道最大的文件描述符是多少,所以一般都会提供辅助数组去所有的文件描述符记录。如果同时要监听读、写、异常事件,那么就需要三个辅助数组了。
  • 因为fd_set位图有比特位有数量限制,所有超过限制时,要进行判断并打印日志警告,并关闭对应的文件描述符链接。
  • 监听的事件越来越多,那么怎么知道文件描述符就绪的是什么事件呢?例如读事件可以分为,新连接到来,和数据到来。所以我们也要进行判断。
  • 为什么有的时候开辟的空间不需要初始化,有的时候需要?首先区别在于,你开的空间可能不是全部都用,而是用多少覆盖多少空间,你读取的数据也都是覆盖过的,然后还有对应的结束策略(\n,0等)所以不会读取到错乱的数据位置,但是例如fd_set rfds这种,开辟的是一个位图的空间,那么整个空间都是我们需要的,但是我们在写数据时,可能不会覆盖空间内所有的地方,所有有的地方没有被覆盖的化,就会有错乱的数据,所以要进行初始化处理。struct sockaddr_in结构体也是一样,它内部的数据都要被用到,如果初始化不完全,会导致错误,所以处于安全性考虑,一般会先memset函数处理一下。

(4) 代码实现

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <cstring>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include <sys/select.h>

static const uint16_t defaultport = 8082;
static const int backlog = 10;
static const int fd_max_num = (sizeof(fd_set) * 8);

class SelectServer
{
public:
    SelectServer(const uint16_t port = defaultport)
        : _listen_sockfd(0), _port(port)
    {
        _read_array.resize(fd_max_num, -1);
    }
    // 初始化服务器
    void Init()
    {
        // 创建监听套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            std::cerr << "listensockfd socket fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd socket success]" << std::endl;

        int opt = 1;
        setsockopt(_listen_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 绑定端口号
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = INADDR_ANY;

        int retbind = bind(_listen_sockfd, (const struct sockaddr *)&server, sizeof(server));
        if (retbind < 0)
        {
            std::cerr << "listensockfd bind fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd bind success]" << std::endl;

        // 开始监听
        int retlisten = listen(_listen_sockfd, backlog);
        if (retlisten < 0)
        {
            std::cerr << "listensockfd bind fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd listen......]" << std::endl;
    }

    // 启动服务器
    void Start()
    {
        _read_array[0] = _listen_sockfd;
        while (true)
        {
            fd_set readfds;
            FD_ZERO(&readfds);

            int maxfd = _read_array[0];
            for (int i = 0; i < fd_max_num; i++)
            {
                if (_read_array[i] != -1)
                {
                    FD_SET(_read_array[i], &readfds);
                    maxfd = std::max(_read_array[i], maxfd);
                }
            }

            struct timeval timeout = {2, 0};

            int ret = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
            if (ret < 0)
            {
                std::cerr << "select fail" << std::endl;
            }
            else if (ret == 0)
            {
                std::cout << "select time out" << std::endl;
            }
            else
            {
                std::cout << "get a new line" << std::endl;
                // 处理事件
                HandlerEvent(readfds);
            }
        }
    }

    void HandlerEvent(fd_set &readfds)
    {
        for (int i = 0; i < fd_max_num; i++)
        {
            int fd = _read_array[i];
            if (fd == -1)
                continue;

            // fd存在
            if (FD_ISSET(fd, &readfds))
            {
                // 是监听套接字
                if (fd == _listen_sockfd)
                {
                    // 处理监听事件
                    struct sockaddr_in client;
                    memset(&client, 0, sizeof(client));
                    socklen_t len = sizeof(client);

                    //获取新连接
                    int sockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);
                    if (sockfd < 0)
                    {
                        std::cerr << "accept fail" << std::endl;
                        continue;
                    }
                    //将sockfd放入到读事件数组中
                    int pos = 1;
                    for (; pos < fd_max_num; pos++)
                    {
                        if (_read_array[pos] == -1)
                            break;
                    }
                    //数组如果满了就关闭连接
                    if (pos == fd_max_num)
                    {
                        std::cerr << "warning _read_array isfull" << std::endl;
                        close(sockfd);
                    }
                    else
                    {
                        _read_array[pos] = sockfd;
                    }
                }
                else
                {
                    // 处理读事件
                    char buffer[1024];
                    //这里的读数据没有处理粘包问题和是否为一个完整报文的问题,需要应用层协议定制
                    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);  
                    if (n < 0)
                    {
                        std::cerr << "read fail" << std::endl;
                        close(fd);
                        _read_array[i] = -1;
                    }
                    else if (n == 0)
                    {
                        std::cerr << "client quit" << std::endl;
                        close(fd);
                        _read_array[i] = -1;
                    }
                    else
                    {
                        buffer[n] = '\0';
                        std::cout << "client say#" << buffer << std::endl;
                    }
                }
            }
        }
    }

private:
    int _listen_sockfd;
    uint16_t _port;
    std::vector<int> _read_array;
};

        

3. select服务器的优缺点

(1) 缺点

  • select监听的文件描述符是有上限的
  • 输入输出型参数比较多,每次都需要修改这些输入输出型参数。用户层借助fd数组对位图进行修改并传给内核,以及内核对位图的修改都是遍历式的修改,而且频率很高,开销会比较大。

(2) 优点 

  • 可移植性比较好,在某些系统上不支持poll,以及对于超时值提供了更好的微秒精度
  • 单进程处理,占用的资源比较少
  • 可以一次等待多个进程,减少服务器的等待时间,让read、write等接口不在阻塞,一直做事,大大提高了效率。

五、 IO多路转接-poll 

1. poll接口介绍

        poll的作用和select一样,都是进行IO操作中的等待过程,但解决了select的等待文件描述符有上限以及输入输出型参数过多需要重复设置的问题。

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

(1) struct pollfd结构体

struct pollfd {

        int   fd;         /* file descriptor */
        short events;     /* requested events */
        short revents;    /* returned events */

};

        fd:文件描述符
        events:用户告诉内核要帮我关注哪些事件
        revents:内核告诉用户,你让我帮你关注的这些事件中,哪些就绪了
        ( 将输入输出进行了分离 )
        系统定义了16个宏,代表16中不同的事件,所以用的式short类型。

(2) 参数介绍

        timeout:设置超时值事件(-1:阻塞式等待)(3000=3秒)
        nfds:关心的文件描述符个数
        fds:传一个struct pollfd结构体数组

 2. 代码实现

#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <cstring>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include <poll.h>

static const uint16_t defaultport = 8082;
static const int backlog = 10;
static const int fd_max_num = 64;

static const int defaultfd = -1;
static const int non_event = 0;

class PollServer
{
public:
    PollServer(const uint16_t port = defaultport)
        : _listen_sockfd(0), _port(port)
    {
        for(int i = 0; i < fd_max_num; i++)
        {
            _eventfds[i].fd = defaultfd;
            _eventfds[i].events = non_event;
            _eventfds[i].revents = non_event;
        }
    }
    // 初始化服务器
    void Init()
    {
        // 创建监听套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            std::cerr << "listensockfd socket fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd socket success]" << std::endl;

        int opt = 1;
        setsockopt(_listen_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 绑定端口号
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = INADDR_ANY;

        int retbind = bind(_listen_sockfd, (const struct sockaddr *)&server, sizeof(server));
        if (retbind < 0)
        {
            std::cerr << "listensockfd bind fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd bind success]" << std::endl;

        // 开始监听
        int retlisten = listen(_listen_sockfd, backlog);
        if (retlisten < 0)
        {
            std::cerr << "listensockfd bind fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd listen......]" << std::endl;
    }

    // 启动服务器
    void Start()
    {
        _eventfds[0].fd = _listen_sockfd;
        _eventfds[0].events = POLLIN;
        int timeout = 3000;
        while (true)
        {

            int ret = poll(_eventfds, fd_max_num, timeout);
            if (ret < 0)
            {
                std::cerr << "poll fail" << std::endl;
            }
            else if (ret == 0)
            {
                std::cout << "poll time out" << std::endl;
            }
            else
            {
                std::cout << "get a new link" << std::endl;
                // 处理事件
                HandlerEvent();
            }
        }
    }

    void HandlerEvent()
    {
        for (int i = 0; i < fd_max_num; i++)
        {
            int fd = _eventfds[i].fd;
            if (fd == defaultfd)
                continue;

            // fd的读事件是否就绪
            if (_eventfds[i].revents & POLLIN)
            {
                // 是监听套接字
                if (fd == _listen_sockfd)
                {
                    // 处理监听事件
                    struct sockaddr_in client;
                    memset(&client, 0, sizeof(client));
                    socklen_t len = sizeof(client);

                    //获取新连接
                    int sockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);
                    if (sockfd < 0)
                    {
                        std::cerr << "accept fail" << std::endl;
                        continue;
                    }
                    //将sockfd放入到读事件数组中
                    int pos = 1;
                    for (; pos < fd_max_num; pos++)
                    {
                        if (_eventfds[pos].fd == defaultfd)
                            break;
                    }
                    //数组如果满了就关闭连接
                    if (pos == fd_max_num)
                    {
                        //可以把_eventfds设置成指针,满了扩容即可
                        std::cerr << "warning _read_array isfull" << std::endl;
                        close(sockfd);
                    }
                    else
                    {
                        _eventfds[pos].fd = sockfd;
                        _eventfds[pos].events = POLLIN;
                    }
                }
                else
                {
                    // 处理读事件
                    char buffer[1024];
                    //这里的读数据没有处理粘包问题和是否为一个完整报文的问题,需要应用层协议定制
                    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);  
                    if (n < 0)
                    {
                        std::cerr << "read fail" << std::endl;
                        close(fd);
                        _eventfds[i].fd = defaultfd;
                        _eventfds[i].events = 0;
                    }
                    else if (n == 0)
                    {
                        std::cerr << "client quit" << std::endl;
                        close(fd);
                        _eventfds[i].fd = defaultfd;
                        _eventfds[i].events = 0;
                    }
                    else
                    {
                        buffer[n] = '\0';
                        std::cout << "client say#" << buffer << std::endl;
                    }
                }
            }
        }
    }

private:
    int _listen_sockfd;
    uint16_t _port;
    struct pollfd _eventfds[fd_max_num];
};

3. poll服务器的优缺点

(1) 优点:

  • 解决了select服务器的监听文件描述符上限的问题,用户可以传一个结构体数组指针,容量可以通过扩容动态的调整。
  • 解决了select接口输入输出型参数过多,每次都要进行对输入输出型参数重新赋值的问题。

(2) 缺点 

  • 遍历成为主要的问题,因为文件描述符上限被解决,用户层、内核进行遍历的数据个数会更多,效率会降低很多。

六、 IO多路转接-epoll 

1. epoll接口介绍

(1) epoll_create

#include <sys/epoll.h>

int epoll_create(int size);      

size:传一个大于0的数即可,现今已经废弃了。
返回值:一个文件描述符

(2) epoll_wait

#include <sys/epoll.h>

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

epfd:传入epoll_create返回的文件描述符
events:返回已经就绪的fd和事件
maxevents:一次从就绪队列拿对少个就绪节点,如果没拿完,也返回。
timeout:设置超时时间
返回值:已经就绪的fd的个数

(3) struct epoll_event结构体

typedef union epoll_data {

        void    *ptr;

        int      fd;

        uint32_t u32;
        uint64_t u64;
} epoll_data_t;

struct epoll_event {

        uint32_t     events;    /* Epoll events */
        epoll_data_t data;      /* User data variable */
};

events:监听什么事件(是一个位图)
data:

(4) epoll_ctl

#include <sys/epoll.h>

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

epfd:传入epoll_create的返回值
op:要进行什么操作,修改(EPOLL_CTL_MOD)、删除(EPOLL_CTL_DEL)、增加(EPOLL_CTL_ADD)
fd:修改哪个文件描述符
event:修改哪些事件

2. epoll原理

        我们使用的select和poll模型对于我们想要监听的文件描述符都要哪些,需要自己使用数组去管理维护,也就导致os想要知道哪些事件并通知我们,就需要循环遍历数组,查看fd对应的各种状态来确定fd对应的事件是否就绪,这样效率会很低。

7e4558c9fd3c440db9113633fe775dfd.png

        这样的话,用户在处理事件的时候,只需要在就绪队列中获取就绪节点就可以了。

3. epoll接口的作用

        (1)在调用epoll_create创建epoll模型时,会创建一个eventpoll对象,返回值为一个文件描述符就是该对象的文件,那么就说明创建了一个struct file结构体,struct file结构体里的指针指向epoll模型文件,只会struct file结构体放入到了文件描述符表中的。

        (2) 调用epoll_ctl时,通过epfd找到epoll模型,再通过fd,确认红黑树中节点的位置,通过op确定要执行什么操作,最后修改为event的事件。

        (3) 调用epoll_wait时,会将就绪队列的内容,放到events输出型参数中,让用户对就绪的事件进行处理。

4. epoll的优势

  • 内核:事件就绪的检测,是由触发回调函数完成的,不需要os循环遍历检测了
  • 用户层:检测是否有事件就绪,并获取就绪事件是直接在就绪队列中拿,事件复杂读是O(1)的
  • 获取到的就绪事件是连续的,不需要判断了
  • 用户不需要在自己维护fd数组了,由os内部自动维护红黑树和就绪队列。

5. epoll的两种工作模式

(1)LT水平触发

        epoll的默认模式就是LT模式,事件到来,如果上层不处理的话,就会一直有效,也就是epoll_wait一直返回>0的值。

(2)  ET边缘触发

        就绪事件,从无到就绪,或者从就绪个数变化时,才会通知一次。

        ET的通知效率会高一些,因为在一定时间内,通知的次数是有限的,ET可以通知更多的事件就绪,而LT会重复通知一个事件就绪。

        ET的IO效率也会高一些,因为只会通知一次,所以也就倒逼用户每 轮都必须把本轮的数据全部拿走。
        那么如何保证全部取走呢?那么就需要循环读取,一直读取到错误,循环读取时,没有数据,那么底层读取函数会阻塞挂起,这是不可以的,这样整个服务器就不运行了,所以要设置为non_block非阻塞式等待,当数据全部读取完了,底层读取函数在读取时会读取错误,返回错误码为EWOULDBLOCK,当读取到改错误码,就代表数据全部取走了,停止循环读取即可。

        如果不一次取走的话,可以能造成通信无法进行,例如:客户端给服务器发了100KB的数据,而服务端在ET模式下,没有循环读取,而是只读取了50KB,那么如果在想读取另外的50KB数据,就需要客户端在给服务器发一个内容,触发ET模式的通知,但此时客户端也在等服务器给自己响应,所以就会造成通信无法继续进行了。

        但是也不是绝对的,LT模式下,当通知第一次的时候,就把读取设置为非阻塞读取,然后数循环读取所有数据,也就相当于ET模式了。

(3)ET和LT的本质区别

        本质区别在于ET只会将就绪的事件放入就绪队列一次,而LT模式会无线次数的放置,直到就绪事件被处理完成。

6. epoll服务器代码

(1)代码细节

  • epoll_wait函数传参本来就有sockfd,为什么还要在event结构体中设置fd呢,这样方便后续在就绪队列中获取就绪事件event结构体,知道是哪个文件描述符就绪。
  • 当一个连接出错时,要先调用epoll_ctl将文件描述符从红黑树中删除,之后再关闭文件描述符,否则epoll_ctl函数在对一个已经关闭的文件描述符做处理会有出错。

(2) 代码实现

#pragma once
#include <iostream>
#include <cstring>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include <sys/epoll.h>
#include <cerrno>

static const uint16_t defaultport = 8082;
static const int backlog = 10;
static const int defaultsize = 10;
static const int reventnum = 64; // 一次从就绪队列拿出来多少节点

class EpollServer
{
public:
    EpollServer(const uint16_t port = defaultport, int timeout = 3000)
        : _listen_sockfd(-1), _epfd(-1), _port(port), _timeout(timeout)
    {
    }

    ~EpollServer()
    {
        close(_listen_sockfd);
    }

    // 初始化服务器
    void Init()
    {
        // 创建epoll模型
        _epfd = epoll_create(defaultsize);
        if (_epfd < 0)
        {
            std::cerr << "epoll_create fail:" << strerror(errno) << std::endl;
        }
        std::cout << "[epoll_create success]" << std::endl;

        // 创建监听套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            std::cerr << "listensockfd socket fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd socket success]" << std::endl;

        // 设置端口号复用
        int opt = 1;
        setsockopt(_listen_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 绑定端口号
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = INADDR_ANY;

        int retbind = bind(_listen_sockfd, (const struct sockaddr *)&server, sizeof(server));
        if (retbind < 0)
        {
            std::cerr << "listensockfd bind fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd bind success]" << std::endl;

        // 开始监听
        int retlisten = listen(_listen_sockfd, backlog);
        if (retlisten < 0)
        {
            std::cerr << "listensockfd bind fail" << std::endl;
            exit(-1);
        }
        std::cout << "[listensockfd listen......]" << std::endl;
    }
    void Start()
    {
        struct epoll_event revents[reventnum];

        // 将监听套接字和他关心的事件添加到epoll模型中的红黑树中
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listen_sockfd; // 方便后期事件就绪时,获取fd

        int retctl = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listen_sockfd, &ev);
        if (retctl != 0)
        {
            std::cerr << "epoll_ctl listen_sockfd fail" << std::endl;
        }

        while (true)
        {
            int retwait = epoll_wait(_epfd, revents, reventnum, _timeout);
            if (retwait > 0)
            {
                std::cout << "event happened" << std::endl;
                Dispatcher(revents, retwait);
            }
            else if (retwait == 0)
            {
                std::cout << "time out" << std::endl;
            }
            else
            {
                std::cout << "epoll wait fail" << std::endl;
            }
        }
    }

    //事件派发器
    void Dispatcher(struct epoll_event *revents, int num)
    {
        for (int i = 0; i < num; i++)
        {
            // 获取哪个文件描述符的哪个事件就绪
            uint32_t event = revents[i].events;
            int fd = revents[i].data.fd;

            // 读事件
            if (event & EPOLLIN)
            {
                // 监听套接字
                if (fd == _listen_sockfd)
                {
                    // 获取新连接
                    struct sockaddr_in client;
                    memset(&client, 0, sizeof(client));
                    socklen_t len = sizeof(client);

                    int newsockfd = accept(_listen_sockfd, (struct sockaddr *)&client, &len);
                    if (newsockfd < 0)
                    {
                        std::cerr << "accept fail" << std::endl;
                        continue;
                    }
                    std::cout << "[get a new link, newsockfd : " << newsockfd << "]" << std::endl;

                    // 将新连接的读事件添加到epoll模型中
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = newsockfd;

                    int retctl = epoll_ctl(_epfd, EPOLL_CTL_ADD, newsockfd, &ev);
                    if (retctl != 0)
                    {
                        std::cerr << "epoll_ctl fd fail" << std::endl;
                    }
                }
                // 其他读事件就绪
                else
                {
                    char buffer[1024];
                    ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
                    if (n > 0)
                    {
                        buffer[n] = '\0';
                        std::cout << "client say#" << buffer << std::endl;
                    }
                    else if (n == 0)
                    {
                        std::cerr << "client quit" << std::endl;
                        int retctl = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
                        //文件描述符要先移除后关闭
                        if (retctl != 0)
                        {
                            std::cerr << "epoll_ctl fd fail" << std::endl;
                        }
                        close(fd);
                    }
                    else
                    {
                        std::cerr << "read fail" << std::endl;
                        int retctl = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);
                        if (retctl != 0)
                        {
                            std::cerr << "epoll_ctl fd fail" << std::endl;
                        }
                        close(fd);
                    }
                }
            }
            // 写事件
            else if (event & EPOLLOUT)
            {
            }
            // 其他事件
            else
            {
            }
        }
    }

private:
    int _listen_sockfd;
    int _epfd;
    uint16_t _port;
    int _timeout;
};

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值