<14>_Linux高级IO

目录

一、五种IO模型

1,阻塞IO

2,非阻塞IO

3,信号驱动IO

4,IO多路转接

5,异步IO

6,IO模型小结

二、高级IO重要概念

[1,同步通信 vs 异步通信](#1,同步通信 vs 异步通信)

[2,阻塞 vs 非阻塞](#2,阻塞 vs 非阻塞)

3,其他高级IO

三、非阻塞IO

1,fcntl函数

2,实现函数SetNoBlock:将一个文件描述符设置为非阻塞

3,示例:使用fcntl实现轮询方式读取标准输入

四、I/O多路转接之select

1,select函数

2,select的执行过程

3,socket的就绪条件

4,select的特点

5,select的缺点

[6,示例: 使用select实现echo服务器](#6,示例: 使用select实现echo服务器)

五、I/O多路转接之poll

1,poll函数

2,poll函数的优点

3,poll函数的缺点

[4,示例: 使用poll监控标准输入](#4,示例: 使用poll监控标准输入)

六、I/O多路转接之epoll

1,epoll函数

2,epoll的工作原理

3,epoll的优点

4,epoll的工作方式

5,理解LT和ET模式

6,epoll的使用场景

[7,epoll示例: epoll服务器(LT模式)](#7,epoll示例: epoll服务器(LT模式))

[8,epoll示例: epoll服务器(ET模式)](#8,epoll示例: epoll服务器(ET模式))


一、五种IO模型

1,阻塞IO

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

2,非阻塞IO

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用.

3,信号驱动IO

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

4,IO多路转接

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态.

5,异步IO

异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

6,IO模型小结

网络通信的本质就是:IO

在任何IO过程中, 都包含两个步骤. 第一是等待 , 第二是拷贝.

IO为什么低效?

在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间.

以读取为例:

1.当我们read/recv的时候,如果底层缓冲区没有数据,read/recv会怎么办? 阻塞 >等待

2.当我们read/recv的时候,如果底层缓冲区有数据,read/recv会怎么办? 拷贝

如何提高IO效率?

在单位时间中,让等待的比重变得越低,效率越高。

让IO更高效, 最核心的办法就是让等待的时间尽量少.

二、高级IO重要概念

1,同步通信 vs 异步通信

同步和异步关注的是消息通信机制.

所谓同步,就是在发出一个调用 时,在没有得到结果之前,该调用 就不返回. 但是一旦调用返回,就得 到返回值了; 换句话说,就是由调用者 主动等待这个调用的结果;

异步则是相反,调用 在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用 发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用;

另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相关的概念.

进程/线程同步也是进程/线程之间直接的制约关系;

是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候;

2,阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回. 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

3,其他高级IO

非阻塞IO, I/O多路转接(也叫I/O多路复用),纪录锁,系统V流机制,readv和writev函数以及存储映射 IO( mmap),这些统称为高级IO.

三、非阻塞IO

1,fcntl函数

fcntl的函数原型如下:

复制代码
头文件:
    #include <unistd.h> 
    #include <fcntl.h>
函数:
    int fcntl(int fd, int cmd, ... /* arg */ );
参数:
    根据传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
    复制一个现有的描述符		  (cmd=F_DUPFD).
    获得/设置文件描述符标记  (cmd=F_GETFD 或 F_SETFD).   
    获得/设置文件状态标记	  (cmd=F_GETFL 或 F_SETFL).
    获得/设置异步I/O所有权		(cmd=F_GETOWN 或 F_SETOWN).   
    获得/设置记录锁					(cmd=F_GETLK,F_SETLK 或 F_SETLKW).

我们设置一个文件描述符, 默认都是阻塞IO.

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

2,实现函数SetNoBlock:将一个文件描述符设置为非阻塞

基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.

复制代码
bool SetNoBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if(fl < 0) 
        return false;
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
    return true;
}

使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).

然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

3,示例:使用fcntl实现轮询方式读取标准输入

复制代码
[user@iZwz9eoohx59fs5a6ampomZ SetNoBlock]$ cat myfile.cc
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

bool SetNoBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if(fl < 0) 
        return false;
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
    return true;
}

int main()
{
    // 把0号文件描述符(标准输入流)设置为非阻塞
    SetNoBlock(0); // 只要设置一次,后续都是非阻塞了
 
    char buffer[1024];
    while(true)
    {
        sleep(1);
        errno = 0;
        // 非阻塞的时候,我们以出错的形式返回, 告知上层没有就绪
        // a,我们如何甄别是真的出错了
        // b,还是仅仅是数据没有就绪呢?
        // 数据就绪了的话,我们就正常读取就行
        // 出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if(s > 0)
        {
            buffer[s-1] = 0;
            std::cout << "echo# " << buffer << " read success " << "errno: " << errno << " " << strerror(errno) << std::endl;
        }
        else
        {
            // 如果失败的errno值是11,就代表其实没错,只不过是底层数据没就绪
            // std::cout << "read failure " << "errno: " << errno << " " << strerror(errno) << std::endl;
            if(errno == EWOULDBLOCK || errno == EAGAIN)
            {
                std::cout << "当前0号fd数据没有就绪, 再试一次吧" << std::endl;
                continue;
            }
            else if(errno == EINTR)
            {
                std::cout << "当前IO可能被信号中断, 再试一次吧" << std::endl;
                continue;
            }
            else
            {
                // 差错处理
            }
        }
    }

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ SetNoBlock]$ ./myfile 
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
asdfsa当前0号fd数据没有就绪, 再试一次吧
fWD
echo# asdfsafWD read success errno: 0 Success
ASDFASFE当前0号fd数据没有就绪, 再试一次吧

echo# ASDFASFE read success errno: 0 Success
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
AESFASEFAS
echo# AESFASEFAS read success errno: 0 Success
当前0号fd数据没有就绪, 再试一次吧
23154RDF当前0号fd数据没有就绪, 再试一次吧
1234T
echo# 23154RDF1234T read success errno: 0 Success
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
当前0号fd数据没有就绪, 再试一次吧
^C

四、I/O多路转接之select

1,select函数

系统提供select函数来实现多路复用输入/输出模型.

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;

程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select的函数原型如下:

复制代码
头文件:
    #include <sys/select.h>
函数:
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
    参数nfds是需要监视的最大的文件描述符值+1;
    rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;
    参数timeout为结构timeval,用来设置select()的等待时间;
函数返回值:
    执行成功则返回文件描述词状态已改变的个数;
    如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;
    当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测;
错误值可能为:
    EBADF 文件描述词为无效的或该文件已关闭;
    EINTR 此调用被信号所中断;
    EINVAL 参数n 为负值;
    ENOMEM 核心内存不足;

参数fd_set的结构

其实这个结构就是一个整数数组, 更严格的说, 是一个"位图".

使用位图中对应的位来表示要监视的文件描述符.

系统提供了一组操作fd_set的接口, 来比较方便的操作位图.

复制代码
参数结构:
    /* fd_set for select and pselect.  */
    typedef struct
    {
    /* XPG4.2 requires this member name.  Otherwise avoid the name from the global namespace.  */
    #ifdef __USE_XOPEN
            __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    #define __FDS_BITS(set) ((set)->fds_bits)
    #else
            __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
    #define __FDS_BITS(set) ((set)->__fds_bits)
    #endif
    } fd_set;
    
    /* The fd_set member is required to be an array of longs.  */
    typedef long int __fd_mask;
系统接口:
    void FD_CLR(int fd, fd_set *set);  // 用来清除描述词组set中相关fd的位 
    int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd的位是否为真
    void FD_SET(int fd, fd_set *set);  // 用来设置描述词组set中相关fd的位 
    void FD_ZERO(fd_set *set);				 // 用来清除描述词组set的全部位

参数timeval的结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。

复制代码
参数结构:
    struct timeval
    {
        __time_t      tv_sec;		//秒
        __suseconds_t tv_usec;	//微秒
    };
参数timeout取值:
    NULL:则表示select()没有timeout ,select将一直被阻塞,直到某个文件描述符上发生了事件;
    0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
    特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回;

常见的程序写法

复制代码
fd_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){......}

2,select的执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.

*(1)执行fd_set set;FD_ZERO(&set); 则set用位表示是0000,0000。

*(2)若fd=5,执行FD_SET(fd,&set); 后set变为0001,0000。(第5位置为1)

*(3)若再加入fd=2,fd=1,则set变为0001,0011。(第5/2/1位置为1)

*(4)执行 select(6,&set,0,0,0) 阻塞等待。

*(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。(第2/1位置为1)注意:没有事件发生的fd=5被会清空。

3,socket的就绪条件

读就绪

socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;

socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;

监听的socket上有新的连接请求;

socket上有未处理的错误;

写就绪

socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;

socket的写操作被关闭(close或者shutdown).对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE 信号;

socket使用非阻塞connect连接成功或失败之后;

socket上有未读取的错误;

异常就绪

socket上收到带外数据;

关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段,就与此相关);

4,select的特点

可监控的文件描述符个数取决于sizeof(fd_set)的值.

如果服务器上sizeof(fd_set)=512,每bit表示一个文件 描述符,则服务器上支持的最大文件描述符是512*8=4096.

将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd:

一是,用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。

二是,select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得 fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注:fd_set的大小可以调整,可能涉及到重新编译内核.

5,select的缺点

每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.

每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大.

同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大.

select支持的文件描述符数量太小.

6,示例: 使用select实现echo服务器

复制代码
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ ls
Log.hpp  Main.cc  Makefile  SelectServer.hpp  Server  Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ cat Main.cc 
#include "SelectServer.hpp"

int main()
{
    // fd_set是一个固定大小的位图, 直接决定了select能同时关心的个数是有上限的
    // std::cout << sizeof(fd_set) * 8 << std::endl; // max值为1024

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Start();
 
    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ cat SelectServer.hpp 
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__

#include <iostream>
#include <string>
#include <vector>
#include <sys/select.h>
#include <sys/time.h>
#include "Log.hpp"
#include "Sock.hpp"

#define BITS 8
#define NUM (sizeof(fd_set)*BITS) //总共为1024
#define FD_NONE -1

// SelectServer 我们只完成读取,写入和异常不做处理 -- 在EpollServer会写完整
class SelectServer
{
public:
    SelectServer(const uint16_t &port = 8080) : _port(port)
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");
        // 把fd数组初始化,没有用到的位置设置为-1
        for(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;
        // 规定监听到的套接字放在数组首部 _fd_array[0] = _listensock;
        _fd_array[0] = _listensock;
    }
    ~SelectServer()
    {
        if (_listensock >= 0) close(_listensock);
    }
    void Start()
    {
        while (true)
        {
            // struct timeval timeout = {0, 0};
            // 如何看待listensock? 获取新连接,我们把它依旧看做成为IO,input事件,如果没有连接到来呢?阻塞
            // int sock = Sock::Accept(listensock, ...); //不能直接调用accept了
            // 将listensock添加到读文件描述符集中
            // FD_SET(_listensock, &rfds); 
            // int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);

            // 1. nfds: 随着我们获取的sock越来越多,随着我们添加到select的sock越来越多,注定了nfds每一次都可能要变化,我们需要对它动态计算
            // 2. rfds/writefds/exceptfds:都是输入输出型参数,输入输出不一定相同,所以注定了我们每一次都要对rfds进行重新添加
            // 3. timeout: 也是输入输出型参数,每一次都要进行重置,前提是你需要使用它
            // 4. 1和2点 => 注定了我们必须自己将合法的文件描述符需要单独全部保存起来,用来支持:1. 更新最大fd 2.更新位图结构

            DebugPrint();

            // 每次select后,fd_set都只保留状态变化的文件描述符
            // 所以需要设置_fd_array,保存所有需要监听的sock
            fd_set rfds; // 定义fd位图
            FD_ZERO(&rfds); // 清空位图
            int maxfd = _listensock; //标记fd最大值

            // 1. 遍历数组,更新最大值,并添加所有需要关心的fd到fd_set位图中
            for(int i = 0; i < NUM; i++)
            {
                if(_fd_array[i] == FD_NONE) continue;
                FD_SET(_fd_array[i], &rfds);
                if(maxfd < _fd_array[i]) maxfd = _fd_array[i];
            }

            // 2. 调用select进行事件的检测
            // rfds未来,一定会有两类sock,监听listensock,普通sock
            // 我们select中,就绪的fd会越来越多!
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case 0:
                // 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
                logMessage(DEBUG, "%s", "time out...");
                break;
            case -1:
                // 当有错误发生时则返回-1,错误原因存于errno 
                logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
                break;
            default:
                // 为什么会一直打印连接到来呢?连接已经建立完成,就绪了,但是你没有取走,select要一直通知你!
                logMessage(DEBUG, "get a new link event..."); 
                // 3. 遍历数组,找到就绪事件,完成对应动作
                HandlerEvent(rfds);
                break;
            }
        }
    }

private:
    void HandlerEvent(const fd_set &rfds) // fd_set 是一个集合,里面可能会存在多个sock
    {
        for(int i = 0; i < NUM; i++)
        {
            // 1. 去掉不合法的fd
            if(_fd_array[i] == FD_NONE) continue;
            // 2. 合法的就一定就绪了?不一定
            if(FD_ISSET(_fd_array[i], &rfds)) // 用FD_ISSET测试描述词组rfds中相关fd的位是否为真
            {
                // 新获取到的sock放到_fd_array数组里
                if(_fd_array[i] == _listensock) Accepter();
                // 监听的fd,读事件就绪了,就可以读取了
                else Recver(i);
            }
        }
    }
    void Accepter()
    {
        std::string clientip;
        uint16_t clientport = 0;
        // listensock上面的读事件就绪了,表示可以读取了
        // 1. 获取新连接
        int sock = Sock::Accept(_listensock, &clientip, &clientport); // 这里在进行accept会不会阻塞?不会!
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        // 这里可以调用read / recv吗? 不能!
        // 为什么不能?我们不清楚该sock上面数据什么时候到来,recv、read就有可能先被阻塞,IO = 等+数据拷贝
        // 谁可能最清楚呢?select!
        // 得到新连接的时候,此时我们应该考虑的是,将新的sock托管给select,让select帮我们进行检测sock上是否有新的数据
        // 有了数据select,读事件就绪,select就会通知我,我们在进行读取,此时我们就不会被阻塞了
        // 要将sock 添加给 select,其实我们只要将fd放入到数组中即可!
        // 2.把获取的sock放入fd数组
        int pos = 1;
        for(; pos < NUM; pos++)
        {
            if(_fd_array[pos] == FD_NONE) break;
        }
        if(pos == NUM)
        {
            logMessage(WARNING, "%s:%d", "select server already full, close: %d", sock);
            close(sock);
        }else
        {
            _fd_array[pos] = sock;
        }
    }
    void Recver(int pos)
    {
        // 读事件就绪:INPUT事件到来、recv,read
        logMessage(DEBUG, "message in, get IO event: %d", _fd_array[pos]);
        // 暂时先不做封装, 此时select已经帮我们进行了事件检测,fd上的数据一定是就绪的,即 本次 不会被阻塞
        // 这样读取有bug吗?有的,你怎么保证以读到了一个完整包文呢?
        char buffer[1024];
        int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {
            // 读取信息
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);
        }
        else if(n == 0)
        {
            // 对端关闭
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[pos]);
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
        else
        {
            //读取出错
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
    }
    void DebugPrint()
    {
        std::cout << "_fd_array[]: ";
        for(int i = 0; i < NUM; i++)
        {
            if(_fd_array[i] == FD_NONE) continue;
            std::cout << _fd_array[i] << " ";
        }
        std::cout << std::endl;
    }

private:
    uint16_t _port;
    int _listensock;
    int _fd_array[NUM];
    // int _fd_write[NUM];
    // std::vector<int> arr;
};

#endif[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ ./Server 
[DEBUG] [1761992446] create base socket success
_fd_array[]: 3 
[DEBUG] [1761992460] get a new link event...
[DEBUG] [1761992460] get a new line success : [127.0.0.1:59404] : 4
_fd_array[]: 3 4 
[DEBUG] [1761992487] get a new link event...
[DEBUG] [1761992487] message in, get IO event: 4
[DEBUG] [1761992487] client[4]# hello select!!!

_fd_array[]: 3 4 
[DEBUG] [1761992497] get a new link event...
[DEBUG] [1761992497] message in, get IO event: 4
[DEBUG] [1761992497] client[4]# nihao6666666

_fd_array[]: 3 4 
[DEBUG] [1761992505] get a new link event...
[DEBUG] [1761992505] message in, get IO event: 4
[DEBUG] [1761992505] client[4] quit, me too...
_fd_array[]: 3 
^C

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 2_SelectServer]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]

telnet> 
hello select!!!
nihao6666666
^]

telnet> quit
Connection closed.

五、I/O多路转接之poll

1,poll函数

poll函数的原型如下:

复制代码
头文件:
    #include <poll.h>
函数原型:
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
    fds是一个poll函数监听的结构列表;
    每一个元素中, 包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合;
    nfds表示fds数组的长度;
    timeout表示poll函数的超时时间, 单位是毫秒(ms);
返回结果:
    返回值小于0, 表示出错;
    返回值等于0, 表示poll函数等待超时;
    返回值大于0, 表示poll由于监听的文件描述符就绪而返回;

参数struct pollfd的结构:

复制代码
struct pollfd 
{
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

参数events和revents的取值:

|------------|--------------------------------------|----------|----------|
| 事件 | 描述 | 是否可以作为输入 | 是否可以作为输出 |
| POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
| POLLRDNORM | 普通数据可读 | 是 | 是 |
| POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
| POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
| POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
| POLLWRNORM | 普通数据可写 | 是 | 是 |
| POLLWRBAND | 优先级带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作。它由GNU 引人。 | 是 | 是 |
| POLLERR | 错误 | 否 | 是 |
| POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到 POLLHUP 事件。 | 否 | 是 |
| POLLNVAL | 文件描述符没有打开 | 否 | 是 |

2,poll函数的优点

不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.

pollfd结构包含了要监视的event和发生的event,不再使用select"参数-值"传递的方式. 接口使用比 select 更方便.

poll并没有最大数量限制(但是数量过大后性能也是会下降).

3,poll函数的缺点

poll中监听的文件描述符数目会逐渐增多.

和select函数一样, poll返回后,需要轮询pollfd来获取就绪的描述符.

每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.

同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

4,示例: 使用poll监控标准输入

复制代码
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ ls
Log.hpp  Main.cc  Makefile  PollServer.hpp  Server  Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ cat PollServer.hpp 
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__

#include <iostream>
#include <string>
#include <vector>
#include <poll.h>
#include <sys/time.h>
#include "Log.hpp"
#include "Sock.hpp"

#define FD_NONE -1

class PollServer
{
public:
    static const int nfds = 100;
public:
    PollServer(const uint16_t &port = 8080) 
        : _port(port), _nfds(nfds)
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");

        _fds = new struct pollfd[_nfds];
        for(int i = 0; i < _nfds; i++) 
        {
            _fds[i].fd = FD_NONE;
            _fds[i].events = _fds[i].revents = 0;
        }

        _fds[0].fd = _listensock;
        _fds[0].events = POLLIN;
        _timeout = 1000;
    }
    ~PollServer()
    {
        if (_listensock >= 0) close(_listensock);
        if (_fds) delete [] _fds;
    }
    void Start()
    {
        while (true)
        {
            int n = poll(_fds, _nfds, _timeout);
            switch (n)
            {
            case 0:
                logMessage(DEBUG, "%s", "time out...");
                break;
            case -1:
                logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
                break;
            default:
                HandlerEvent();
                break;
            }
        }
    }

private:
    void HandlerEvent()
    {
        for(int i = 0; i < _nfds; i++)
        {
            if(_fds[i].fd == FD_NONE) continue;
            if(_fds[i].revents & POLLIN) 
            {
                if(_fds[i].fd == _listensock) Accepter();
                else Recver(i);
            }
        }
    }
    void Accepter()
    {
        std::string clientip;
        uint16_t clientport = 0;
        int sock = Sock::Accept(_listensock, &clientip, &clientport); // 这里在进行accept会不会阻塞?不会!
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);

        int pos = 1;
        for(; pos < _nfds; pos++)
        {
            if(_fds[pos].fd == FD_NONE) break;
        }
        if(pos == _nfds)
        {
            logMessage(WARNING, "%s:%d", "select server already full, close: %d", sock);
            close(sock);
        }else
        {
            _fds[pos].fd = sock;
            _fds[pos].events = POLLIN;
        }
    }
    void Recver(int pos)
    {
        logMessage(DEBUG, "message in, get IO event: %d", _fds[pos]);
        char buffer[1024];
        int n = recv(_fds[pos].fd, buffer, sizeof(buffer)-1, 0);
        if(n > 0)
        {
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fds[pos].fd, buffer);
        }
        else if(n == 0)
        {
            logMessage(DEBUG, "client[%d] quit, me too...", _fds[pos]);
            close(_fds[pos].fd);
            _fds[pos].fd = FD_NONE;
            _fds[pos].events = 0;
        }
        else
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fds[pos], errno, strerror(errno));
            close(_fds[pos].fd);
            _fds[pos].fd = FD_NONE;
            _fds[pos].events = 0;
        }
    }
    void DebugPrint()
    {
        std::cout << "_fd_array[]: ";
        for(int i = 0; i < _nfds; i++)
        {
            if(_fds[i].fd == FD_NONE) continue;
            std::cout << _fds[i].fd << " ";
        }
        std::cout << std::endl;
    }

private:
    uint16_t _port;
    int _listensock;
    struct pollfd *_fds;
    int _nfds;
    int _timeout;
};

#endif
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ ./Server 
[DEBUG] [1762059604] create base socket success
[DEBUG] [1762059605] time out...
[DEBUG] [1762059606] time out...
[DEBUG] [1762059606] get a new line success : [127.0.0.1:43702] : 4
[DEBUG] [1762059607] time out...
[DEBUG] [1762059608] time out...
[DEBUG] [1762059609] time out...
[DEBUG] [1762059610] time out...
[DEBUG] [1762059611] message in, get IO event: 4
[DEBUG] [1762059611] client[4]# hahahaha

[DEBUG] [1762059612] time out...
[DEBUG] [1762059612] message in, get IO event: 4
[DEBUG] [1762059612] client[4]# 66666

[DEBUG] [1762059613] time out...
[DEBUG] [1762059614] message in, get IO event: 4
[DEBUG] [1762059614] client[4]# 77777

[DEBUG] [1762059615] message in, get IO event: 4
[DEBUG] [1762059615] client[4]# dfvsbtsdh

[DEBUG] [1762059616] time out...
[DEBUG] [1762059616] message in, get IO event: 4
[DEBUG] [1762059616] client[4]# 325r3r43tq

[DEBUG] [1762059617] time out...
[DEBUG] [1762059618] time out...
[DEBUG] [1762059619] time out...
[DEBUG] [1762059620] time out...
[DEBUG] [1762059620] message in, get IO event: 4
[DEBUG] [1762059620] client[4] quit, me too...
[DEBUG] [1762059621] time out...
^C

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 3_PollServer]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]

telnet> 
hahahaha
66666
77777
dfvsbtsdh
325r3r43tq
^]

telnet> quit
Connection closed.

六、I/O多路转接之epoll

1,epoll函数

按照man手册的说法: epoll 是为处理大批量句柄而作了改进的poll

它是在2.5.44内核中被引进的,几乎具备了之前所说的一切优点。

它公认为Linux2.6以下,性能最好的多路I/O就绪通知方法。

epoll 有3个相关的系统调用:
(1)epoll_create:

复制代码
功能:
    创建一个epoll的句柄.
函数:
    int epoll_create(int size);
说明:
    自从linux2.6.8之后, size参数是被忽略的.
    用完之后, 必须调用close()关闭.

(2)epoll_ctl:

复制代码
功能:
    epoll的事件注册函数.
    它不同于select()是在监听事件时,告诉内核要监听什么类型的事件. 
    epoll在这里要先注册要监听的事件类型. 
    
函数:
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:
    第一个参数是epoll_create()的返回值(epoll的句柄).
    第二个参数表示动作,用三个宏来表示.
    第三个参数是需要监听的fd.
    第四个参数是告诉内核需要监听什么事
    
第二个参数op取值:
    EPOLL_CTL_ADD : 注册新的fd到epfd中;
    EPOLL_CTL_MOD : 修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL : 从epfd中删除一个fd;
    
第四个参数struct epoll_event结构:
    struct epoll event
    {
        uint32 t events;   /* Epoll events */
        epoll data t data; /* User data variable */
    } EPOLL PACKED;
    
    typedef union epoll data
    {
        void *ptr;
        int fd;
        uint32 t u32;
        uint64 t u64;
    } epoll_data_t;
    
第四个参数events可以是以下几个宏的集合:
    EPOLLIN  : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
    EPOLLOUT : 表示对应的文件描述符可以写;
    EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); .  EPOLLERR : 表示对应的文件描述符发生错误;
    EPOLLHUP : 表示对应的文件描述符被挂断;
    EPOLLET  : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的;
    EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里;

(3)epoll_wait:

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

2,epoll的工作原理

epoll使用到了红黑树和双向链表:

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

eventpoll结构体

复制代码
struct eventpoll
{
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
    struct rb_root  rbr;     
    /*双链表中则存放着,将要通过epoll_wait返回给用户的满足条件的事件*/ 
    struct list_head rdlist; 
    ......
};

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.

这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插 入时间效率是lgn,其中n为树的高度).

而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时,会调用这个回调方法.

这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中. 在epoll中,对于每一个事件,都会建立一个epitem结构体.

epitem结构体:

复制代码
struct epitem
{
    struct rb_node  	  rbn;     //红黑树节点
    struct list_head    rdllink; //双向链表节点 
    struct epoll_filefd ffd;     //事件句柄信息
    struct eventpoll 	  *ep;     //指向其所属的eventpoll对象
    struct epoll_event  event;   //期待发生的事件类型 
};

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem 元素即可.

如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度 是O(1).

总结一下, 使用epoll的三部曲:

调用epoll_create创建一个epoll句柄;

调用epoll_ctl, 将要监控的文件描述符进行注册;

调用epoll_wait, 等待文件描述符就绪;

3,epoll的优点

接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开

数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而select/poll都是每次循环都要进行拷贝)

事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.

没有数量限制: 文件描述符数目无上限.

4,epoll的工作方式

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

举个例子:你妈喊你吃饭

你正在吃鸡 , 眼看进入了决赛圈 , 你妈饭做好了 , 喊你吃饭的时候有两种方式 :

  1. 如果你妈喊你一次 , 你没动 , 那么你妈会继续喊你第二次 , 第三次 ...(亲妈 , 水平触发)

  2. 如果你妈喊你一次 , 你没动 , 你妈就不管你了(后妈 , 边缘触发)

再举个例子:

我们已经把一个tcp socket添加到epoll描述符

这个时候socket的另一端被写入了2KB的数据

调用epoll_wait,并且它会返回 . 说明它已经准备好读取操作

然后调用read, 只读取了1KB的数据

继续调用epoll_wait......

水平触发 Level Triggered 工作模式

epoll默认状态下就是LT工作模式.

当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分;

如上面的例子,由于只读了1K数据,缓冲区中还剩1K数据;

在第二次调用 epoll_wait 时,epoll_wait仍然会立刻返回并通知socket读事件就绪;

直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回;

支持阳塞读写和非阻塞读写;

边缘触发 Edge Triggered 工作模式

如果我们在第1步将socket添加到epoll描述符的时候,使用了EPOLLET标志,epoll进入ET工作模式.

当epol检测到socket上事件就绪时,必须立刻处理;

如上面的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用 epoll_wait 的时候epoll_wait 不会再返回了;

也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会;

ET的性能比LT性能更高( epoll_wait 返回的次数少了很多):Nginx默认采用ET模式使用epoll;

只支持非阻塞的读写;

select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET。

5,理解LT和ET模式

LT是epoll 的默认行为. 使用ET 能够减少epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把 所有的数据都处理完.

相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比LT 更高效一些. 但是在LT 情况下如果也能做到 每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.

另一方面, ET 的代码复杂程度更高了.

使用ET 模式的epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是"工程实践" 上的要求.

假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求.

如果服务端写的代码是阻塞式的read, 并且一次只read 1k 数据的话(read不能保证一次就把所有的数据都读出来, 参考man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中.

此时由于 epol 是ET模式,并不会认为文件描述符读就绪;epo11_wait 就不会再次返回;剩下的 9k数据会一直在缓冲区中;直到下一次客户端再给服务器写数据;epoll _wait 才能返回

但是问题来了:

服务器只读到1k个数据, 要10k读完才会给客户端返回响应数据.

客户端要读到服务器的响应, 才会发送下一个请求

客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据.

所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来.

而如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.

6,epoll的使用场景

epoll的高性能, 是有一定的特定场景的.

如果场景选择的不适宜, epoll的性能可能适得其反.

对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.

例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.

如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根 据需求和场景特点来决定使用哪种IO模型.

7,epoll示例: epoll服务器(LT模式)

复制代码
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ ll
total 64
-rw-rw-r-- 1 user user   835 Nov  4 15:36 Epoll.hpp
-rw-rw-r-- 1 user user  4923 Nov  4 15:36 EpollServer.hpp
-rw-rw-r-- 1 user user  2582 Nov  2 16:28 Log.hpp
-rw-rw-r-- 1 user user   338 Nov  4 15:36 Main.cc
-rw-rw-r-- 1 user user    88 Nov  2 13:30 Makefile
-rwxrwxr-x 1 user user 34088 Nov  4 15:36 Server
-rw-rw-r-- 1 user user  3573 Nov  2 16:28 Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ cat Main.cc 
#include "EpollServer.hpp"
#include <memory>

using namespace std;
using namespace ns_epoll;

// 业务处理回调函数
void change(std::string request)
{
    std::cout << "change : " << request << std::endl;
}

int main()
{
    unique_ptr<EpollServer> epoll_server(new EpollServer(change));
    epoll_server->start();

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ cat Epoll.hpp 
#pragma once

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

class Epoll
{
public:
    static const int gsize = 256;
public:
    static int CreateEpoll()
    {
        int epfd = epoll_create(gsize);
        if(epfd > 0) return epfd;
        exit(5);
    }
    static bool CtlEpoll(int epfd, int oper, int sock, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(epfd, oper, sock, &ev);
        return n == 0;
    }
    static bool WaitEpoll(int epfd, struct epoll_event revs[], int num, int timeout)
    {
        // 细节1: 如果底层就绪的sock非常多,revs装不下怎么办? 不影响,下次接着拿
        // 细节2: 关于epoll_wait的返回值问题
        return epoll_wait(epfd, revs, num, timeout); 
    }
};
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ cat EpollServer.hpp 
#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__

#include <iostream>
#include <string>
#include <error.h>
#include <functional>
#include <cassert>
#include "Log.hpp"
#include "Sock.hpp"
#include "Epoll.hpp"

namespace ns_epoll
{
    static const int default_port = 8080;
    static const int gnum = 64;

    // LT模式 -- 只处理读取
    class EpollServer
    {
    public:
        using func_t = std::function<void(std::string)>;
    public:
        EpollServer(func_t HandlerRequest, const int &port = default_port)
            :_port(port), _revs_num(gnum), _HandlerRequest(HandlerRequest)
        {
            // 0.申请对应空间
            _revs = new struct epoll_event[_revs_num];            

            // 1.创建listensock
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);

            // 2.创建epoll模型
            _epfd = Epoll::CreateEpoll();
            logMessage(DEBUG, "create epoll mode success, _listensock:%d, _epfd:%d", _listensock, _epfd);
        
            // 3.将listensock添加到epoll中
            if(!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD , _listensock, EPOLLIN)) exit(6);
            logMessage(DEBUG, "add listensock to epoll success, _listensock:%d, _epfd:%d", _listensock, _epfd);
        }
        ~EpollServer()
        {
            if(_listensock >= 0) close(_listensock);
            if(_epfd >= 0) close(_epfd);
            if(_revs) delete[] _revs;
        }
        void start()
        {
            int timeout = -1;
            while(true)
            {
                loopOnce(timeout);
            }
        }
        void loopOnce(int timeout) // 循环一次
        {
            int n = Epoll::WaitEpoll(_epfd, _revs, _revs_num, timeout);
            switch (n)
            {
            case 0:
                logMessage(DEBUG, "timeout ......");
                break;
            case -1:
                logMessage(WARNING, "epoll wait error: %s", strerror(errno));
                break;            
            default:
                // 等待成功
                logMessage(DEBUG, "get a event");
                HandlerEvents(n);
                break;
            }
        }
        void HandlerEvents(int n)
        {
            assert(n > 0);
            for(int i = 0; i < n; i++)
            {
                uint32_t revents = _revs[i].events;
                int sock = _revs[i].data.fd;
                // 读事件就绪
                if(revents & EPOLLIN)
                {
                    if(sock == _listensock) Accepter(_listensock); // 1. listensock 就绪
                    else Recver(sock);                             // 2. 一般sock就绪 - read   
                }
            }
        }
        void Accepter(int listensock)
        {
            std::string clientip;
            uint16_t clientport;
            int sock = Sock::Accept(listensock, &clientip, &clientport);
            if(sock < 0)
            {
                logMessage(WARNING, "accept error!");
                return;
            }
            // 能不能直接读取?不能,因为你并不清楚,底层是否有数据!
            // 将新的sock,添加给epoll
            if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN)) return;
            logMessage(DEBUG, "add new sock : %d to epoll success", sock);   
        }
        void Recver(int sock)
        {
            // 1. 读取数据
            char buffer[10240];
            ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);
            if(n > 0)
            {
                // 假设这里就是读到了一个完整的报文 // 如何保证??
                buffer[n] = 0;
                _HandlerRequest(buffer); // 2. 处理数据
            }
            else if(n == 0)
            {
                // 1. 先在epoll中去掉对sock的关心
                bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
                assert(res);
                (void)res;
                // 2. 在close文件
                close(sock);
                logMessage(NORMAL, "client %d quit, me too...", sock);
            }
            else
            {
                // 1. 先在epoll中去掉对sock的关心
                bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
                assert(res);
                (void)res;
                // 2. 在close文件
                close(sock);
                logMessage(NORMAL, "client recv %d error, close error sock", sock);
            }
        }
    private:
        uint16_t _port;
        int _listensock; 
        int _epfd; // epoll_create()的返回值(epoll的句柄)
        struct epoll_event *_revs; // 就绪元素
        int _revs_num; // 就绪元素容量大小
        func_t _HandlerRequest; // 回调处理函数
    };
    
} // namespace name

#endif
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ ./Server 
[DEBUG] [1762241823] create epoll mode success, _listensock:3, _epfd:4
[DEBUG] [1762241823] add listensock to epoll success, _listensock:3, _epfd:4
[DEBUG] [1762241836] get a event
[DEBUG] [1762241836] add new sock : 5 to epoll success
[DEBUG] [1762241844] get a event
change : hahahahaha

[DEBUG] [1762241848] get a event
change : 6666666666

[DEBUG] [1762241859] get a event
change : qaqaqaqaqa

[DEBUG] [1762241863] get a event
[NORMAL] [1762241863] client 5 quit, me too...
^C

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 4_EpollServer]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]

telnet> 
hahahahaha
6666666666
qaqaqaqaqa
^]

telnet> quit
Connection closed.

8,epoll示例: epoll服务器(ET模式)

复制代码
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ ll
total 168
-rw-rw-r-- 1 user user   1068 Nov  6 11:44 Epoll.hpp
-rw-rw-r-- 1 user user  10263 Nov  6 11:57 EpollServer.hpp
-rw-rw-r-- 1 user user   2582 Nov  2 16:28 Log.hpp
-rw-rw-r-- 1 user user   1635 Nov  6 11:57 Main.cc
-rw-rw-r-- 1 user user     88 Nov  2 13:30 Makefile
-rw-rw-r-- 1 user user   3043 Nov  6 11:35 Protocol.hpp
-rwxrwxr-x 1 user user 131712 Nov  6 11:58 Server
-rw-rw-r-- 1 user user   3992 Nov  5 15:05 Sock.hpp
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Main.cc 
#include "EpollServer.hpp"
#include <memory>

static Response calculator(const Request &req)
{
    Response resp(0, 0);
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;
    case '-':
        resp.result_ = req.x_ - req.y_;
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
        if (0 == req.y_)
            resp.code_ = 1;
        else
            resp.result_ = req.x_ / req.y_;
        break;
    case '%':
        if (0 == req.y_)
            resp.code_ = 2;
        else
            resp.result_ = req.x_ % req.y_;
        break;
    default:
        resp.code_ = 3;
        break;
    }
    return resp;
}

void NetCal(Connection *conn, std::string &request)
{
    logMessage(DEBUG, "NetCal been called, get request: %s", request.c_str());
    // 1. 反序列化
    Request req;
    if (!req.Deserialized(request))
        return;
    // 2. 业务处理
    Response resp = calculator(req);
    // 3. 序列化,构建应答
    std::string sendstr = resp.Serialize();
    sendstr = Encode(sendstr);
    // 4. 交给服务器conn
    conn->_outbuffer += sendstr;
    // 5. 想办法,让底层的TcpServer,让它开始发送
    // a. 需要有完整的发送逻辑
    // b. 我们触发发送的动作,一旦我们开启EPOLLOUT,epoll会自动立马触发一次发送事件就绪,如果后续保持发送的开启,epoll会一直发送
    conn->_esvr->EnableReadWrite(conn, true, true);
}

int main()
{
    std::unique_ptr<EpollServer> epoll_server(new EpollServer());
    epoll_server->start(NetCal);

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Protocol.hpp 
#pragma once

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

// 1. 报文和报文之间,我们采用特殊字符来进行解决粘报问题
// 2. 获取一个一个独立完整的报文,序列和反序列化 -- 自定义
// 100+19X100+19X100+19

// 支持解决粘报问题,处理独立报文
#define SEP "X"
#define SEP_LEN strlen(SEP)

// 自己手写序列反序列化
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

// 我们要把传入进来的缓冲区进行切分
// 1. buffer被切走的,也同时要从buffer中移除
// 2. 可能会存在多个报文,多个报文依次放入out
// buffer: 输入输出型参数
// out: 输出型参数
void SpliteMessage(std::string &buffer, std::vector<std::string> *out)
{
    // 100+
    // 100+19X1
    // 100+19X100+19
    while (true)
    {
        auto pos = buffer.find(SEP);
        if (std::string::npos == pos)
            break;
        std::string message = buffer.substr(0, pos);
        buffer.erase(0, pos + SEP_LEN);
        out->push_back(message);
        // std::cout << "debug: " << message << " : " << buffer << std::endl;
        // sleep(1);
    }
}

// TODO
std::string Encode(std::string &s)
{
    return s + SEP;
}

class Request
{
public:
    std::string Serialize()
    {
        std::string str;
        str = std::to_string(x_);
        str += SPACE;
        str += op_; // TODO
        str += SPACE;
        str += std::to_string(y_);
        return str;
    }
    bool Deserialized(const std::string &str) // 1 + 1
    {
        std::size_t left = str.find(SPACE);
        if (left == std::string::npos)
            return false;
        std::size_t right = str.rfind(SPACE);
        if (right == std::string::npos)
            return false;
        x_ = atoi(str.substr(0, left).c_str());
        y_ = atoi(str.substr(right + SPACE_LEN).c_str());
        if (left + SPACE_LEN > str.size())
            return false;
        else
            op_ = str[left + SPACE_LEN];
        return true;
    }

public:
    Request() {}
    Request(int x, int y, char op) : x_(x), y_(y), op_(op)
    {
    }
    ~Request() {}

public:
    int x_;   // 是什么?
    int y_;   // 是什么?
    char op_; // '+' '-' '*' '/' '%'
};

class Response
{
public:
    // "code_ result_"
    std::string Serialize()
    {

        std::string s;
        s = std::to_string(code_);
        s += SPACE;
        s += std::to_string(result_);
        return s;
    }
    // "111 100"
    bool Deserialized(const std::string &s)
    {
        std::size_t pos = s.find(SPACE);
        if (pos == std::string::npos)
            return false;
        code_ = atoi(s.substr(0, pos).c_str());
        result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
        return true;
    }

public:
    Response() {}
    Response(int result, int code) : result_(result), code_(code)
    {
    }
    ~Response() {}

public:
    // 约定!
    // result_? code_? code_ 0? 1?2?3?
    int result_; // 计算结果
    int code_;   // 计算结果的状态码
};
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Epoll.hpp 
#pragma once

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

class Epoll
{
public:
    static const int gsize = 256;

public:
    Epoll() {}
    ~Epoll() {}
    bool CtrlEpoll(int sock, uint32_t events)
    {
        events |= EPOLLET;
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
        return n == 0;
    }
    bool AddSockToEpoll(int sock, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
        return n == 0;
    }
    int WaitEpoll(struct epoll_event revs[], int num, int timeout)
    {
        return epoll_wait(_epfd, revs, num, timeout);
    }
    void CreateEpoll()
    {
        _epfd = epoll_create(gsize);
        if (_epfd < 0)
            exit(5);
    }
    bool DelFromEpoll(int sock)
    {
        int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
        return n == 0;
    }

private:
    int _epfd;
};
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat EpollServer.hpp 
#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__

#include <iostream>
#include <string>
#include <error.h>
#include <functional>
#include <cassert>
#include <vector>
#include <unordered_map>
#include "Log.hpp"
#include "Sock.hpp"
#include "Epoll.hpp"
#include "Protocol.hpp"

class EpollServer;
class Connection;

using func_t = std::function<void(Connection *)>;
using callback_t = std::function<void(Connection *, std::string &request)>;

// ET模式 -- 常规sock必须要有自己的 接受缓冲区 && 发送缓冲区
class Connection
{
public:
    Connection(int sock = -1) : _sock(sock), _esvr(nullptr)
    {
    }
    ~Connection() {}
    void SetHandler(func_t recv_cb, func_t send_cb, func_t except_cb)
    {
        _recv_cb = recv_cb;
        _send_cb = send_cb;
        _except_cb = except_cb;
    }

public:
    int _sock; // 负责进行IO的fd
    // 三个回调方法,表征的就是对_sock进行特点读写对应的方法
    func_t _recv_cb;   // 读取 -- 回调函数
    func_t _send_cb;   // 写入 -- 回调函数
    func_t _except_cb; // 异常 -- 回调函数
    // 接受缓冲区 && 发送缓冲区
    std::string _inbuffer;  // 接收缓冲区 -- 暂时没有办法处理二进制流,文本是可以的
    std::string _outbuffer; // 发送缓冲区
    // 设置对EpollServer的回值指针
    EpollServer *_esvr;
};

// ET模式
class EpollServer
{
    const static int gport = 8080;
    const static int gnum = 128;
    const static int gtimeout = 5;

public:
    EpollServer(int port = gport) : _port(port), _revs_num(gnum)
    {
        // 0.构建一个获取就绪事件的缓冲区
        _revs = new struct epoll_event[_revs_num];

        // 1.创建listensock
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        // 2.创建epoll模型
        _epoll.CreateEpoll();

        // 3.添加_listensock到服务器中
        AddConnection(_listensock, std::bind(&EpollServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
    }
    ~EpollServer()
    {
        if (_listensock >= 0)
            close(_listensock);
        if (_revs)
            delete[] _revs;
    }

    // 专门针对任意sock添加到EpollServer
    void AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb)
    {
        // 1.使用ET模式,需要先把sock设置为非阻塞
        Sock::SetNonBlock(sock);

        // 2.构建conn对象,封装sock
        // 除了_listensock, 未来会获取大量socket,每一个socket都要封装成Connection
        // 所以EpollServer本身,就要管理所有的Connection
        Connection *conn = new Connection(sock);
        conn->SetHandler(recv_cb, send_cb, except_cb);
        conn->_esvr = this;

        // 3.将sock[]添加到epoll中
        // 任何多路转接的服务器,一般默认只打开对读取的关心
        _epoll.AddSockToEpoll(sock, EPOLLIN | EPOLLET);

        // 4.将Connection*添加到map映射表
        _Connections.insert(std::make_pair(sock, conn));
    }

    // 接收连接请求,并把sock添加到Epoll模型
    void Accepter(Connection *conn)
    {
        // logMessage(DEBUG, "Accepter is called");
        // 这里一定是_listensock已经就绪了,此次读取会阻塞吗?不会
        // 底层不只一个连接就绪,需要用while多次读取
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int accept_errno = 0;
            // sock一定是常规的IO sock
            int sock = Sock::Accept(conn->_sock, &clientip, &clientport, &accept_errno);
            if (sock < 0)
            {
                if (accept_errno == EAGAIN || accept_errno == EWOULDBLOCK)
                    break;
                else if (accept_errno == EINTR)
                    continue; // 概率非常低
                else
                {
                    // accept失败
                    logMessage(WARNING, "accept error, %d : %s", accept_errno, strerror(accept_errno));
                    break;
                }
            }
            // 将sock托管给epollserver
            if (sock >= 0)
            {
                // accept成功
                AddConnection(sock, std::bind(&EpollServer::Recver, this, std::placeholders::_1),
                              std::bind(&EpollServer::Sender, this, std::placeholders::_1),
                              std::bind(&EpollServer::Excepter, this, std::placeholders::_1));
                logMessage(DEBUG, "accept client %s:%d success, add to epoll && EpollServer success", clientip.c_str(), clientport);
            }
        }
    }

    // 读取处理 (v1 -> v2还能优化)
    void Recver(Connection *conn)
    {
        const int num = 1024;
        bool err = false;
        // logMessage(DEBUG, "Recver event exists, Recver() been called");
        // v1: 直接面向字节流,先进行常规读取
        while (true)
        {
            char buffer[num];
            ssize_t n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);
            if (n < 0)
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break; // 正常的
                else if (errno == EINTR)
                    continue;
                else
                {
                    logMessage(ERROR, "recv error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    err = true;
                    break;
                }
            }
            else if (n == 0)
            {
                logMessage(DEBUG, "client[%d] quit, server close [%d]", conn->_sock, conn->_sock);
                conn->_except_cb(conn);
                err = true;
                break;
            }
            else
            {
                // 读取成功
                buffer[n] = 0;
                conn->_inbuffer += buffer;
            }
        }
        // end while
        logMessage(DEBUG, "conn->_inbuffer[sock: %d]: %s", conn->_sock, conn->_inbuffer.c_str());
        // err = false 报文完整
        if (!err)
        {
            std::vector<std::string> messages;
            SpliteMessage(conn->_inbuffer, &messages);
            // 我能保证走到这里,就是一个完整报文
            for (auto &msg : messages)
                _cb(conn, msg); // 可以在这里将message封装成为task,然后push到任务队列,任务处理交给后端线程池
        }
    }

    // 发送处理 - 最开始的时候,我们的conn 是没有被触发的!
    void Sender(Connection *conn)
    {
        while (true)
        {
            ssize_t n = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
            if (n > 0)
            {
                conn->_outbuffer.erase(0, n);
                if (conn->_outbuffer.empty())
                    break;
            }
            else
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                else if (errno == EINTR)
                    continue;
                else
                {
                    logMessage(ERROR, "send error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    break;
                }
            }
        }
        // 发完了吗?不确定,但是我们保证,如果没有出错,一定是要么发完,要么发送条件不满足,下次发送
        if (conn->_outbuffer.empty())
            EnableReadWrite(conn, true, false);
        else
            EnableReadWrite(conn, true, true);
    }

    // 使事件能够读和写
    void EnableReadWrite(Connection *conn, bool readable, bool writeable)
    {
        uint32_t events = ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
        bool res = _epoll.CtrlEpoll(conn->_sock, events);
        assert(res); // 更改成if
    }

    // 异常处理
    void Excepter(Connection *conn)
    {
        if (!IsConnectionExists(conn->_sock))
            return;
        // 1. 从epoll中移除
        bool res = _epoll.DelFromEpoll(conn->_sock);
        assert(res); // 要判断
        // 2. 从我们的unorder_map中移除
        _Connections.erase(conn->_sock);
        // 3. close(sock);
        close(conn->_sock);
        // 4. delete conn;
        delete conn;

        logMessage(DEBUG, "Excepter 回收完毕,所有的异常情况");
    }

    // 根据就绪的事件,进行特点事件派发
    void start(callback_t cb)
    {
        _cb = cb;
        while (true)
        {
            LoopOnce();
        }
    }

    // 循环一次
    void LoopOnce()
    {
        int n = _epoll.WaitEpoll(_revs, _revs_num, gtimeout);
        for (int i = 0; i < n; i++)
        {
            uint32_t revents = _revs[i].events;
            int sock = _revs[i].data.fd;
            // 读事件就绪
            if (revents & EPOLLIN)
            {
                if (IsConnectionExists(sock) && _Connections[sock]->_recv_cb != nullptr)
                    _Connections[sock]->_recv_cb(_Connections[sock]);
            }
            // 写事件就绪
            if (revents && EPOLLOUT)
            {
                if (IsConnectionExists(sock) && _Connections[sock]->_send_cb != nullptr)
                    _Connections[sock]->_recv_cb(_Connections[sock]);
            }
        }
    }

    // 判断连接是否存在
    bool IsConnectionExists(int sock)
    {
        auto iter = _Connections.find(sock);
        if (iter == _Connections.end())
            return false;
        else
            return true;
    }

private:
    uint16_t _port;                                     // 监听套接字
    int _listensock;                                    // 端口号
    struct epoll_event *_revs;                          // 就绪元素
    int _revs_num;                                      // 就绪元素容量大小
    Epoll _epoll;                                       // epll模型
    std::unordered_map<int, Connection *> _Connections; // 哈希映射 Sock : Connections
    callback_t _cb;                                     // 上层业务处理的回调函数
};

#endif
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Sock.hpp 
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// #include "Log.hpp"

// 套接字类
class Sock
{
public:
    Sock() {}
    ~Sock() {}

    // 创建socket文件描述符  (TCP/UDP, 客户端 + 服务器)
    static int Socket()
    {
        // 1.创建套接字
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            // logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        // logMessage(NORMAL, "create socket success , _listensock:%d", listensock);
        
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        return listensock;
    }
 
    // 绑定端口号  (TCP/UDP, 服务器)
    static int Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // 2.设置缓冲区,把ip和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 = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());

        // 3.绑定进程
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            // logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    // 监听套接字 (TCP, 服务器)
    static void Listen(int sock)
    {
        // 4.建立链接
        // tcp是面向连接的,正式通信的时候,需要先建立链接
        if (listen(sock, gbacklog) < 0)
        {
            // logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        // logMessage(NORMAL, "listen socket success");
    }

    // 三种参数类型
    // const std::string &: 输入型参数
    // std::string *:       输出型参数
    // std::string &:       输入输出型参数

    // 接收请求 (TCP, 服务器)
    static int Accept(int listensock, std::string *ip, uint16_t *port, int *accept_errno)
    {
        // 5.获取连接
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        *accept_errno = 0;
        // ServiceSock(提供服务的李四王五) vs listensock(拉客的张三)
        int ServiceSock = accept(listensock, (struct sockaddr *)&src, &len);
        if (ServiceSock < 0) // 获取连接失败
        {
            // logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            *accept_errno = errno;
            return -1;
        }

        // 6.获取连接成功,把ip和port转换成主机序列
        if (port)
            *port = ntohs(src.sin_port);
        if (ip)
            *ip = inet_ntoa(src.sin_addr);
        return ServiceSock;
    }

    // 建立连接  (TCP, 客户端)
    static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
            return true;
        else 
            return false;
    }

    // 把socket设置为非阻塞 (Epoll - ET模式)
    static bool SetNonBlock(int sock)
    {
        int fl = fcntl(sock, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
        if(fl < 0) return false;
        fcntl(sock, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
        return true;
    }
private:
    // gbacklog连接队列总容量 = accept已连接数 + SYN待连接数
    const static int gbacklog = 10;
};
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ cat Log.hpp 
#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

// 日志级别映射表
const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

// 日志文件设置
#define LOGFILE "./log.log"

// 可变参数日志
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
// 条件编译
#ifndef DEBUG_SHOW
    // 如果编译的时候没有携带DEBUG_SHOW选项,就不会打印DEBUG级别的日志
    if(level== DEBUG) return;
#endif
    // 传统写法: 手动提取参数列表
    // va_list ap;
    // va_start(ap, format);
    // int x = va_arg(ap, int);
    // va_end(ap);
    
    // 现代写法: 调用可变参数打印函数
    // 让用户传入参数的打印函数
    // #include <stdio.h>  
    // int printf(const char *format, ...);
    // int fprintf(FILE *stream, const char *format, ...);
    // int sprintf(char *str, const char *format, ...);
    // int snprintf(char *str,size_t size,const char *format, ...);

    // 处理可变参数的打印函数
    // #include <stdarg.h>
    // int vprintf(const char *format, va_list ap);                         // 格式化显示到 -> 显示器
    // int vfprintf(FILE *stream, const char *format, va_list ap);          // 格式化显示到 -> 文件
    // int vsprintf(char *str, const char *format,va_list ap);              // 格式化显示到 -> 字符串
    // int vsnprintf(char *str,size_t size, const char *format,va_list ap); // 格式化显示到 -> 指定长度字符串

    // 日志固定消息部分 -- 等级/时间
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);

    // 可变参数部分
    char logBuffer[1024];
    va_list ap;
    va_start(ap, format);
    // vprintf(format, ap); // 格式化显示到 -> 显示器
    vsnprintf(logBuffer, sizeof(logBuffer), format, ap); // 格式化显示到 -> 指定长度字符串
    va_end(ap);

    // 打印日志消息 = 固定部分 + 可变部分
    printf("%s%s\n", stdBuffer, logBuffer);

    // 日志消息 -- 还可以打印到文件里
    // FILE *fp = fopen(LOGFILE, "a");
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ make clean;make
rm -f Server
g++ -o Server Main.cc -std=c++11 -DDEBUG_SHOW
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ ./Server 
[DEBUG] [1762402213] accept client 127.0.0.1:50944 success, add to epoll && EpollServer success
[DEBUG] [1762402219] conn->_inbuffer[sock: 5]: 1+2X3-4X5*6X

[DEBUG] [1762402219] NetCal been called, get request: 1+2
[DEBUG] [1762402219] NetCal been called, get request: 3-4
[DEBUG] [1762402219] NetCal been called, get request: 5*6
[DEBUG] [1762402219] conn->_inbuffer[sock: 5]: 

[DEBUG] [1762402226] conn->_inbuffer[sock: 5]: 
7/8

[DEBUG] [1762402226] conn->_inbuffer[sock: 5]: 
7/8

[DEBUG] [1762402230] conn->_inbuffer[sock: 5]: 
7/8
X9%9X

[DEBUG] [1762402230] NetCal been called, get request: 
7/8

[DEBUG] [1762402230] NetCal been called, get request: 9%9
[DEBUG] [1762402230] conn->_inbuffer[sock: 5]: 

[DEBUG] [1762402245] conn->_inbuffer[sock: 5]: 
1#10

[DEBUG] [1762402245] conn->_inbuffer[sock: 5]: 
1#10

[DEBUG] [1762402251] conn->_inbuffer[sock: 5]: 
1#10
X

[DEBUG] [1762402251] NetCal been called, get request: 
1#10

[DEBUG] [1762402251] conn->_inbuffer[sock: 5]: 

[DEBUG] [1762402258] client[5] quit, server close [5]
[DEBUG] [1762402258] Excepter 回收完毕,所有的异常情况
[DEBUG] [1762402258] conn->_inbuffer[sock: -1685493832]: 

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ 5_EpollServer_ET]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]

telnet> 
1+2X3-4X5*6X
7/8
X9%9X
10#10      
X
^]

telnet> quit
Connection closed.

点个赞吧!666

相关推荐
python百炼成钢1 小时前
解决——linux通过网络挂载tftp无法下载
linux·运维·网络
林鸿群1 小时前
Ubuntu 25.10编译Chromium源码
linux·chrome·ubuntu·chromium·源码编译
潇I洒1 小时前
Ubuntu Linux 24.04 安装MySQL 8.4.7
linux·数据库·mysql·ubuntu
程序员 _孜然1 小时前
linux调试外部RTC hym8563
linux·驱动开发·嵌入式硬件·ubuntu
milanyangbo1 小时前
像Git一样管理数据:深入解析数据库并发控制MVCC的实现
服务器·数据库·git·后端·mysql·架构·系统架构
无奈笑天下1 小时前
银河麒麟高级服务器版本【更换bond绑定的网卡】操作方法
linux·运维·服务器·arm开发·经验分享
jwybobo20071 小时前
redis7.x源码分析:(9) 内存淘汰策略
linux·c++·redis
红石榴花生油1 小时前
Linux服务器权限与安全核心笔记
java·linux·前端
深海里的鱼(・ω<)★1 小时前
CentOS 7 默认 yum 源官方不维护解决方案
linux·运维·centos