多路转接之Poll&Epoll

文章目录

  • Poll
    • poll函数接口
    • poll的优缺点
    • poll示例
      • Util.hpp(所用到的函数方法)
      • Server.hpp
      • Server.cc
      • log.hpp(日志)
  • Epoll
    • epoll的相关系统调用
      • epoll_create
      • epoll_ctl
      • epoll_wait
    • epoll工作原理
    • epoll的优点
    • epoll工作方式
    • 对比LT和ET
    • epoll服务器(LT模式)示例
      • Util.hpp(需要调用的函数)
      • Server.hpp
      • Server.cc
      • log.hpp
      • 演示效果

Poll

poll函数接口

#include 

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

参数一是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合

对于pollfd结构体:

// pollfd结构
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events */
    short revents; /* returned events */
};

fd表示需要关心的文件描述符

events表示需要关心该fd的哪些事件

revents表示该fd的哪些事件已经就绪

因此fd + events 用来输入的时候看,fd + revents 用来输出的时候看

对于events和revents的取值:

多路转接之Poll&Epoll_第1张图片

参数二表示fds数组的长度

参数三表示poll函数的超时时间, 单位是毫秒(ms)

timeout > 0 : 表示在timeout时间内阻塞,否则非阻塞返回一次

timeout == 0:表示非阻塞等待

timeout < 0:表示阻塞等待

返回值:

返回值小于0, 表示出错;
返回值等于0, 表示poll函数等待超时;
返回值大于0, 表示poll由于监听的文件描述符就绪而返回

poll的优缺点

  1. 不同与select使用三个位图来表示三个fdset的方式, poll使用一个pollfd的指针实现
  2. pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便.
  3. poll并没有最大数量限制 (但是数量过大后性能也是会下降)

poll中监听的文件描述符数目增多时的缺点:

  1. 和select函数一样, poll返回后,需要轮询pollfd来获取就绪的描述符.
  2. 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  3. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降

poll示例

Util.hpp(所用到的函数方法)

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "log.hpp"
#include 
#include 
#include 
#include 

using namespace std;

#define INITPORT 8000
#define DEFAULTFD -1

// 打印函数调试
void Print(const struct pollfd *fdv)
{
    cout << "fd list: ";
    for (int i = 0; i < 1024; i++)
    {
        if (fdv[i].fd != DEFAULTFD)
            cout << fdv[i].fd << " ";
    }
    cout << endl;
}

class Util
{
public:
    static void Recv(struct pollfd *fdv, int i)
    {
        cout << "recv in" << endl;
        // 读取
        // 读取失败就关闭sock并且修改集合组里的数据
        char buff[1024];
        ssize_t s = recv(fdv[i].fd, buff, sizeof(buff) - 1, 0);
        if (s > 0)
        {
            buff[s] = 0;
            cout << "client: " << buff << endl;
            LogMessage(NORMAL, "client: %s", buff);
        }
        else if (s == 0)
        {
            close(fdv[i].fd);
            fdv[i].fd = DEFAULTFD;
            fdv[i].events = 0;
            fdv[i].revents = 0;
            LogMessage(NORMAL, "client quit");
            return;
        }
        else
        {
            close(fdv[i].fd);
            fdv[i].fd = DEFAULTFD;
            fdv[i].events = 0;
            fdv[i].revents = 0;
            LogMessage(ERROR, "client quit: %s", strerror(errno));
            return;
        }

        // 写回数据
        // 这里不考虑写事件
        string response = buff;

        write(fdv[i].fd, response.c_str(), response.size());
        LogMessage(DEBUG, "Recver end");
    }

    // 将通信sock添加进集合组
    static void AddSock(struct pollfd *fdv, int listensock)
    {
        // listensock读事件就绪
        string clientip;
        uint16_t clientport;
        int sock = Util::GetSock(listensock, &clientip, &clientport);
        if (sock < 0)
            return;
        LogMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
        int i = 0;
        for (; i < 1024; i++)
        {
            if (fdv[i].fd != DEFAULTFD)
                continue;
            else
                break;
        }
        if (i == 1024)
        {
            LogMessage(WARNING, "server if full, please wait");
            close(sock);
        }
        else
        {
            fdv[i].fd = sock;
            fdv[i].events = POLLIN;
            fdv[i].revents = 0;
        }

        Print(fdv);
        LogMessage(DEBUG, "Accepter out");
    }

    // 获取新连接创建通信sock
    static int GetSock(int listensock, string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        socklen_t len = sizeof(peer);
        int sock = accept(listensock, (struct sockaddr *)&peer, &len);
        if (sock < 0)
            LogMessage(ERROR, "accept socket error, next");
        else
        {
            LogMessage(NORMAL, "accept socket %d success", sock);
            cout << "sock: " << sock << endl;
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }

    // 设置监听套接字为监听状态
    static void setListen(int listensock)
    {
        if (listen(listensock, 5) < 0)
        {
            LogMessage(FATAL, "listen socket error!");
            exit(3);
        }
        LogMessage(NORMAL, "listen socket success");
    }

    // 绑定网络信息
    static void bindSock(int port, int listensock)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LogMessage(FATAL, "bind socket error!");
            exit(2);
        }
        LogMessage(NORMAL, "bind sock success");
    }

    // 创建监听套接字
    static void createSock(int *listensock)
    {
        *listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            LogMessage(FATAL, "create socket error!");
            exit(1);
        }
        LogMessage(NORMAL, "create socket success");

        // 设置进程可以立即重启
        int opt = 1;
        setsockopt(*listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    }

    // 设置非阻塞
    static void SetNonBlock(int fd)
    {
        int f = fcntl(fd, F_GETFL);
        if (f < 0)
        {
            cerr << "fcntl" << endl;
            return;
        }
        fcntl(fd, F_SETFL, f | O_NONBLOCK);
    }
};

Server.hpp

#pragma once

#include "Util.hpp"

class Server
{
public:
    Server(const uint16_t port = INITPORT)
        : _port(port), _listensock(-1)
    {
    }

    void HandlerEvent()
    {
        int i = 0;
        for (int i = 0; i < 1024; ++i)
        {
            // 过滤掉非法的fd
            if (fdv[i].fd == DEFAULTFD)
                continue;
            if(!(fdv[i].events & POLLIN))
                continue;

            if (fdv[i].fd == _listensock && (fdv[i].revents & POLLIN)) // 判断listensock在不在就绪的集合中
                Util::AddSock(fdv, _listensock);
            else if (fdv[i].revents & POLLIN)
                Util::Recv(fdv, i);
            else
            {
            }
        }
    }

    void Init()
    {
        // 创建监听套接字
        Util::createSock(&_listensock);

        // 绑定网络信息
        Util::bindSock(_port, _listensock);

        // 设置监听套接字为监听状态
        Util::setListen(_listensock);

        fdv = new struct pollfd[1024];
        for (int i = 0; i < 1024; ++i)
        {
            fdv[i].fd = DEFAULTFD;
            fdv[i].events = 0;
            fdv[i].revents = 0;
        }
        fdv[0].fd = _listensock;
        fdv[0].events = POLLIN;
    }

    void start()
    {
        int timeout = 1000;
        while (1)
        {         
            int n = poll(fdv, 1024, timeout);
            switch (n)
            {
            case 0:
                cout << "timeout...." << endl;
                LogMessage(NORMAL, "timeout....");
                break;
            case -1:
                printf("select error, code: %d, err string: %s", errno, strerror(errno));
                LogMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));
                break;
            default:
                // 有事件就绪
                cout << "event readly" << endl;
                LogMessage(NORMAL, "event readly");
                // 处理事件
                HandlerEvent();
                break;
            }
        }
    }

    ~Server()
    {
        if (_listensock < 0)
            close(_listensock);
    }

private:
    int _listensock;
    uint16_t _port;
    struct pollfd *fdv;
};

Server.cc

#include "Server.hpp"
#include 

// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_port\n\n";
}

int main(int argc, char *argv[])
{
    // 启动服务端不需要指定IP
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);
    unique_ptr<Server> sptr(new Server(port));

    sptr->Init();
    sptr->start();

    return 0;
}

log.hpp(日志)

#pragma once

#include 
#include 
#include 
#include 
#include 

using namespace std;

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *to_levelstr(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return nullptr;
    }
}

void LogMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logpre[NUM];
    snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());

    char line[NUM];
    // 可变参数
    va_list arg;
    va_start(arg, format);

    vsnprintf(line, sizeof(line), format, arg);

    // 保存至文件
    FILE* log = fopen("log.txt", "a");
    FILE* err = fopen("log.error", "a");

    if(log && err)
    {
        FILE *curr = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING) 
            curr = log;
        if(level == ERROR || level == FATAL) 
            curr = err;
        if(curr) fprintf(curr, "%s%s\n", logpre, line);

        fclose(log);
        fclose(err);
    }
}

Epoll

是为处理大批量句柄而作了改进的poll

它几乎具备了select 和poll 的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法

epoll的相关系统调用

epoll 有3个相关的系统调用

epoll_create

#include 

int epoll_create(int size);

创建一个epoll的句柄,size就是epoll的容量大小。返回epoll的文件描述符

注意用完之后, 必须调用close()关闭

epoll_ctl

#include 

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

epoll的事件注册函数

  1. 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
  2. 第一个参数是epoll_create()的返回值(epoll的句柄).
  3. 第二个参数表示动作,用三个宏来表示.
  4. 第三个参数是需要监听的fd.
  5. 第四个参数是告诉内核需要监听什么事

其中参数二的三个动作分别为:

  1. EPOLL_CTL_ADD :注册新的fd到epfd中;
  2. EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
  3. EPOLL_CTL_DEL :从epfd中删除一个fd;

参数四的结构为:

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 */
} __EPOLL_PACKED;

events可以是以下几个宏的集合:

  1. EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  2. EPOLLOUT : 表示对应的文件描述符可以写;
  3. EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  4. EPOLLERR : 表示对应的文件描述符发生错误;
  5. EPOLLHUP : 表示对应的文件描述符被挂断;
  6. EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  7. EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

epoll_wait

收集在epoll监控的事件中已经发送的事件

#include 

int epoll_wait(int epfd, struct epoll_event *events,
              int maxevents, int timeout);
  1. 参数events是分配好的epoll_event结构体数组.
  2. epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
  3. maxevents告诉内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
  4. 参数timeout是超时时间 (毫秒, 0会立即返回, -1是永久阻塞).
  5. 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败

epoll工作原理

当某一进程调用epoll_create方法时, Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关

  1. 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
  2. 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度).
  3. 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
  4. 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中.
  5. 在epoll中,对于每一个事件,都会建立一个epitem结构体
  6. 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
  7. 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1)

总的来说,epoll的使用过程就是三部曲

  1. 调用epoll_create创建一个epoll句柄;
  2. 调用epoll_ctl, 将要监控的文件描述符进行注册;
  3. 调用epoll_wait, 等待文件描述符就绪

epoll的优点

  1. 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  2. 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  3. 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  4. 没有数量限制: 文件描述符数目无上限

epoll工作方式

epoll有2种工作方式-水平触发(LT)和边缘触发(ET)

对比LT和ET

LT:就好比快递员给你送快递,他打电话叫你下楼拿,可是你告诉快递员还没有空,于是快递员就一直在打电话给你,直到你下楼将快递取回。

ET:就好比快递员给你送快递,他打电话叫你下楼拿,可是你告诉快递员还没有空,这时这位快递员就不等你也不会再打电话给你,把快递放在楼下就走了,至于你有没有下楼取快递他不关心了

这两种模式的区别就在于其通知机制不一样,在LT模式下只要还有数据没有取走就会一直通知而对于ET从始至终只会通知一次,除非数据从无到有,从有到多发生变化的时候才会再次通知

LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完,相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。另一方面, ET 的代码复杂程度更高了

LT的文件描述符可以是阻塞的也可以是非阻塞的,但是ET模式下必须为非阻塞的

epoll服务器(LT模式)示例

Util.hpp(需要调用的函数)

#pragma once

#include "log.hpp"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

#define INITPORT 8000
#define DEFAULTFD -1
#define EPOLLSIZE 128

class Util
{
public:
    // 获取新连接创建通信sock
    static int GetSock(int listensock, string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        socklen_t len = sizeof(peer);
        int sock = accept(listensock, (struct sockaddr *)&peer, &len);
        if (sock < 0)
            LogMessage(ERROR, "accept socket error, next");
        else
        {
            LogMessage(NORMAL, "accept socket %d success", sock);
            cout << "sock: " << sock << endl;
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }

    // 创建epoll模型
    static int CreateEpollFD()
    {
        int epfd = epoll_create(EPOLLSIZE);
        if(epfd < 0)
        {
            LogMessage(FATAL, "Create epoll_fd error!");
            exit(4);
        }
        LogMessage(NORMAL, "Create epoll_fd success!");

        return epfd;
    }

    // 设置监听套接字为监听状态
    static void SetListen(int sock)
    {
        int n = listen(sock, 5);
        if(n < 0)
        {
            LogMessage(FATAL, "Set listen error!");
            exit(3);
        }
        LogMessage(NORMAL, "Set listen success!");
    }

    // 绑定网络信息
    static void BindSock(int sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        int n = bind(sock, (struct sockaddr *)&local, sizeof(local));
        if(n < 0)
        {
            LogMessage(FATAL, "Bind error!");
            exit(2);
        }
        LogMessage(NORMAL, "Bind success!");
    }

    // 创建监听套接字
    static int CreateSock()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            LogMessage(FATAL, "Create sock error!");
            exit(1);
        }
        LogMessage(NORMAL, "Create sock success!");

        return sock;
    }
};

Server.hpp

#pragma once

#include "Util.hpp"

class Server
{
public:
    Server(const uint16_t port = INITPORT) : _port(port), _listensock(-1), _epfd(-1), _revs(nullptr), _num(64)
    {
    }

    void Init()
    {
        // 创建监听套接字
        _listensock = Util::CreateSock();
        // 绑定网络信息
        Util::BindSock(_listensock, _port);
        // 设置监听套接字为监听状态
        Util::SetListen(_listensock);

        // 创建epoll模型
        _epfd = Util::CreateEpollFD();
        // 添加listensock到epoll中
        struct epoll_event ev;
        ev.events = EPOLLIN;
        // 记录fd,当事件就绪时可以知道是哪一个就绪
        ev.data.fd = _listensock;
        epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, &ev);

        // 申请就绪事件的空间
        _revs = new struct epoll_event[_num];
    }

    void start()
    {
        int timeout = 1000;
        while (1)
        {
            int n = epoll_wait(_epfd, _revs, _num, timeout);
            switch (n)
            {
            case 0:
                LogMessage(NORMAL, "....timeout....");
                break;
            case -1:
                LogMessage(WARNING, "epoll_wait error: %s", strerror(errno));
                break;
            default:
                HandlerEvent(n);
                break;
            }
        }
    }

    // 获取事件
    void HandlerEvent(int n)
    {
        LogMessage(NORMAL, "HandlerEvent in");
        for (int i = 0; i < n; ++i)
        {
            // 保留事件就绪的类型
            uint32_t events = _revs[i].events;
            // 获取当前就绪事件的fd
            int sock = _revs[i].data.fd;

            if (sock == _listensock && (events & EPOLLIN))
            {
                string Clienip;
                uint16_t ClientPort;
                // 获取通信的套接字
                int fd = Util::GetSock(_listensock, &Clienip, &ClientPort);
                if (fd < 0)
                    continue;

                // 将sock添加进epoll
                struct epoll_event ev;
                ev.events = EPOLLIN;
                ev.data.fd = fd;
                epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
            }
            else if (events & EPOLLIN)
            {
                // 读取数据
                char buff[1024];
                int s = recv(sock, buff, sizeof(buff), 0);
                if (s > 0)
                {
                    buff[s] = 0;
                    cout << "client: " << buff << endl;
                    LogMessage(NORMAL, "client: %s", buff);
                }
                else if (s == 0)
                {
                    // 将sock从epoll中移除并关闭sock
                    epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                    close(sock);
                    LogMessage(NORMAL, "client quit!");
                }
                else
                {
                    // 将sock从epoll中移除并关闭sock
                    epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                    close(sock);
                    LogMessage(ERROR, "Recv error: %s", strerror(errno));
                }

                string response = buff;
                write(sock, response.c_str(), response.size());
            }
            else
            {
            }
        }
        LogMessage(NORMAL, "HandlerEvent end");
    }

    ~Server()
    {
        if (_listensock != -1)
            close(_listensock);
        if (_epfd != -1)
            close(_epfd);
    }

private:
    int _listensock;
    uint16_t _port;
    int _epfd;                 // epoll
    struct epoll_event *_revs; // 保存就绪事件
    int _num;                  // 就绪事件的空间大小
};

Server.cc

#include "Server.hpp"
#include 

// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_port\n\n";
}

int main(int argc, char *argv[])
{
    // 启动服务端不需要指定IP
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);
    unique_ptr<Server> sptr(new Server(port));

    sptr->Init();
    sptr->start();

    return 0;
}

log.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 

using namespace std;

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *to_levelstr(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return nullptr;
    }
}

void LogMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logpre[NUM];
    snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());

    char line[NUM];
    // 可变参数
    va_list arg;
    va_start(arg, format);

    vsnprintf(line, sizeof(line), format, arg);

    // 保存至文件
    FILE* log = fopen("log.txt", "a");
    FILE* err = fopen("log.error", "a");

    if(log && err)
    {
        FILE *curr = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING) 
            curr = log;
        if(level == ERROR || level == FATAL) 
            curr = err;
        if(curr) fprintf(curr, "%s%s\n", logpre, line);

        fclose(log);
        fclose(err);
    }
}

演示效果

多路转接之Poll&Epoll_第2张图片

你可能感兴趣的:(网络,linux,网络,c++)