• 【全局教学】手把手教你做一个muduo服务器

【全局教学】手把手教你做一个muduo服务器

2025-05-25 14:57:12 1 阅读

目录

1.准备知识

1.1时间轮

1.2正则表达式

1.3 通用类型Any类型的实现

2.Server服务器模块实现:

2.1 缓冲区Buffer类的实现

2.2 日志宏的实现

 2.3 套接字模块的实现

2.4 事件管理Channel类的实现

2.5 描述符事件监控Poller类实现

2.6 定时任务管理TimerWheel类实现

2.7 Reactor-EventLoop线程池类实现

 2.8 用于管理单个事件循环线程LoopThread类

2.9  线程池的主要逻辑LoopThreadPool类

 2.10 Any类代码的实现

2.11 通信连接管理Connection类实现

 2.12 监听描述符管理Acceptor模块

2.13 服务器类TcpServer类的实现

3.HTTP协议支持模块实现

3.1 Util实用工具类实现

3.2 HttpRequest 类的实现

3.3 HttpResponse类的实现

3.4 HttpContext类的实现

3.5 HttpServer类的实现

4.结语


1.准备知识

1.1时间轮

如下图,这就是时间轮的图像,接下来我来解释一下这个时间轮的原理

 中间的指针我们称为_tick,用来表示现在指针指向的位置,指针指向这个位置的时候,就去检查这个位置上面是否有对应任务到了时间,如果时间已到,那么就移除这个任务,并且执行这个对应的任务。

如何实现?

首先我们实现一个TimerTask的类,这个类就是用来包含具体的任务,超时时间等等,以便这个任务时间到了的时候执行对应的任务

先包装任务函数

using TaskFunc = std::function;
using ReleaseFunc = std::function;

包含的私有成员

uint64_t _id;         // 定时器任务对象id
uint32_t _timeout;    // 定时任务的超时时间
bool _canceled;       // false-表示没有被取消
TaskFunc _task_cb;    // 定时器对象要执行的定时任务
ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息

包含的公有成员有设置是否取消任务、设置释放时用来删除定时器对象信息的函数、设置超时时间

void Cancel()
{
    _canceled = true;
}

void SetRelease(const ReleaseFunc& cb)
{
    _release = cb;
}

uint32_t DelayTime()
{
    return _timeout;
}

TimerTask的整体代码如下:

class TimerTask
{
private:
    uint64_t _id;         // 定时器任务对象id
    uint32_t _timeout;    // 定时任务的超时时间
    bool _canceled;       // false-表示没有被取消
    TaskFunc _task_cb;    // 定时器对象要执行的定时任务
    ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb)
        : _id(id), _timeout(delay), _task_cb(cb), _canceled(false)
    {
    }

    ~TimerTask()
    {
        if(_canceled == false)_task_cb();
        _release();
    }

    void Cancel()
    {
        _canceled = true;
    }

    void SetRelease(const ReleaseFunc& cb)
    {
        _release = cb;
    }

    uint32_t DelayTime()
    {
        return _timeout;
    }
};

接下来就是时间轮的主体,时间轮_wheel我们使用一个vector来实现,_tick指针遍历完一遍数组之后从头开始遍历数组,数组的每一个位置中,也存放着一个数组,这个数组中存放任务。

using WeakTask = std::weak_ptr;
using PtrTask = std::shared_ptr;
int _tick;     // 当前的秒针,走到哪里释放哪里
int _capacity; // 表盘最大数量,其实就是最大延迟时间
std::vector> _wheel;
std::unordered_map _timers; //使用任务ID找到对应的weak_ptr

这里解释一下为什么要使用weak_ptr来作为对应TimerTask的指针,因为weak_ptr不会增加其引用计数,不影响shared_ptr中的引用计数为0时对TimerTask的正常释放

TimerTask_timers中移除

void RemoveTimer(uint16_t id)
{
    auto it = _timers.find(id);
    if(it != _timers.end())
    {
        _timers.erase(it);
    }
}

添加定时任务

void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc& cb)
{
    // 创建一个新的
    PtrTask pt(new TimerTask(id, delay, cb));
    // 设置释放函数
    pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
    // 当前指针的位置 加上 超时时间 除 时间轮的容量得到插入位置
    int pos = (_tick + delay) % _capacity;
    _wheel[pos].push_back(pt);
    // 把任务放入哈希表中
    _timers[id] = WeakTask(pt);
}

刷新/延迟定时任务,这里需要说明的是,这里使用的lock(),是可以把weak_ptr提升为shared_ptr 强智能指针,再添加一个TimerTask进去,相当于把shared_ptr的引用计数增加1,变相地起到了刷新/延迟定时任务的作用

void TimerRefresh(uint64_t id)//刷新/延迟定时任务
{
    //通过保存的定时器对象的weakptr构造一个shared_ptr出来,添加到轮子中
    auto it = _timers.find(id);
    if(it == _timers.end())
    {
        //没有找到定时任务
        return;
    }
    PtrTask pt = it->second.lock();//lock获取weakptr管理的对象对应的shared_ptr;
    int delay = pt->DelayTime();
    int pos = (_tick + delay) % _capacity;
    _wheel[pos].push_back(pt);
}

取消定时任务

void TimerCancel(uint64_t id)
{
    auto it = _timers.find(id);
    if(it == _timers.end())
    {
        //没有找到定时任务
        return;
    }
    PtrTask pt = it->second.lock();
    if(pt) pt->Cancel();
}

执行定时任务

void RunTimeTask()
{
    _tick = (_tick + 1) % _capacity;
    _wheel[_tick].clear();
}

总体的时间轮代码

class TimerWheel
{
private:
    using WeakTask = std::weak_ptr;
    using PtrTask = std::shared_ptr;
    int _tick;     // 当前的秒针,走到哪里释放哪里
    int _capacity; // 表盘最大数量,其实就是最大延迟时间
    std::vector> _wheel;
    std::unordered_map _timers; //使用任务ID找到对应的weak_ptr
private:
    void RemoveTimer(uint16_t id)
    {
        auto it = _timers.find(id);
        if(it != _timers.end())
        {
            _timers.erase(it);
        }
    }
public:
    TimerWheel()
        : _capacity(60),
          _tick(0),
          _wheel(_capacity)
    {
    }
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc& cb) //添加定时任务
    {
        PtrTask pt(new TimerTask(id, delay, cb));
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
        _timers[id] = WeakTask(pt);
    }
    void TimerRefresh(uint64_t id)//刷新/延迟定时任务
    {
        //通过保存的定时器对象的weakptr构造一个shared_ptr出来,添加到轮子中
        auto it = _timers.find(id);
        if(it == _timers.end())
        {
            //没有找到定时任务
            return;
        }
        PtrTask pt = it->second.lock();//lock获取weakptr管理的对象对应的shared_ptr;
        int delay = pt->DelayTime();
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
    }
    void TimerCancel(uint64_t id)
    {
        auto it = _timers.find(id);
        if(it == _timers.end())
        {
            //没有找到定时任务
            return;
        }
        PtrTask pt = it->second.lock();
        if(pt) pt->Cancel();
    }
    //这个函数每秒被执行一次,相当于秒针向后走了一步
    void RunTimeTask()
    {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear();
    }
};

1.2正则表达式

首先,必不可少的,是对http请求行的解析,但是,如果直接手撕请求行,把请求行分为多个部分,未免有些麻烦,所以我们选择使用正则表达式来解析请求行,更为精确高效,

首先我们需要知道HTTP请求行的基本结构

<请求目标>

 HTTP方法包括GET、POST、PUT、DELETE等方法

请求目标就是用户想要请求资源的对应路径

下面是一个HTTP请求行的例子:

GET /helloworld/index?user=diluodexingzi&pass=123456 HTTP/1.1

 首先我们需要匹配HTTP方法,如下:

(GET|HEAD|POST|PUT|DELETE) 

| 代表或的意思,表示从这几个常见的HTTP方法中选择一个

接下来我们需要来匹配资源路径

 ([^?]*)

[^?]表示匹配的是非?的字符,加上后面的*就表示匹配非?字符0次或多次

 后面的user=diluodexingzi&pass=123456是我们不需要获取的,只匹配但是不捕获

(?:?(.*))?

(?:pattern)表示匹配pattern但是不获取匹配结果

? 表示原始的?字符,这里表示以?字符作为起始

.*代表提取除 之外的任意字符0次或多次

最后的?表示匹配前面的表达式0次或者一次,有的请求是没有查询字符串的

 最后来匹配HTTP版本

(HTTP/1.[01])(?: | )?

HTTP/1 表示以HTTP/1开始的字符串

. 表示匹配 . 原始字符

[01]表示匹配字符串0或者1字符

(?: ) 表示匹配一个 或者 字符,但是不捕捉这个内容

总的来说就是匹配以HTTP/1.开始,后面跟了一个0或1的字符,且最终以 或者 作为结尾的字符串

1.3 通用类型Any类型的实现

每一个Connection对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在Connection中需要设置协议处理的上下文来控制处理节奏。但是应用层协议千千万,为了降低耦合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息因此就需要一个通用的类型来保存各种不同的数据结构。

为了增加代码的可移植性,我们减少对第三方库的依赖,决定自己来实现。

Any类肯定不能是一个模版类,否则编译的时候Any a,Any b,需要传入类型作为模版参数,也就是说在使用的时候就要确定其类型。

这是不可以的,因为保存在Content中的协议上下文,我们在定义any对象的时候是不知道他们的协议类型的,因此无法传递类型作为模版参数

因此考虑Any内部设计一个模版容器holder类,可以保存各种类型数据

而因为在Any类中无法定义这个holder对象或指针,因为Any也不知道这个类要保存什么类型的数据,因此无法传递类型参数

所以。定义一个基类placeholder,让holder继承于placeholder,而Any类保存父类指针即可

当需要保存数据时,则new一个带有模版参数的子类holder对象去保存数据。然后让Any类中的父类指针,指向这个子类对象就可以了

Any类的具体实现:

class Any
{
private:
    class holder
    {
    public:
        virtual ~holder()
        {}
        // 数据类型
        virtual const std::type_info& type() = 0;
        // 克隆出新的对象
        virtual holder* clone() = 0;
    };
    template
    class placeholder: public holder
    {
    public:
        placeholder(const T& val): _val(val){}
        // 获取子类对象保存的数据类型
        virtual const std::type_info& type()
        {
            return typeid(T);
        }
        // 针对出当前的对象自身,克隆出一个新的对象
        virtual holder* clone()
        {
            return new placeholder(_val);
        }
    public:
        T _val;
    };
    holder* _content;
    public:
        Any():_content(NULL){}
        template
        Any(const T& val):_content(new placeholder(val)){}
        Any(const Any& other):_content(other._content ? other._content->clone() : NULL) {}
        ~Any(){ delete _content;}

        Any &swap(Any& other)
        {
            std::swap(_content, other._content);
            return *this;
        }   

        template
        //返回子类对象保存的数据的指针
        T* get() 
        {
            // 想要获取的数据类型,必须和保存的数据类型一致
            //if(typeid(T) != _content->type()) return NULL;
            assert(typeid(T) == _content->type());
            return &((placeholder*)_content)->_val;
        }
        //赋值运算符重载函数
        template
        Any& operator=(const T& val)
        {
            //为val构造一个临时的通用容器,然后与当前容器自身进行指针交换
            //临时对象释放的时候,原先保存的数据也就被释放了
            Any(val).swap(*this);
            return *this;
        }
        Any& operator=(const Any& other)
        {
            Any(other).swap(*this);
            return *this;
        }
};

2.Server服务器模块实现:

2.1 缓冲区Buffer类的实现

Buffer类用于实现用户态缓冲区,提供数据缓冲,取出的功能

首先Buffer这个类有三个私有成员变量,分别是_buffer,_reader_idx,_writer_idx。这三个私有成员分别代表缓冲区,读偏移和写偏移。需要解释的是读偏移是指读到的位置,写偏移是指已经写进缓冲区中的位置

私有成员变量的代码如下:

// 使用vector进行内存空间管理
std::vector _buffer;
uint64_t _reader_idx; // 读偏移
uint64_t _writer_idx; // 写偏移

首先我们需要有个函数能返回这个_buffer的起始地址

char* Begin()
{
    return &*_buffer.begin();
}

_buffer.begin()返回的是迭代器的初始位置,我们需要对迭代器进行解引用然后取地址才能获得char*的地址。

然后我们也需要获得当前写入起始地址和当前读取起始地址

// 获取当前写入起始地址
char *WritePosition()
{
    //_buffer的空间起始地址,加上写偏移量
    return Begin() + _writer_idx;
}
// 获取当前读取起始地址
char *ReadPosition()
{
    return Begin() + _reader_idx;
}

获得缓冲区末尾和起始空间的大小

// 获取缓冲区末尾空闲空间大小--写偏移之后的空闲空间
uint64_t TailIdleSize()
{
    return _buffer.size() - _writer_idx;
}
// 获取缓冲区起始空闲空间大小--读偏移之前的空闲空间
uint64_t HeadIdleSize()
{
    return _reader_idx;
}

可读数据的大小即是写偏移减去读偏移

// 获取可读数据大小
uint64_t ReadAbleSize()
{
    return _writer_idx - _reader_idx;
}

读完数据或者写完数据都需要将读偏移和写偏移向后移动

// 将读偏移向后移动
void MoveReadOffset(uint64_t len)
{
    if (len == 0)
        return;
    // 向后移动的大小,必须小于可读数据的大小
    assert(len <= ReadAbleSize());
    _reader_idx += len;
}
// 将写偏移向后移动
void MoveWriteOffset(uint64_t len)
{
    // 向后移动的大小,必须小于当前后边的空闲空间大小
    assert(len <= TailIdleSize());
    _writer_idx += len;
}

然后,我们也需要确保可写的空间足够,如果空间够就直接移动数据。

问题来了,如何移动数据?

很简单,即把在读偏移和写偏移之间的数据都移动到_buffer的起始位置,这里需要用到std::copy函数,下面是我们需要用到的copy函数的定义

inline char *std::copy(char *__first, char *__last, char *__result)

第一个和第二个参数分别表示数据的起始位置和结束位置,第三个参数则表示拷贝到的位置

如果即使移动数据,空间还是不够,我们就需要扩容,使用到的函数是resize 

下面是详细代码

// 确保可写空间足够(足够就移动数据,否则就扩容)
void EnsureWriteSpace(uint64_t len)
{
    // 如果末尾空闲空间大小足够,直接返回
    if (TailIdleSize() >= len)
    {
        return;
    }
    // 末尾空闲空间不够,则判断加上起始位置的空闲空间大小是否足够,够了就将数据移动到起始位置
    if (len <= TailIdleSize() + HeadIdleSize())
    {
        // 将数据移动到起始位置
        uint64_t rsz = ReadAbleSize();
        // 把可读数据拷贝到起始位置
        std::copy(ReadPosition(), ReadPosition() + rsz, Begin());
        _reader_idx = 0;   // 将读偏移归0
        _writer_idx = rsz; // 将写位置置为可读数据大小,因为当前的可读数据大小就是写偏移量
    }
    else
    {
        // 总体空间不够,则需要扩容,不移动数据,直接给写偏移之后扩容足够空间即可
        DBG_LOG("RESIZE %ld", _writer_idx + len);
        _buffer.resize(_writer_idx + len);
    }
}

要写入数据进去,需要两步,即保证有足够空间和拷贝数据进入_buffer

// 写入数据
void Write(const void *data, uint64_t len)
{
    // 1.保证有足够空间
    if (len == 0)
        return;
    EnsureWriteSpace(len);
    const char *d = (const char *)data;
    // 2.拷贝数据进去
    std::copy(d, d + len, WritePosition());
}

接下来的几个函数都是基于Write函数的拓展集成,包括写入string、Buffer类型的数据

void WriteAndPush(const void *data, uint64_t len)
{
    Write(data, len);
    MoveWriteOffset(len);
}
void WriteString(const std::string &data)
{
    return Write(data.c_str(), data.size());
}
void WriteStringAndPush(const std::string &data)
{
    WriteString(data);
    // std::cout << WritePosition() << std::endl;
    MoveWriteOffset(data.size());
    // std::cout << ReadAbleSize() << std::endl;
}
void WriteBuffer(Buffer &data)
{
    return Write(data.ReadPosition(), data.ReadAbleSize());
}
void WriteBufferAndPush(Buffer &data)
{
    WriteBuffer(data);
    MoveWriteOffset(data.ReadAbleSize());
}

接下来的是Read函数,包含两个参数分别是把数据读到那个位置,和读取的长度

// 读取数据
void Read(void *buf, uint64_t len)
{
    assert(len <= ReadAbleSize());
    // std::cout << ReadPosition() << std::endl;
    std::copy(ReadPosition(), ReadPosition() + len, (char *)buf);
}

同样,下面的函数也是基于Read函数的拓展集成

void ReadAndPop(void *buf, uint64_t len)
{
    Read(buf, len);
    MoveReadOffset(len);
}
// 把缓冲区中的数据当做string读取
std::string ReadAsString(uint64_t len)
{
    assert(len <= ReadAbleSize());
    std::string str;
    str.resize(len);
    Read(&str[0], len);
    return str;
}
std::string ReadAsStringAndPop(uint64_t len)
{
    assert(len <= ReadAbleSize());
    std::string str = ReadAsString(len);
    MoveReadOffset(len);
    return str;
}

在读取的时候,例如读取Http协议,就需要一行一行的读,这时候就需要读取到每一行的 ,所以就有了FindCRLF函数使用到的是cstring中的memchr函数

void *memchr(const void *buf, int c, size_t count);

buf:指向待搜索内存块的指针

c:要查找的字符

count:要搜索的字节数

由于我们需要的是char* 的返回值,所以需要强制转换一下

char *FindCRLF()
{
    char *res = (char *)memchr(ReadPosition(), '
', ReadAbleSize());
    return res;
}
/*通常获取一行数据,这种情况针对是HTTP协议*/
std::string GetLine()
{
    char *pos = FindCRLF();
    if (pos == NULL)
    {
        return "";
    }
    // +1 是为了把换行字符也取出来
    return ReadAsString(pos - ReadPosition() + 1);
}

下面是这个Buffer类的整体代码

#define BUFFER_DEFAULT_SIZE 1024
class Buffer
{
private:
    // 使用vector进行内存空间管理
    std::vector _buffer;
    uint64_t _reader_idx; // 读偏移
    uint64_t _writer_idx; // 写偏移
public:
    Buffer() : _reader_idx(0), _writer_idx(0), _buffer(BUFFER_DEFAULT_SIZE) {}
    char *Begin() { return &*_buffer.begin(); }
    void TestPrint()
    {
        for (auto e : _buffer)
        {
            std::cout << e;
        }
    }
    // 获取当前写入起始地址
    char *WritePosition()
    {
        //_buffer的空间起始地址,加上写偏移量
        return Begin() + _writer_idx;
    }
    // 获取当前读取起始地址
    char *ReadPosition()
    {
        return Begin() + _reader_idx;
    }
    // 获取缓冲区末尾空闲空间大小--写偏移之后的空闲空间
    uint64_t TailIdleSize()
    {
        return _buffer.size() - _writer_idx;
    }
    // 获取缓冲区起始空闲空间大小--读偏移之前的空闲空间
    uint64_t HeadIdleSize()
    {
        return _reader_idx;
    }
    // 获取可读数据大小
    uint64_t ReadAbleSize()
    {
        return _writer_idx - _reader_idx;
    }
    // 将读偏移向后移动
    void MoveReadOffset(uint64_t len)
    {
        if (len == 0)
            return;
        // 向后移动的大小,必须小于可读数据的大小
        assert(len <= ReadAbleSize());
        _reader_idx += len;
    }
    // 将写偏移向后移动
    void MoveWriteOffset(uint64_t len)
    {
        // 向后移动的大小,必须小于当前后边的空闲空间大小
        assert(len <= TailIdleSize());
        _writer_idx += len;
    }
    // 确保可写空间足够(足够就移动数据,否则就扩容)
    void EnsureWriteSpace(uint64_t len)
    {
        // 如果末尾空闲空间大小足够,直接返回
        if (TailIdleSize() >= len)
        {
            return;
        }
        // 末尾空闲空间不够,则判断加上起始位置的空闲空间大小是否足够,够了就将数据移动到起始位置
        if (len <= TailIdleSize() + HeadIdleSize())
        {
            // 将数据移动到起始位置
            uint64_t rsz = ReadAbleSize();
            // 把可读数据拷贝到起始位置
            std::copy(ReadPosition(), ReadPosition() + rsz, Begin());
            _reader_idx = 0;   // 将读偏移归0
            _writer_idx = rsz; // 将写位置置为可读数据大小,因为当前的可读数据大小就是写偏移量
        }
        else
        {
            // 总体空间不够,则需要扩容,不移动数据,直接给写偏移之后扩容足够空间即可
            DBG_LOG("RESIZE %ld", _writer_idx + len);
            _buffer.resize(_writer_idx + len);
        }
    }
    // 写入数据
    void Write(const void *data, uint64_t len)
    {
        // 1.保证有足够空间
        if (len == 0)
            return;
        EnsureWriteSpace(len);
        const char *d = (const char *)data;
        // 2.拷贝数据进去
        std::copy(d, d + len, WritePosition());
    }
    void WriteAndPush(const void *data, uint64_t len)
    {
        Write(data, len);
        MoveWriteOffset(len);
    }
    void WriteString(const std::string &data)
    {
        return Write(data.c_str(), data.size());
    }
    void WriteStringAndPush(const std::string &data)
    {
        WriteString(data);
        // std::cout << WritePosition() << std::endl;
        MoveWriteOffset(data.size());
        // std::cout << ReadAbleSize() << std::endl;
    }
    void WriteBuffer(Buffer &data)
    {
        return Write(data.ReadPosition(), data.ReadAbleSize());
    }
    void WriteBufferAndPush(Buffer &data)
    {
        WriteBuffer(data);
        MoveWriteOffset(data.ReadAbleSize());
    }
    // 读取数据
    void Read(void *buf, uint64_t len)
    {
        assert(len <= ReadAbleSize());
        // std::cout << ReadPosition() << std::endl;
        std::copy(ReadPosition(), ReadPosition() + len, (char *)buf);
    }
    void ReadAndPop(void *buf, uint64_t len)
    {
        Read(buf, len);
        MoveReadOffset(len);
    }
    // 把缓冲区中的数据当做string读取
    std::string ReadAsString(uint64_t len)
    {
        assert(len <= ReadAbleSize());
        std::string str;
        str.resize(len);
        Read(&str[0], len);
        return str;
    }
    std::string ReadAsStringAndPop(uint64_t len)
    {
        assert(len <= ReadAbleSize());
        std::string str = ReadAsString(len);
        MoveReadOffset(len);
        return str;
    }
    char *FindCRLF()
    {
        char *res = (char *)memchr(ReadPosition(), '
', ReadAbleSize());
        return res;
    }
    /*通常获取一行数据,这种情况针对是HTTP协议*/
    std::string GetLine()
    {
        char *pos = FindCRLF();
        if (pos == NULL)
        {
            return "";
        }
        // +1 是为了把换行字符也取出来
        return ReadAsString(pos - ReadPosition() + 1);
    }
    std::string GetLineAndPop()
    {
        std::string str = GetLine();
        MoveReadOffset(str.size());
        return str;
    }
    // 清空缓冲区
    void Clear()
    {
        // 只需要将偏移量归零
        _reader_idx = 0;
        _writer_idx = 0;
    }
};

2.2 日志宏的实现

作为一名合格的程序员,日志宏是不可或缺的,我们将在这里写一个简单的日志宏

首先我们的日志应该包含日志等级,方便我们调试,这里我们给出了三种等级的日志等级

INF,DBG,ERR,分别设置为0 1 2 然后1以上的日志等级将打印出具体的日志信息

我们也想日志能打印出现在的时间,需要使用到ctime这个头文件

里面有一个tm结构体,定义如下:

struct tm

{

  int tm_sec;        /* Seconds. [0-60] (1 leap second) */

  int tm_min;        /* Minutes. [0-59] */

  int tm_hour;       /* Hours.   [0-23] */

  int tm_mday;       /* Day.     [1-31] */

  int tm_mon;        /* Month.   [0-11] */

  int tm_year;       /* Year  - 1900.  */

  int tm_wday;       /* Day of week.   [0-6] */

  int tm_yday;       /* Days in year.[0-365] */

  int tm_isdst;         /* DST.     [-1/0/1]*/

}

我们可以使用localtime这个函数来把时间戳的信息转移到这个tm结构体当中然后读取,在以特定格式来打印出时间

这里有几个小的知识点

1.如何打印对应的行号和文件呢?

可以使用__FILE__ 和 __LINE__ 两个宏定义来打印对应的文件名和行号

2.宏定义中的可变参数该如何表示呢?

使用##__VA_ARGS__来表示

下面就是详细的代码

#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG
#define LOG(level, format, ...)                                                                                       
    do                                                                                                                
    {                                                                                                                 
        if (level < LOG_LEVEL)                                                                                        
            break;                                                                                                    
        time_t t = time(NULL);                                                                                        
        struct tm *ltm = localtime(&t);                                                                               
        char tmp[32] = {0};                                                                                           
        strftime(tmp, 31, "%H:%M:%S", ltm);                                                                           
        fprintf(stdout, "[%p %s %s:%d]" format "
", (void *)pthread_self(), tmp, __FILE__, __LINE__, ##__VA_ARGS__); 
    } while (0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

 2.3 套接字模块的实现

套接字是服务器编程中不可缺少的,我们使用一个结构体Socket来统一管理

1.首先封装创建套接字的函数Create()

bool Create()
{
    _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (_sockfd < 0)
    {
        ERR_LOG("CREATE SOCKET FAILED!!");
        return false;
    }
    return true;
}

这里解释一下socket的三个参数,第一个参数表示使用IPV4协议,第二个参数表示使用面向连接的套接字,第三个参数表示选择TCP协议,也可以把第三个参数直接写成0,系统会自动选择协议


2.封装绑定地址信息的函数Bind(),传入两个参数即可,即ipport

bool Bind(const std::string &ip, uint16_t port)
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
    if (ret < 0)
    {
        ERR_LOG("BIND ADDRESS FAILED!");
        return false;
    }
    return true;
}

其中sockaddr_in 这个结构体包含三个成员,分别是sin_family,sin_port,sin_addr

sin_family:sin_family设置为AF_INET表示使用IPV4协议

sin_port:传入sin_port参数的时候,需要将主机字节序转化为网络字节序。因为网络协议中统一使用大端模式作为标准字节序

sin_addr:需要使用inet_addr把点分十进制字符串转化为网络字节序的32位无符号整数的函数

接下来解释bind函数,第一个参数传入创建出来的套接字, 第二个参数是addr强转的地址,第三个参数表示表示addr的大小


3.绑定好地址信息后,就要开始监听了,这里我们使用Listen函数来包装,Listen传入一个缺省参数backlog

bool Listen(int backlog = MAX_LISTEN)
{
    int ret = listen(_sockfd, backlog);
    if (ret < 0)
    {
        ERR_LOG("SOCKET LISTEN FAILED!");
        return false;
    }
    return true;
}

这里解释一下listen函数的两个参数,第一个参数表示传入的套接字,第二个参数表示定义了 已完成连接队列(ESTABLISHED 状态)的最大长度。


4.接下来就是向服务器发起连接Connect(),传入两个参数,ipport

bool Connect(const std::string &ip, uint16_t port)
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
    if (ret < 0)
    {
        ERR_LOG("CONNECT ADDRESS FAILED!");
        return false;
    }
    return true;
}

这里connect的参数就不多说

ssize_t Recv(void *buf, size_t len, int flag = 0) // flag代表设置一些阻塞相关的操作
{
    ssize_t ret = recv(_sockfd, buf, len, flag);
    if (ret <= 0)
    {
        if (errno == EAGAIN || errno == EINTR)
        {
            return 0;
        }
        ERR_LOG("SOCKET RECV FAILED!");
        return -1;
    }
    return ret;
}


5. 获取新链接使用Accept()函数来封装

int Accept()
{
    int newfd = accept(_sockfd, NULL, NULL);
    if (newfd < 0)
    {
        ERR_LOG("SOCKET ACCEPT FAILED!");
        return -1;
    }
    return newfd;
}

这里我们选择将accept的第二个和第三个参数都设置为NULL表示我们不关心客户端地址信息。所以只传入了第一个参数套接字,用于从已完成连接队列中提取连接


6.使用Recv()函数来接受数据

ssize_t Recv(void *buf, size_t len, int flag = 0) // flag代表设置一些阻塞相关的操作
{
    ssize_t ret = recv(_sockfd, buf, len, flag);
    if (ret <= 0)
    {
        if (errno == EAGAIN || errno == EINTR)
        {
            return 0;
        }
        ERR_LOG("SOCKET RECV FAILED!");
        return -1;
    }
    return ret;
}

recv有四个参数,简单解释一下,第一个参数传入套接字,第二个参数表示接受的数据放到的位置,第三个参数表示单次接受的数据量,flags控制接收行为的标志位。传入0表示阻塞读取。


7.再次封装一个函数NonBlockRecv表示非阻塞接受

ssize_t NonBlockRecv(void *buf, size_t len)
{
    if (len == 0)
        return 0;
    return Recv(buf, len, MSG_DONTWAIT);
}

MSG_DONTWAIT表示不要等待的接受


8.发送数据封装一个Send()函数,封装三个参数分别是发送数据的位置,len表示发送数据的大小,第三个参数为flag和Recv参数一样

ssize_t Send(const void *buf, size_t len, int flag = 0)
{
    // ssize_t send(int sockfd, void* data, size_t len, int flag);
    ssize_t ret = send(_sockfd, buf, len, flag);
    if (ret < 0)
    {
        if (errno == EAGAIN || errno == EINTR)
        {
            return 0;
        }
        ERR_LOG("SOCKET SEND FAILED!");
        return -1;
    }
    return ret;
}

send的参数就不多解释了

9.封装一个NonBlockSend函数表示非阻塞发送

ssize_t NonBlockSend(void *buf, size_t len)
{
    if (len == 0)
        return 0;
    return Send(buf, len, MSG_DONTWAIT);
}

这里同样使用MSG_DONTWAIT来表示非阻塞


10.套接字使用完毕后需要关闭,象文件描述符一样

void Close()
{
    if (_sockfd != -1)
    {
        close(_sockfd);
        _sockfd = -1;
    }
}

11.创建一个服务器连接,在这里分为5个步骤,创建套接字,设置非阻塞,绑定地址,开始监听,启动地址重用

bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
{
    // 创建套接字
    if (Create() == false)
        return false;
    // 设置非阻塞
    if (block_flag)
        NonBlock();
    // 绑定地址
    if (Bind(ip, port) == false)
        return false;
    // 开始监听
    if (Listen() == false)
        return false;
    // 启动地址重用
    ReuseAddress();
    return true;
}


12.创建一个客户端连接,分为两个步骤:Create和Connect如下代码所示

bool CreateClient(uint16_t port, const std::string &ip)
{
    // 1.创建套接字, 2.指向连接服务器
    if (Create() == false)
        return false;
    if (Connect(ip, port) == false)
        return false;
    return true;
}

13.开启地址端口重用

这里解释一下什么事启动地址重用,在一个TCP连接关闭时,操作系统会保留该链接的端口一段时间,这个状态被称为TIME_WAIT。在TIME_WAIT状态下,端口不能被立即重新使用,这是为了确保所有相关的TCP报文段(如延迟的ACK或FIN)都已经被处理完毕,从而避免新链接与旧链接的数据混淆。设置地址重用,可以在短时间内连续启用同一个端口号

void ReuseAddress()
{
    int val = 1;
    setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
    val = 1;
    setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
}

下面是setsockopt函数的原型

int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len)

socket参数指定被操作的目标socket,level参数指定要操作那个协议的选项,比如IPV4,IPV6,TCP等。option_name参数指定选项的名字,option_valueoption_len参数分别是被操作选项的值和长度

这里我们把val设置为1,val是一个整数,通常设置为1或0。1表示启动指定的套接字选项,0表示禁用指定的套接字选项


14.设置套接字阻塞属性---设置为非阻塞,这里我们用到的函数是fcntl函数,代码后面将会给出详细的解释

void NonBlock()
{
    int flag = fcntl(_sockfd, F_GETFL, 0);
    fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
}

fcntl函数,正如其名字一样,提供了对文件描述符的各种控制操作,另外要一个常见的控制文件描述符的属性和行为的系统调用时ioctl,但是fcntl是由POSIX 规定的首选方法。

我们先在flag中获取fd的标志,然后再将这个函数的第二个参数设置为F_SETFL表示设置fd的标志,第三个参数就写成flag | O_NONBLOCK表示再加上非阻塞属性


最后是完整的socket代码

#define MAX_LISTEN 1024

class Socket
{
private:
    int _sockfd;

public:
    Socket() : _sockfd(-1) {}
    Socket(int fd) : _sockfd(fd) {}
    ~Socket() { Close(); }
    int Fd() { return _sockfd; }
    // 创建套接字
    bool Create()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (_sockfd < 0)
        {
            ERR_LOG("CREATE SOCKET FAILED!!");
            return false;
        }
        return true;
    }
    // 绑定地址信息
    bool Bind(const std::string &ip, uint16_t port)
    {
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
        if (ret < 0)
        {
            ERR_LOG("BIND ADDRESS FAILED!");
            return false;
        }
        return true;
    }
    // 开始监听
    bool Listen(int backlog = MAX_LISTEN)
    {
        int ret = listen(_sockfd, backlog);
        if (ret < 0)
        {
            ERR_LOG("SOCKET LISTEN FAILED!");
            return false;
        }
        return true;
    }
    // 向服务器发起连接
    bool Connect(const std::string &ip, uint16_t port)
    {
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        socklen_t len = sizeof(struct sockaddr_in);
        int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
        if (ret < 0)
        {
            ERR_LOG("CONNECT ADDRESS FAILED!");
            return false;
        }
        return true;
    }
    // 获取新链接
    int Accept()
    {
        int newfd = accept(_sockfd, NULL, NULL);
        if (newfd < 0)
        {
            ERR_LOG("SOCKET ACCEPT FAILED!");
            return -1;
        }
        return newfd;
    }
    // 接收数据
    ssize_t Recv(void *buf, size_t len, int flag = 0) // flag代表设置一些阻塞相关的操作
    {
        ssize_t ret = recv(_sockfd, buf, len, flag);
        if (ret <= 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0;
            }
            ERR_LOG("SOCKET RECV FAILED!");
            return -1;
        }
        return ret;
    }
    ssize_t NonBlockRecv(void *buf, size_t len)
    {
        if (len == 0)
            return 0;
        return Recv(buf, len, MSG_DONTWAIT);
    }
    // 发送数据
    ssize_t Send(const void *buf, size_t len, int flag = 0)
    {
        // ssize_t send(int sockfd, void* data, size_t len, int flag);
        ssize_t ret = send(_sockfd, buf, len, flag);
        if (ret < 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0;
            }
            ERR_LOG("SOCKET SEND FAILED!");
            return -1;
        }
        return ret;
    }
    ssize_t NonBlockSend(void *buf, size_t len)
    {
        if (len == 0)
            return 0;
        return Send(buf, len, MSG_DONTWAIT);
    }

    // 关闭套接字
    void Close()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }
    // 创建一个服务端连接
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
    {
        // 创建套接字
        if (Create() == false)
            return false;
        // 设置非阻塞
        if (block_flag)
            NonBlock();
        // 绑定地址
        if (Bind(ip, port) == false)
            return false;
        // 开始监听
        if (Listen() == false)
            return false;
        // 启动地址重用
        ReuseAddress();
        return true;
    }
    // 创建一个客户端连接
    bool CreateClient(uint16_t port, const std::string &ip)
    {
        // 1.创建套接字, 2.指向连接服务器
        if (Create() == false)
            return false;
        if (Connect(ip, port) == false)
            return false;
        return true;
    }
    // 设置套接字选项--开启地址端口重用
    void ReuseAddress()
    {
        int val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
        val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
    }
    // 设置套接字阻塞属性--设置为非阻塞
    void NonBlock()
    {
        int flag = fcntl(_sockfd, F_GETFL, 0);
        fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
    }
};

2.4 事件管理Channel类的实现

Channel模块是对一个描述符需要进行IO事件管理的模块,实现对描述符可读,可写,错误事件的管理操作,以及Poller模块对描述符进行IO事件监控就绪后,根据不同的事件,回调不同的处理函数功能。

在Channel类的私有成员中,主要包含文件描述符_fd,当前需要监控的事件_events和当前触发的事件_revents还有各种事件被触发时的回调函数

int _fd;
EventLoop *_loop;
uint32_t _events;  // 当前需要监控的事件
uint32_t _revents; // 当前触发的事件
using EventCallback = std::function;
EventCallback _read_callback;  // 可读事件被触发的回调函数
EventCallback _write_callback; // 可写事件被触发的回调函数
EventCallback _error_callback; // 错误事件被触发的回调函数
EventCallback _close_callback; // 连接断开事件被触发的回调函数
EventCallback _event_callback; // 任意事件被触发的回调函数

 接下来是Channel类中所包含的成员函数

首先是设置各种事件的回调函数

void SetReadCallback(const EventCallback &cb)
{
    _read_callback = cb;
}
void SetWriteCallback(const EventCallback &cb)
{
    _write_callback = cb;
}
void SetErrorCallback(const EventCallback &cb)
{
    _error_callback = cb;
}
void SetCloseCallback(const EventCallback &cb)
{
    _close_callback = cb;
}
void SetEventCallback(const EventCallback &cb)
{
    _event_callback = cb;
}

接下来的两个函数用来检测当前是否可读和可写

bool ReadAble() // 当前是否可读
{
    return (_events & EPOLLIN);
}
bool WriteAble() // 当前是否可写
{
    return (_events & EPOLLOUT);
}

这四个函数用来启动事件监控和关闭事件监控

void EnableRead() // 启动读事件监控
{
    _events |= EPOLLIN; /*后边会添加到EventLoop的事件监控中*/
    Update();
}
void EnableWrite() // 启动写事件监控
{
    _events |= EPOLLOUT;
    Update();
}
void DisableRead() // 关闭读事件监控
{
    _events &= ~EPOLLIN;
    Update();
}
void DisableWrite() // 关闭写事件监控
{
    _events &= ~EPOLLOUT;
    Update();
}
void DisableAll() // 关闭所有事件监控
{
    _events = 0;
    Update();
}

还需要对监控进行移除和更新的函数,这两个函数需要在类外实现,因为EventLoop类在Channel::Remove()和Channel::Update()中被使用时,编译器需要看到他的完整定义

void Channel::Remove() // 移除监控
{
    return _loop->RemoveEvent(this);
}
void Channel::Update()
{
    return _loop->UpdateEvent(this);
}

接下来是事件处理函数,就调用这个函数,自己触发了什么事件如何自己决定。这里解释一些常用的epoll事件类型。

  • EPOLLIN:数据可读
  • EPOLLRDHUP:TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入
  • EPOLLPRI:高优先级数据可读,比如TCP带外数据
  • EPOLLOUT:数据可写
  • EPOLLERR:错误
  • EPOLLHUP:挂起,比如管道的写端被关闭后,读端描述符上将被收到EPOLLHUP事件。
void HandleEvent() // 事件处理,一旦连接触发了事件,就调用这个函数,自己触发了什么事件如何自己决定
{
    if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
    {
        /*不管任何事件,都调用的回调函数*/
        if (_read_callback)
            _read_callback();
    }
    /*有可能会释放连接的操作事件,一次只处理一个*/
    if (_revents & EPOLLOUT)
    {
        /*不管任何事件,都调用的回调函数*/

        if (_write_callback)
            _write_callback();
    }
    else if (_revents & EPOLLERR)
    {

        if (_error_callback)
            _error_callback();
    }
    else if (_revents & EPOLLHUP)
    {

        if (_close_callback)
            _close_callback();
    }
    if (_event_callback)
        _event_callback();
}

下面是整个Channel类的实现

class Channel
{
private:
    int _fd;
    EventLoop *_loop;
    uint32_t _events;  // 当前需要监控的事件
    uint32_t _revents; // 当前触发的事件
    using EventCallback = std::function;
    EventCallback _read_callback;  // 可读事件被触发的回调函数
    EventCallback _write_callback; // 可写事件被触发的回调函数
    EventCallback _error_callback; // 错误事件被触发的回调函数
    EventCallback _close_callback; // 连接断开事件被触发的回调函数
    EventCallback _event_callback; // 任意事件被触发的回调函数
public:
    Channel(EventLoop *loop, int fd) : _fd(fd), _events(0), _revents(0), _loop(loop) {}
    int Fd() { return _fd; }
    void SetREvents(uint32_t events) { _revents = events; }
    uint32_t Events() { return _events; }
    void SetReadCallback(const EventCallback &cb)
    {
        _read_callback = cb;
    }
    void SetWriteCallback(const EventCallback &cb)
    {
        _write_callback = cb;
    }
    void SetErrorCallback(const EventCallback &cb)
    {
        _error_callback = cb;
    }
    void SetCloseCallback(const EventCallback &cb)
    {
        _close_callback = cb;
    }
    void SetEventCallback(const EventCallback &cb)
    {
        _event_callback = cb;
    }
    bool ReadAble() // 当前是否可读
    {
        return (_events & EPOLLIN);
    }
    bool WriteAble() // 当前是否可写
    {
        return (_events & EPOLLOUT);
    }
    void EnableRead() // 启动读事件监控
    {
        _events |= EPOLLIN; /*后边会添加到EventLoop的事件监控中*/
        Update();
    }
    void EnableWrite() // 启动写事件监控
    {
        _events |= EPOLLOUT;
        Update();
    }
    void DisableRead() // 关闭读事件监控
    {
        _events &= ~EPOLLIN;
        Update();
    }
    void DisableWrite() // 关闭写事件监控
    {
        _events &= ~EPOLLOUT;
        Update();
    }
    void DisableAll() // 关闭所有事件监控
    {
        _events = 0;
        Update();
    }
    void Remove(); // 移除监控
    void Update();
    void HandleEvent() // 事件处理,一旦连接触发了事件,就调用这个函数,自己触发了什么事件如何自己决定
    {
        if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
        {
            /*不管任何事件,都调用的回调函数*/
            if (_read_callback)
                _read_callback();
        }
        /*有可能会释放连接的操作事件,一次只处理一个*/
        if (_revents & EPOLLOUT)
        {
            /*不管任何事件,都调用的回调函数*/

            if (_write_callback)
                _write_callback();
        }
        else if (_revents & EPOLLERR)
        {

            if (_error_callback)
                _error_callback();
        }
        else if (_revents & EPOLLHUP)
        {

            if (_close_callback)
                _close_callback();
        }
        if (_event_callback)
            _event_callback();
    }
};

2.5 描述符事件监控Poller类实现

Poller模块是对epoll进行封装的一个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接功能

首先来说明一下这个类的私有成员变量,首先是epoll文件描述符_epfd,然后是_evt,这是用来描述需要监控的文件描述符,最后是_channels,这是用来通过文件描述符找到其对应的Channel模块。

int _epfd;
struct epoll_event _evs[MAX_EPOLLEVENTS];
std::unordered_map _channels;

接下来注意介绍其中的成员函数

首先是对epoll的直接操作函数Update,Update中传入两个参数,分别是channel指针和需要执行的操作op,首先我们需要通过channel指针获得fd,然后创建一个epoll_event类型的结构体并对其进行初始化,这里介绍一下epoll_event

struct epoll_event {
    uint32_t events;  // 事件类型(位掩码)
    epoll_data_t data; // 用户数据(联合体)
};
 
typedef union epoll_data {
    void *ptr;      // 用户自定义数据指针
    int fd;         // 文件描述符
    uint32_t u32;   // 32位无符号整数
    uint64_t u64;   // 64位无符号整数
} epoll_data_t;

在epoll_data这个联合体当中,最常用的数据是fd

然后使用epoll_ctl对epoll实例添加、修改或删除监控的文件描述符及其关注的事件

int epoll_ctl(int __epfd, int __op, int __fd, epoll_event *) throw()

参数解释:

  • __epfd:由epoll_create或epoll_create1创建的epoll实例的文件描述符
  • __op:一般使用以下三种操作类型:EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL
  • __fd:需要被监控或操作的普通文件描述符
  • epoll_event类型参数:定义需要监控的事件类型和关联的用户数据

Update的完整实现:

void Update(Channel *channel, int op)
{
    // int epoll_ctl(int epfd, int op, int fd, struct epoll_event* ev);
    int fd = channel->Fd();
    struct epoll_event ev;
    ev.data.fd = fd;
    ev.events = channel->Events();
    int ret = epoll_ctl(_epfd, op, fd, &ev);
    if (ret < 0)
    {
        ERR_LOG("EPOLLCTL FAILED!");
        // abort(); //退出程序
    }
    return;
}

然后就是判断一个Channel是否已经添加了事件监控

bool HasChannel(Channel *channel)
{
    auto it = _channels.find(channel->Fd());
    if (it == _channels.end())
    {
        return false;
    }
    return true;
}

添加或修改监控事件,如果Channel能找到,那么就修改否则就添加

void UpdateEvent(Channel *channel) 
{
    bool ret = HasChannel(channel);
    if (ret == false)
    {
        // 不存在则添加
        _channels.insert(std::make_pair(channel->Fd(), channel));
        return Update(channel, EPOLL_CTL_ADD);
    }
    return Update(channel, EPOLL_CTL_MOD);
}

移除监控

void RemoveEvent(Channel *channel)
{
    auto it = _channels.find(channel->Fd());
    {
        if (it != _channels.end())
        {
            _channels.erase(it);
        }
        Update(channel, EPOLL_CTL_DEL);
    }
}

开始监控,并且返回活跃连接

介绍一下epoll_wait,用于等待epoll实例监控的文件描述符上发生的事件,返回值是就绪的文件描述符的数量。并且这个函数会把就绪事件填充到第二个参数指向的数组中

void Poll(std::vector *active)
{
    int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1); //-1代表阻塞监控
    if (nfds < 0)
    {
        if (errno == EINTR)
        {
            return;
        }
        ERR_LOG("EPOLL WAIT ERROR:%s
", strerror(errno));
        abort();
    }
    for (int i = 0; i < nfds; i++)
    {
        auto it = _channels.find(_evs[i].data.fd);
        assert(it != _channels.end());
        it->second->SetREvents(_evs[i].events);
        active->push_back(it->second);
    }
    return;
}

下面是完整代码

#define MAX_EPOLLEVENTS 1024
class Poller
{
private:
    int _epfd;
    struct epoll_event _evs[MAX_EPOLLEVENTS];
    std::unordered_map _channels;

private:
    // 对epoll的直接操作
    void Update(Channel *channel, int op)
    {
        // int epoll_ctl(int epfd, int op, int fd, struct epoll_event* ev);
        int fd = channel->Fd();
        struct epoll_event ev;
        ev.data.fd = fd;
        ev.events = channel->Events();
        int ret = epoll_ctl(_epfd, op, fd, &ev);
        if (ret < 0)
        {
            ERR_LOG("EPOLLCTL FAILED!");
            // abort(); //退出程序
        }
        return;
    }
    // 判断一个Channel是否已经添加了事件监控
    bool HasChannel(Channel *channel)
    {
        auto it = _channels.find(channel->Fd());
        if (it == _channels.end())
        {
            return false;
        }
        return true;
    }

public:
    Poller()
    {
        _epfd = epoll_create(MAX_EPOLLEVENTS);
        if (_epfd < 0)
        {
            ERR_LOG("EPOLL CREATE FAILED!");
            abort();
        }
    }
    void UpdateEvent(Channel *channel) // 添加或修改监控事件
    {
        bool ret = HasChannel(channel);
        if (ret == false)
        {
            // 不存在则添加
            _channels.insert(std::make_pair(channel->Fd(), channel));
            return Update(channel, EPOLL_CTL_ADD);
        }
        return Update(channel, EPOLL_CTL_MOD);
    }
    void RemoveEvent(Channel *channel) // 移除监控
    {
        auto it = _channels.find(channel->Fd());
        {
            if (it != _channels.end())
            {
                _channels.erase(it);
            }
            Update(channel, EPOLL_CTL_DEL);
        }
    }
    void Poll(std::vector *active) // 开始监控,返回活跃连接
    {
        int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1); //-1代表阻塞监控
        if (nfds < 0)
        {
            if (errno == EINTR)
            {
                return;
            }
            ERR_LOG("EPOLL WAIT ERROR:%s
", strerror(errno));
            abort();
        }
        for (int i = 0; i < nfds; i++)
        {
            auto it = _channels.find(_evs[i].data.fd);
            assert(it != _channels.end());
            it->second->SetREvents(_evs[i].events);
            active->push_back(it->second);
        }
        return;
    }
};

2.6 定时任务管理TimerWheel类实现

前面的准备知识中我们已经基本实现了时间轮,唯一需要解释的就是对于定时器文件描述符的使用

定时器文件描述符的创建:timerfd_create

第一个参数一般传入的是CLOCK_MONOTONIC,表示使用单调时钟,不收系统时间调整影响

第二个参数传入0表示没有特殊标志

然后我们需要设置定时器参数,这里使用的是itimerspec类型的结构体

struct itimerspec
{
    struct timespec it_interval;
    struct timespec it_value;
};

it_value包含两个参数,tv_sec和tv_nsec分别用来设置秒和毫秒,tv_sec用来设置第一次超时时间,it_interval同样包含两个参数tv_sec和tv_nsec,用来设置第一次超时后,每次的超时时间间隔

可以使用read函数,传入定时器文件描述符来读取一共超时了多少次

int ret = read(_timerfd, ×, 8);

下面就是这个类完整代码了

using TaskFunc = std::function;
using ReleaseFunc = std::function;
class TimerTask
{
private:
    uint64_t _id;         // 定时器任务对象id
    uint32_t _timeout;    // 定时任务的超时时间
    bool _canceled;       // false-表示没有被取消
    TaskFunc _task_cb;    // 定时器对象要执行的定时任务
    ReleaseFunc _release; // 用于删除TimerWheel中保存的定时器对象信息
public:
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb)
        : _id(id), _timeout(delay), _task_cb(cb), _canceled(false)
    {
    }

    ~TimerTask()
    {
        if (_canceled == false)
            _task_cb();
        _release();
    }

    void Cancel()
    {
        _canceled = true;
    }

    void SetRelease(const ReleaseFunc &cb)
    {
        _release = cb;
    }

    uint32_t DelayTime()
    {
        return _timeout;
    }
};

class TimerWheel
{
private:
    using WeakTask = std::weak_ptr;
    using PtrTask = std::shared_ptr;
    int _tick;     // 当前的秒针,走到哪里释放哪里
    int _capacity; // 表盘最大数量,其实就是最大延迟时间
    std::vector> _wheel;
    std::unordered_map _timers; // 使用任务ID找到对应的weak_ptr

    EventLoop *_loop;
    int _timerfd; // 定时器描述符
    std::unique_ptr _timer_channel;

private:
    void RemoveTimer(uint16_t id)
    {
        auto it = _timers.find(id);
        if (it != _timers.end())
        {
            _timers.erase(it);
        }
    }
    static int CreateTimerFd()
    {
        int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
        if (timerfd < 0)
        {
            ERR_LOG("TIMERED CREATE FAILED!");
            abort();
        }

        struct itimerspec itime;
        itime.it_value.tv_sec = 1;
        itime.it_value.tv_nsec = 0; // 第一次超时时间为1s后
        itime.it_interval.tv_sec = 1;
        itime.it_interval.tv_nsec = 0; // 第一次超时后,每次超时的时间间隔
        timerfd_settime(timerfd, 0, &itime, NULL);
        return timerfd;
    }
    int ReadTimefd()
    {
        uint64_t times;
        //有可能因为其他描述符的事件处理花费时间比较长,然后在处理定时器描述符事件的时候,有可能就已经超时了很多次
        //read读取到的数据times就是从上一次read之后超时的次数
        int ret = read(_timerfd, ×, 8);
        if (ret < 0)
        {
            ERR_LOG("READ TIMER FAILED!");
            abort();
        }
        return times;
    }
    // 这个函数每秒被执行一次,相当于秒针向后走了一步
    void RunTimeTask()
    {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear();
    }
    void OnTime()
    {
        //根据实际超时的次数,执行对应的超时任务
        int times = ReadTimefd();
        for(int i = 0; i < times; i++)
        {
            RunTimeTask();
        }
    }
    void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) // 添加定时任务
    {
        PtrTask pt(new TimerTask(id, delay, cb));
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
        _timers[id] = WeakTask(pt);
    }

    void TimerRefreshInLoop(uint64_t id) // 刷新/延迟定时任务
    {
        // 通过保存的定时器对象的weakptr构造一个shared_ptr出来,添加到轮子中
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            // 没有找到定时任务
            return;
        }
        PtrTask pt = it->second.lock(); // lock获取weakptr管理的对象对应的shared_ptr;
        int delay = pt->DelayTime();
        int pos = (_tick + delay) % _capacity;
        _wheel[pos].push_back(pt);
    }

    void TimerCancelInLoop(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            // 没有找到定时任务
            return;
        }
        PtrTask pt = it->second.lock();
        if (pt)
            pt->Cancel();
    }

public:
    TimerWheel(EventLoop *loop)
        : _capacity(60),
          _tick(0),
          _wheel(_capacity),
          _loop(loop),
          _timerfd(CreateTimerFd()),
          _timer_channel(new Channel(_loop, _timerfd))
    {
        _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));
        _timer_channel->EnableRead();
    }
    /*定时器中有个_timers成员,定时器信息的操作有可能在多线程中进行, 因此需要考虑线程安全问题*/
    /*如果不想加锁,那就把对定时的所有操作,都放在一个线程中进行*/
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb);
    // 刷新/延迟定时任务
    void TimerRefresh(uint64_t id);
    void TimerCancel(uint64_t id);
    /*这个接口存在线程安全问题--这个接口实际上不能被外界使用者调用,只能在模块内,在对应的EventLoop线程内执行*/
    bool HasTimer(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            return false;
        }
        return true;
    }
};

2.7 Reactor-EventLoop线程池类实现

EventLoop模块可以理解为Reactor模块,它是对Poller模块,TimeQueue模块,Socket模块的一个整体封装,进行所有描述符的事件监控

首先是EventLoop的私有成员变量

using Functor = std::function;
std::thread::id _thread_id;              // 线程ID
int _event_fd;                           // eventfd唤醒IO事件监控有可能导致的阻塞
std::unique_ptr _event_channel; // 通过这个来管理_event_fd中的事件
Poller _poller;                          // 进行所有描述符的事件监控
std::vector _tasks;             // 任务池
std::mutex _mutex;                       // 实现任务池操作的线程安全
TimerWheel _timer_wheel;                 // 定时器模块

然后就是对其中的成员函数解释

首先是执行任务池中的所有任务RunAllTask函数

void RunAllTask()
{
    std::vector functor;
    {
        std::unique_lock _lock(_mutex);
        _tasks.swap(functor); // 交换之后,任务全部交换到了functor里面
    }
    for (auto &f : functor)
    {
        f();
    }
}

这个函数有一个点需要解释一下,就是为什么下面这段代码要加上大括号

{
    std::unique_lock _lock(_mutex);
    _tasks.swap(functor); // 交换之后,任务全部交换到了functor里面
}

原因:大括号用来定义一个作用域,使用大括号包含std::unique_lock的声明和_tasks.swap(functor)的调用,是为了限制std::unique_lock对象的生命周期和作用域。std::unique_lock是一个RALL对象,会在构造时获取锁,并在析构时自动释放锁,将对象的作用域限制在大括号内,可以确保在离开这个作用域时,锁会被自动释放,这可以防止由于异常或提前返回导致的锁未释放问题


创建eventfd

static int CreateEventFd()
{
    int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
    if (efd < 0)
    {
        ERR_LOG("CREATE EVENTFD FAILED!");
        abort();
    }
    return efd;
}

这里解释一下eventfd函数

第一个参数0表示初始事件计数为0,第二个参数是标志位,用于控制eventfd的行为

EFD_CLOEXEC:设置close-on-exec标志,确保在执行exec系列函数时关闭文件描述符

例如,在创建子进程时,父进程可能打开了一些文件描述符用于内部通信。如果不设置close-on-exec标志,这些文件描述符可能会被子进程继承,导致资源泄漏或安全问题。

EFD_NONBLOCK:设置非阻塞模式,对eventfd的读写操作不会阻塞调用线程


从_event_fd中读取事件计数

void ReadEventFd()
{
    uint64_t res = 0;
    int ret = read(_event_fd, &res, sizeof(res));
    if (ret < 0)
    {
        if (errno == EINTR || errno == EAGAIN)
        {
            return;
        }
        ERR_LOG("READ EVENTFD FAILED!");
        abort();
    }
    return;
}

向eventfd中写入一个事件计数,以通知其他线程或进程事件的发生

void WakeUpEventFd()
{
    uint64_t val = 1;
    int ret = write(_event_fd, &val, sizeof(val));
    if (ret < 0)
    {
        if (errno == EINTR)
        {
            return;
        }
        ERR_LOG("READ EVENTFD FAILED!");
        abort();
    }
    return;
}

Start函数,分为三个步骤:事件监控->就绪事件处理->执行任务

void Start()
{
    while (1)
    {
        std::vector actives;
        _poller.Poll(&actives);

        for (auto &channel : actives)
        {
            channel->HandleEvent();
        }

        RunAllTask();
    }
}

使用IsInLoop函数判断当前线程是否为eventloop对应的线程

bool IsInLoop()
{
    return (_thread_id == std::this_thread::get_id());
}

判断要执行的任务是否处于当前线程中,如果是则执行,不是则压入队列

void RunInLoop(const Functor &cb)
{
    if (IsInLoop())
    {
        return cb();
    }
    return QueueInLoop(cb);
}

将操作压入任务池

void QueueInLoop(const Functor &cb)
{
    {
        std::unique_lock _lock(_mutex);
        _tasks.push_back(cb);
    }
    // 唤醒有可能因为没有事件就绪而导致的epoll阻塞
    // 其实就是给eventfd写入一个数据,eventfd就会触发可读事件
    WakeUpEventFd();
}

最后就是对描述符监控的操作

// 添加/修改描述符的事件监控
void UpdateEvent(Channel *channel)
{
    return _poller.UpdateEvent(channel);
}
// 移除描述符的监控
void RemoveEvent(Channel *channel)
{
    return _poller.RemoveEvent(channel);
}
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{
    return _timer_wheel.TimerAdd(id, delay, cb);
}
void TimerRefresh(uint64_t id)
{
    return _timer_wheel.TimerRefresh(id);
}
void TimerCancel(uint64_t id)
{
    return _timer_wheel.TimerCancel(id);
}
bool HasTimer(uint64_t id)
{
    return _timer_wheel.HasTimer(id);
}

全部代码如下

class EventLoop
{
private:
    using Functor = std::function;
    std::thread::id _thread_id;              // 线程ID
    int _event_fd;                           // eventfd唤醒IO事件监控有可能导致的阻塞
    std::unique_ptr _event_channel; // 通过这个来管理_event_fd中的事件
    Poller _poller;                          // 进行所有描述符的事件监控
    std::vector _tasks;             // 任务池
    std::mutex _mutex;                       // 实现任务池操作的线程安全
    TimerWheel _timer_wheel;                 // 定时器模块
public:
    // 执行任务池中的所有任务
    void RunAllTask()
    {
        std::vector functor;
        {
            std::unique_lock _lock(_mutex);
            _tasks.swap(functor); // 交换之后,任务全部交换到了functor里面
        }
        for (auto &f : functor)
        {
            f();
        }
    }
    static int CreateEventFd()
    {
        int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
        if (efd < 0)
        {
            ERR_LOG("CREATE EVENTFD FAILED!");
            abort();
        }
        return efd;
    }
    void ReadEventFd()
    {
        uint64_t res = 0;
        int ret = read(_event_fd, &res, sizeof(res));
        if (ret < 0)
        {
            if (errno == EINTR || errno == EAGAIN)
            {
                return;
            }
            ERR_LOG("READ EVENTFD FAILED!");
            abort();
        }
        return;
    }
    void WakeUpEventFd()
    {
        uint64_t val = 1;
        int ret = write(_event_fd, &val, sizeof(val));
        if (ret < 0)
        {
            if (errno == EINTR)
            {
                return;
            }
            ERR_LOG("READ EVENTFD FAILED!");
            abort();
        }
        return;
    }

public:
    EventLoop()
        : _thread_id(std::this_thread::get_id()),
          _event_fd(CreateEventFd()),
          _event_channel(new Channel(this, _event_fd)),
          _timer_wheel(this)
    {
        _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd, this));
        // 启动eventfd读事件监控
        _event_channel->EnableRead();
    }
    void Start() // 三步走--事件监控->就绪事件处理->执行任务
    {
        while (1)
        {
            std::vector actives;
            _poller.Poll(&actives);

            for (auto &channel : actives)
            {
                channel->HandleEvent();
            }

            RunAllTask();
        }
    }
    // 用于判断当前线程是否是eventloop对应的线程
    bool IsInLoop()
    {
        return (_thread_id == std::this_thread::get_id());
    }
    void AssertInLoop()
    {
        assert(_thread_id == std::this_thread::get_id());
    }
    // 判断要执行的任务是否处于当前线程中,如果是则执行,如果不是则压入队列
    void RunInLoop(const Functor &cb)
    {
        if (IsInLoop())
        {
            return cb();
        }
        return QueueInLoop(cb);
    }
    // 将操作压入任务池
    void QueueInLoop(const Functor &cb)
    {
        {
            std::unique_lock _lock(_mutex);
            _tasks.push_back(cb);
        }
        // 唤醒有可能因为没有事件就绪而导致的epoll阻塞
        // 其实就是给eventfd写入一个数据,eventfd就会触发可读事件
        WakeUpEventFd();
    }
    // 添加/修改描述符的事件监控
    void UpdateEvent(Channel *channel)
    {
        return _poller.UpdateEvent(channel);
    }
    // 移除描述符的监控
    void RemoveEvent(Channel *channel)
    {
        return _poller.RemoveEvent(channel);
    }
    void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
    {
        return _timer_wheel.TimerAdd(id, delay, cb);
    }
    void TimerRefresh(uint64_t id)
    {
        return _timer_wheel.TimerRefresh(id);
    }
    void TimerCancel(uint64_t id)
    {
        return _timer_wheel.TimerCancel(id);
    }
    bool HasTimer(uint64_t id)
    {
        return _timer_wheel.HasTimer(id);
    }
};

 2.8 用于管理单个事件循环线程LoopThread类

这个类的主要作用是确保线程安全地创建和访问EventLoop对象

关于成员变量

std::mutex _mutex;             // 互斥锁
std::condition_variable _cond; // 条件变量
EventLoop *_loop;              // EventLoop指针变量,这个对象需要在线程
std::thread _thread;           // EventLoop对应的线程

接下来解释一下成员函数

首先是ThreadEntry,负责在线程中实例化EventLoop对象,并确保其他线程能够安全地访问这个EventLoop对象。

void ThreadEntry()
{
    EventLoop loop;
    {
        std::unique_lock lock(_mutex);
        _loop = &loop;
        _cond.notify_all();
    }
    loop.Start();
}

这里解释一下_cond.notify_all(),他可以唤醒所有在_cond条件变量上等待的线程,通知他们_loop已经初始化完成。


关于这个类的初始化函数

初始化_thread成员变量为一个新的线程,该线程将执行ThreadEntry方法。

LoopThread()
    : _loop(NULL),
      _thread(std::thread(&LoopThread::ThreadEntry, this))
{
}

然后就是返回当前线程关联的EventLoop指针

直到_loop不为空才将loop初始化

EventLoop *GetLoop()
{
    EventLoop *loop = NULL;
    {
        std::unique_lock lock(_mutex); // 加锁
        /*满足什么的条件就不会等待*/
        // loop为空就一直阻塞
        _cond.wait(lock, [&]()
                    { return _loop != NULL; });
        loop = _loop;
    }
    return loop;
}

下面是完整代码

class LoopThread
{
private:
    /*用于实现_loop获取的同步关系,避免线程创建了,但是_loop还没有实例化之前去获取_loop*/
    std::mutex _mutex;             // 互斥锁
    std::condition_variable _cond; // 条件变量
    EventLoop *_loop;              // EventLoop指针变量,这个对象需要在线程
    std::thread _thread;           // EventLoop对应的线程
private:
    /*实例化EventLoop对象,唤醒_cond上有可能阻塞的线程,并且开始运行EventLoop模块的功能*/
    void ThreadEntry()
    {
        EventLoop loop;
        {
            std::unique_lock lock(_mutex);
            _loop = &loop;
            _cond.notify_all();
        }
        loop.Start();
    }

public:
    /*创建线程,设置线程入口函数*/
    LoopThread()
        : _loop(NULL),
          _thread(std::thread(&LoopThread::ThreadEntry, this))
    {
    }
    /*返回当前线程关联的EventLoop指针*/
    EventLoop *GetLoop()
    {
        EventLoop *loop = NULL;
        {
            std::unique_lock lock(_mutex); // 加锁
            /*满足什么的条件就不会等待*/
            // loop为空就一直阻塞
            _cond.wait(lock, [&]()
                       { return _loop != NULL; });
            loop = _loop;
        }
        return loop;
    }
};

2.9  线程池的主要逻辑LoopThreadPool类

LoopThreadPool用于管理多个LoopThread对象,提供线程池功能

首先是成员变量

int _thread_count;
int _next_idx;
EventLoop *_baseloop;
std::vector _threads;
std::vector _loops;
  • _thread_count:线程的数量
  • _next_idx:下一个线程的索引
  • _baseloop:指向一个EventLoop的指针
  • _threads:存储指向LoopThread对象的指针
  • _loops:存储指向EventLoop对象的指针

然后就是对主要的成员函数的解释

首先是对线程池的创建Create()

当线程的总数大于0的时候,首先需要对_threads和_loops这两个数组进行初始化,然后创建_thread_count数目的线程以及线程对应的EventLoop


接下来是NextLoop函数,这个使用了轮询的方式来分配任务

EventLoop *NextLoop()
{
    if (_thread_count == 0)
    {
        return _baseloop;
    }
    _next_idx = (_next_idx + 1) % _thread_count;
    return _loops[_next_idx];
}

如果可用的工作线程数目为0,那么返回_baseloop,即基础的事件循环。通过模运算更新_next_idx实现轮询选择,最后根据更新后的_next_idx返回相应的事件循环


下面就是这个类的完整代码

class LoopThreadPool
{
private:
    int _thread_count;
    int _next_idx;
    EventLoop *_baseloop;
    std::vector _threads;
    std::vector _loops;

public:
    LoopThreadPool(EventLoop *baseloop)
        : _thread_count(0),
          _next_idx(0),
          _baseloop(baseloop)
    {
    }
    void SetThreadCount(int count)
    {
        _thread_count = count;
    }
    void Create()
    {
        if (_thread_count > 0)
        {
            _threads.resize(_thread_count);
            _loops.resize(_thread_count);
            for (int i = 0; i < _thread_count; i++)
            {
                _threads[i] = new LoopThread();
                _loops[i] = _threads[i]->GetLoop();
            }
        }
        return;
    }
    EventLoop *NextLoop()
    {
        if (_thread_count == 0)
        {
            return _baseloop;
        }
        _next_idx = (_next_idx + 1) % _thread_count;
        return _loops[_next_idx];
    }
};

 2.10 Any类代码的实现

前面的准备知识中已经详细解释了Any类的具体实现,那么这里直接给出代码

class Any
{
private:
    class holder
    {
    public:
        virtual ~holder()
        {
        }
        // 数据类型
        virtual const std::type_info &type() = 0;
        // 克隆出新的对象
        virtual holder *clone() = 0;
    };
    template 
    class placeholder : public holder
    {
    public:
        placeholder(const T &val) : _val(val) {}
        // 获取子类对象保存的数据类型
        virtual const std::type_info &type()
        {
            return typeid(T);
        }
        // 针对出当前的对象自身,克隆出一个新的对象
        virtual holder *clone()
        {
            return new placeholder(_val);
        }

    public:
        T _val;
    };
    holder *_content;

public:
    Any() : _content(NULL) {}
    template 
    Any(const T &val) : _content(new placeholder(val)) {}
    Any(const Any &other) : _content(other._content ? other._content->clone() : NULL) {}
    ~Any() { delete _content; }

    Any &swap(Any &other)
    {
        std::swap(_content, other._content);
        return *this;
    }

    template 
    // 返回子类对象保存的数据的指针
    T *get()
    {
        // 想要获取的数据类型,必须和保存的数据类型一致
        // if(typeid(T) != _content->type()) return NULL;
        assert(typeid(T) == _content->type());
        return &((placeholder *)_content)->_val;
    }
    // 赋值运算符重载函数
    template 
    Any &operator=(const T &val)
    {
        // 为val构造一个临时的通用容器,然后与当前容器自身进行指针交换
        // 临时对象释放的时候,原先保存的数据也就被释放了
        Any(val).swap(*this);
        return *this;
    }
    Any &operator=(const Any &other)
    {
        Any(other).swap(*this);
        return *this;
    }
};

2.11 通信连接管理Connection类实现

Connection模块时对Buffer模块,Socket模块Channel模块的一个整体封装,实现了对一个套接通信字整体的管理,每一个进行数据通信的套接字(也就是accept获取到的新链接)都会使用Connecition进行管理。

首先,连接Connection的状态肯定有多种,这里我们使用enum来管理连接的状态

typedef enum
{
    DISCONNECTED, // 连接关闭状态
    CONNECTING,   // 连接建立成功待处理状态
    CONNECTED,    // 连接建立完成,各种设置已完成可以通信的状态
    DISCONNECTING // 待关闭状态
} ConnStatu;

接下来介绍一下Connection主题的设计

这里介绍一下shared_from_this,他是C++标准库的一个机制,定义在头文件中,通过std::enable_shared_fron_this模版类提供。它允许一个对象安全地生成指向自身的shared_ptr,而不会创建额外的控制块

shared_fron_this解决了以下问题

  1. 避免多控制块:确保所有shared_ptr共享同一个控制块
  2. 防止双重释放:消除因多个控制块导致的重复析构
  3. 安全自引用:使对象能安全地参与回调等需要延长生命周期的场景

所以,为了安全地生成指向自身的shared_ptr,我们让Connection继承enable_shared_from_this


下面介绍一下这个类的私有成员

uint64_t _conn_id; // 连接唯一ID,便于连接的管理和查找
// uint64_t _timer_id; //定时器ID,必须是唯一的,为了简化操作使用conn_id作为定时器ID
int _sockfd;                   // 连接关联的文件描述符
bool _enable_inactive_release; // 连接是否启动非活跃销毁,默认是false
EventLoop *_loop;              // 连接所关联的一个loop
ConnStatu _statu;              // 连接状态
Socket _socket;                // 套接字连接管理
Channel _channel;              // 连接的事件管理
Buffer _in_buffer;             // 输入缓冲区---存放从socket中读取到的数据
Buffer _out_buffer;            // 输出缓冲区---存放要发送给对端的数据
Any _context;                  // 请求的处理接收处理上下文
// 对外操作使用智能指针
// 这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的)
/**/
using ConnectedCallback = std::function;
using MessageCallback = std::function;
using ClosedCallback = std::function;
using AnyEventCallback = std::function;
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
/*组件内的连接关闭回调--组件内设置的,因为服务器组件内会把所有的链接管理起来,一旦某个连接要关闭*/
/*就应该从管理的地方移除掉自己的信息*/
ClosedCallback _server_closed_callback;

然后就是关于成员函数的介绍

首先是对描述符可读事件触发后调用的函数,接收socket数据放到接收缓冲区中,然后调用_message_callback

void HandleRead()
{
    /*接收socket的数据,放到缓冲区*/
    char buf[65536];
    ssize_t ret = _socket.NonBlockRecv(buf, 65535);
    if (ret < 0)
    {
        /*出错了,不能直接关闭连接,要看接受缓冲区和发送缓冲区中还有没有数据*/
        return ShutDownInLoop();
    }
    // else if(ret == 0)
    // {
    //     //这里的0表示没有读取到数据,而不是连接断开了,连接断开返回的是-1
    //     return;
    // }
    // 将数据放入到输入缓冲区,并且移动写偏移
    _in_buffer.WriteAndPush(buf, ret);
    /*调用_message_callback进行业务处理*/
    if (_in_buffer.ReadAbleSize() > 0)
    {
        // shared_fron_this从当前对象自身获取到
        return _message_callback(shared_from_this(), &_in_buffer);
    }
}

处理写事件的函数逻辑与上面差不多

void HandleWrite() // 描述符可写事件触发后调用的函数,将发送缓冲区中的数据进行发送
{
    //_out_buffer中保存的数据就是要发送的数据
    ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
    if (ret < 0)
    {
        // 发送错误就该关闭连接了
        if (_in_buffer.ReadAbleSize() > 0)
        {
            _message_callback(shared_from_this(), &_in_buffer);
        }
        return Release(); // 这时候就是实际的关闭释放操作
    }
    _out_buffer.MoveReadOffset(ret);
    if (_out_buffer.ReadAbleSize() == 0)
    {
        _channel.DisableWrite(); // 没有数据待发送了,关闭写事件监控
        // 如果连接时待关闭状态,则有数据,发送完数据释放连接,没有数据则直接释放
        if (_statu == DISCONNECTING)
        {
            return Release();
        }
    }
    return;
}

然后就是描述符触发挂断事件和出错事件

void HandleClose() // 描述符触发挂断事件
{
    // 一旦连接挂断了,套接字就什么都干不了,因此有数据就处理一下,完毕关闭连接
    if (_in_buffer.ReadAbleSize() > 0)
    {
        _message_callback(shared_from_this(), &_in_buffer);
    }
    return Release();
}
void HandleError() // 描述符触发出错事件
{
    HandleClose();
}

当描述符触发任一事件时,我们需要刷新连接的活跃度

void HandleEvent() // 描述符触发任一事件
{
    // 刷新链接的活跃度 + 调用组件使用者的任意事件回调
    if (_enable_inactive_release == true)
    {
        _loop->TimerRefresh(_conn_id);
    }
    if (_event_callback)
    {
        _event_callback(shared_from_this());
    }
}

下个函数会把连接状态从半连接修改为连接状态,并且会启动读事件监控

void EstablishedInLoop() // 连接获取之后,所处的状态下要进行的各种设置(给Channel设置时间回调,启动读监控)
{
    // 修改连接状态 + 启动读事件监控 + 调用回调函数
    assert(_statu == CONNECTING); // 当前的状态必须一定是上层的半连接状态
    _statu = CONNECTED;           // 当前函数执行完毕,则连接进入已完成连接状态
    // 一旦启动读事件监控就有可能会立即触发读事件,如果这时候启动了非活跃链接销毁
    _channel.EnableRead();
    if (_connected_callback)
        _connected_callback(shared_from_this());
}

对连接进行释放的函数ReleaseInLoop()

void ReleaseInLoop() // 这个接口才是实际的释放接口
{
    // 修改连接状态,设置为DISCONNECTED
    _statu = DISCONNECTED;
    // 移除连接的事件监控
    _channel.Remove();
    // 关闭描述符
    _socket.Close();
    // 如果当前定时器队列中还有定时销毁任务,则取消任务,避免野指针操作
    if (_loop->HasTimer(_conn_id))
        CancelInactiveReleaseInLoop();
    // 调用关闭回调函数,避免因为先移除服务器管理的连接信息导致Connection被释放,再去处理就会出错,因此先调用用户
    if (_closed_callback)
        _closed_callback(shared_from_this());
    // 移除服务器内部管理的连接信息
    if (_server_closed_callback)
        _server_closed_callback(shared_from_this());
}

把数据放入到发送缓冲区,启动可写事件监控

void SendInLoop(Buffer &buf)
{
    if (_statu == DISCONNECTED)
        return;
    _out_buffer.WriteBufferAndPush(buf);
    if (_channel.WriteAble() == false)
    {
        _channel.EnableWrite();
    }
}

这个关闭操作并非实际的连接释放操作,需要判断还有没有数据待处理,待发送

void ShutDownInLoop()
{
    _statu = DISCONNECTED; // 设置连接为半关闭状态
    if (_in_buffer.ReadAbleSize() > 0)
    {
        if (_message_callback)
            _message_callback(shared_from_this(), &_in_buffer);
    }
    // 要么就是写入数据的时候出错关闭,要么就是没有待发送数据,直接关闭
    if (_out_buffer.ReadAbleSize() > 0)
    {
        if (_channel.WriteAble() == false) // 是否启动写事件监控
        {
            _channel.EnableWrite();
        }
    }
    // 没有数据了
    if (_out_buffer.ReadAbleSize() == 0)
    {
        Release();
    }
}

启动非活跃链接超时释放,不存在这个销毁任务就新增,已经存在则刷新定时器

void EnableInactiveReleaseInLoop(int sec)
{
    // 将判断标志_enable_inactive_release 置为true
    _enable_inactive_release = true;
    // 如果当前定时销毁任务已经存在,那就刷新延迟一下即可
    if (_loop->HasTimer(_conn_id))
    {
        return _loop->TimerRefresh(_conn_id);
    }
    // 如果不存在定时销毁任务,则新增
    _loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));
}

取消非活跃连接释放

void CancelInactiveReleaseInLoop()
{
    _enable_inactive_release = false;
    if (_loop->HasTimer(_conn_id))
    {
        _loop->TimerCancel(_conn_id);
    }
}

切换协议,重置上下文以及阶段性处理函数

void UpgradeInLoop(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,
                    const ClosedCallback &closed, const AnyEventCallback &event)
{
    _context = context;
    _connected_callback = conn;
    _message_callback = msg;
    _closed_callback = closed;
    _event_callback = event;
}

下面是Connection类的完整代码

typedef enum
{
    DISCONNECTED, // 连接关闭状态
    CONNECTING,   // 连接建立成功待处理状态
    CONNECTED,    // 连接建立完成,各种设置已完成可以通信的状态
    DISCONNECTING // 待关闭状态
} ConnStatu;

using PtrConnection = std::shared_ptr;

class Connection : public std::enable_shared_from_this
{
private:
    uint64_t _conn_id; // 连接唯一ID,便于连接的管理和查找
    // uint64_t _timer_id; //定时器ID,必须是唯一的,为了简化操作使用conn_id作为定时器ID
    int _sockfd;                   // 连接关联的文件描述符
    bool _enable_inactive_release; // 连接是否启动非活跃销毁,默认是false
    EventLoop *_loop;              // 连接所关联的一个loop
    ConnStatu _statu;              // 连接状态
    Socket _socket;                // 套接字连接管理
    Channel _channel;              // 连接的事件管理
    Buffer _in_buffer;             // 输入缓冲区---存放从socket中读取到的数据
    Buffer _out_buffer;            // 输出缓冲区---存放要发送给对端的数据
    Any _context;                  // 请求的处理接收处理上下文
    // 对外操作使用智能指针
    // 这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的)
    /**/
    using ConnectedCallback = std::function;
    using MessageCallback = std::function;
    using ClosedCallback = std::function;
    using AnyEventCallback = std::function;
    ConnectedCallback _connected_callback;
    MessageCallback _message_callback;
    ClosedCallback _closed_callback;
    AnyEventCallback _event_callback;
    /*组件内的连接关闭回调--组件内设置的,因为服务器组件内会把所有的链接管理起来,一旦某个连接要关闭*/
    /*就应该从管理的地方移除掉自己的信息*/
    ClosedCallback _server_closed_callback;

private:
    /*五个channel的事件回调函数*/
    void HandleRead() // 描述符可读事件触发后调用的函数,接收socket数据放到接受缓冲区中,然后调用_message_callback
    {
        /*接收socket的数据,放到缓冲区*/
        char buf[65536];
        ssize_t ret = _socket.NonBlockRecv(buf, 65535);
        if (ret < 0)
        {
            /*出错了,不能直接关闭连接,要看接受缓冲区和发送缓冲区中还有没有数据*/
            return ShutDownInLoop();
        }
        // else if(ret == 0)
        // {
        //     //这里的0表示没有读取到数据,而不是连接断开了,连接断开返回的是-1
        //     return;
        // }
        // 将数据放入到输入缓冲区,并且移动写偏移
        _in_buffer.WriteAndPush(buf, ret);
        /*调用_message_callback进行业务处理*/
        if (_in_buffer.ReadAbleSize() > 0)
        {
            // shared_fron_this从当前对象自身获取到
            return _message_callback(shared_from_this(), &_in_buffer);
        }
    }
    void HandleWrite() // 描述符可写事件触发后调用的函数,将发送缓冲区中的数据进行发送
    {
        //_out_buffer中保存的数据就是要发送的数据
        ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
        if (ret < 0)
        {
            // 发送错误就该关闭连接了
            if (_in_buffer.ReadAbleSize() > 0)
            {
                _message_callback(shared_from_this(), &_in_buffer);
            }
            return Release(); // 这时候就是实际的关闭释放操作
        }
        _out_buffer.MoveReadOffset(ret);
        if (_out_buffer.ReadAbleSize() == 0)
        {
            _channel.DisableWrite(); // 没有数据待发送了,关闭写事件监控
            // 如果连接时待关闭状态,则有数据,发送完数据释放连接,没有数据则直接释放
            if (_statu == DISCONNECTING)
            {
                return Release();
            }
        }
        return;
    }
    void HandleClose() // 描述符触发挂断事件
    {
        // 一旦连接挂断了,套接字就什么都干不了,因此有数据就处理一下,完毕关闭连接
        if (_in_buffer.ReadAbleSize() > 0)
        {
            _message_callback(shared_from_this(), &_in_buffer);
        }
        return Release();
    }
    void HandleError() // 描述符触发出错事件
    {
        HandleClose();
    }
    void HandleEvent() // 描述符触发任一事件
    {
        // 刷新链接的活跃度 + 调用组件使用者的任意事件回调
        if (_enable_inactive_release == true)
        {
            _loop->TimerRefresh(_conn_id);
        }
        if (_event_callback)
        {
            _event_callback(shared_from_this());
        }
    }
void EstablishedInLoop() // 连接获取之后,所处的状态下要进行的各种设置(给Channel设置时间回调,启动读监控)
{
    // 修改连接状态 + 启动读事件监控 + 调用回调函数
    assert(_statu == CONNECTING); // 当前的状态必须一定是上层的半连接状态
    _statu = CONNECTED;           // 当前函数执行完毕,则连接进入已完成连接状态
    // 一旦启动读事件监控就有可能会立即触发读事件,如果这时候启动了非活跃链接销毁
    _channel.EnableRead();
    if (_connected_callback)
        _connected_callback(shared_from_this());
}
    void ReleaseInLoop() // 这个接口才是实际的释放接口
    {
        // 修改连接状态,设置为DISCONNECTED
        _statu = DISCONNECTED;
        // 移除连接的事件监控
        _channel.Remove();
        // 关闭描述符
        _socket.Close();
        // 如果当前定时器队列中还有定时销毁任务,则取消任务,避免野指针操作
        if (_loop->HasTimer(_conn_id))
            CancelInactiveReleaseInLoop();
        // 调用关闭回调函数,避免因为先移除服务器管理的连接信息导致Connection被释放,再去处理就会出错,因此先调用用户
        if (_closed_callback)
            _closed_callback(shared_from_this());
        // 移除服务器内部管理的连接信息
        if (_server_closed_callback)
            _server_closed_callback(shared_from_this());
    }

    // 这个接口并不是实际的发送接口,而只是把数据放到发送缓冲区,启动可写事件监控
    void SendInLoop(Buffer &buf)
    {
        if (_statu == DISCONNECTED)
            return;
        _out_buffer.WriteBufferAndPush(buf);
        if (_channel.WriteAble() == false)
        {
            _channel.EnableWrite();
        }
    }
    // 这个关闭操作并非实际的连接释放操作,需要判断还有没有数据待处理,待发送
    void ShutDownInLoop()
    {
        _statu = DISCONNECTED; // 设置连接为半关闭状态
        if (_in_buffer.ReadAbleSize() > 0)
        {
            if (_message_callback)
                _message_callback(shared_from_this(), &_in_buffer);
        }
        // 要么就是写入数据的时候出错关闭,要么就是没有待发送数据,直接关闭
        if (_out_buffer.ReadAbleSize() > 0)
        {
            if (_channel.WriteAble() == false) // 是否启动写事件监控
            {
                _channel.EnableWrite();
            }
        }
        // 没有数据了
        if (_out_buffer.ReadAbleSize() == 0)
        {
            Release();
        }
    }
    // 启动非活跃连接超时释放规则
    void EnableInactiveReleaseInLoop(int sec)
    {
        // 将判断标志_enable_inactive_release 置为true
        _enable_inactive_release = true;
        // 如果当前定时销毁任务已经存在,那就刷新延迟一下即可
        if (_loop->HasTimer(_conn_id))
        {
            return _loop->TimerRefresh(_conn_id);
        }
        // 如果不存在定时销毁任务,则新增
        _loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));
    }
    void CancelInactiveReleaseInLoop()
    {
        _enable_inactive_release = false;
        if (_loop->HasTimer(_conn_id))
        {
            _loop->TimerCancel(_conn_id);
        }
    }
    // 切换协议,重置上下文以及阶段性处理函数
    void UpgradeInLoop(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,
                       const ClosedCallback &closed, const AnyEventCallback &event)
    {
        _context = context;
        _connected_callback = conn;
        _message_callback = msg;
        _closed_callback = closed;
        _event_callback = event;
    }

public:
    Connection(EventLoop *loop, uint64_t conn_id, int sockfd)
        : _conn_id(conn_id),
          _sockfd(sockfd),
          _enable_inactive_release(false),
          _loop(loop),
          _statu(CONNECTING),
          _socket(_sockfd),
          _channel(loop, _sockfd)
    {
        _channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));
        _channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));
        _channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
        _channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
        _channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
    }
    ~Connection()
    {
        DBG_LOG("RELEASE CONNECTION:%p", this);
    }
    int Fd() // 获取管理的文件描述符
    {
        return _sockfd;
    }
    int Id() // 获取连接的ID
    {
        return _conn_id;
    }
    bool Connected() // 是否处于Connected状态
    {
        return (_statu == CONNECTED);
    }
    void SetContext(const Any &context) // 设置上下文--连接建立完成时
    {
        _context = context;
    }
    Any *GetContext() // 获取上下文,返回的是指针
    {
        return &_context;
    }

    void SetConnectedCallback(const ConnectedCallback &cb)
    {
        _connected_callback = cb;
    }
    void SetMessageCallback(const MessageCallback &cb)
    {
        _message_callback = cb;
    }
    void SetClosedCallback(const ClosedCallback &cb)
    {
        _closed_callback = cb;
    }
    void SetAnyEventCallback(const AnyEventCallback &cb)
    {
        _event_callback = cb;
    }
    void SetSrvClosedCallback(const ClosedCallback &cb)
    {
        _server_closed_callback = cb;
    }
    void Established() // 连接建立就绪后,进行Channel回调设置,启动读监控--连接建立完成时进行调用
    {
        _loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));
    }
    void Send(const char *data, size_t len) // 发送数据,将数据放到发送缓冲区,启动写事件监控
    {
        // 外界传入的data,可能是个临时的空间,我们只是把发送操作压入了任务池,有可能并没有被立即执行
        // 因此有可能执行的时候,data指向的空间有可能已经被释放了
        Buffer buf;
        buf.WriteAndPush(data, len);
        _loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf))); // 右值引用
        //_loop->RunInLoop(std::bind(&Connection::SendInLoop, this, data, len));
    }
    void Shutdown() // 提供给组件使用者的关闭接口--并不实际关闭,需要判断有没有事件待处理
    {
        _loop->RunInLoop(std::bind(&Connection::ShutDownInLoop, this));
    }
    void Release()
    {
        _loop->QueueInLoop(std::bind(&Connection::ReleaseInLoop, this));
    }
    void EnableInactiveRelease(int sec) // 启动非活跃销毁并定义多长时间无通信就是非活跃
    {
        _loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));
    }
    void CancelInactiveRelease() // 取消非活跃销毁
    {
        _loop->RunInLoop(std::bind(&Connection::CancelInactiveReleaseInLoop, this));
    }
    // 切换协议,重置上下文以及阶段性回调处理函数 -- 非线程安全的 -- 这个接口必须在EventLoop线程中执行
    // 防备新的事件触发后,处理的时候,切换任务还没有被执行 -- 会导致数据使用原协议处理了
    void Upgrade(const Any &context, const ConnectedCallback &conn, const MessageCallback &msg,
                 const ClosedCallback &closed, const AnyEventCallback &event)
    {
        _loop->AssertInLoop();
        _loop->RunInLoop(std::bind(&Connection::UpgradeInLoop, this, context, conn, msg, closed, event));
    }
};

 2.12 监听描述符管理Acceptor模块

Acceptor模块是对Socket模块,Channel模块的整体封装,实现对一个监听套接字的整体的管理

首先这是Acceptor类的成员变量介绍

Socket _socket;                                  // 用于创建监听套接字
EventLoop *_loop;                                // 用于对监听套接字进行事件监控
Channel _channel;                                // 用于对监听套接字进行事件管理
using AcceptCallback = std::function; // 获取到链接的回调函数
AcceptCallback _accept_callback;

下面是对其中的成员函数解释

监听套接字的读事件处理函数,获取新链接,调用_accept_callback进行新链接处理

void HandleRead()
{
    // DBG_LOG("ACCEPT HANDLE READ");
    int newfd = _socket.Accept();
    if (newfd < 0)
    {
        return;
    }
    if (_accept_callback)
        _accept_callback(newfd);
}

创建服务端

int CreateServer(int port)
{
    bool ret = _socket.CreateServer(port);
    assert(ret == true);
    return _socket.Fd();
}

然后是Acceptor的初始化函数,这里需要注意的是,不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动

否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没有被设置,新链接得不到处理,且资源泄露

Acceptor(EventLoop *loop, int port)
    : _socket(CreateServer(port)),
        _loop(loop),
        _channel(loop, _socket.Fd())
{
    _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}

下面是这个类的完整代码

class Acceptor
{
private:
    Socket _socket;                                  // 用于创建监听套接字
    EventLoop *_loop;                                // 用于对监听套接字进行事件监控
    Channel _channel;                                // 用于对监听套接字进行事件管理
    using AcceptCallback = std::function; // 获取到链接的回调函数
    AcceptCallback _accept_callback;

private:
    /*监听套接字的读事件处理函数---获取新链接,调用_accept_callback进行新链接处理*/
    void HandleRead()
    {
        // DBG_LOG("ACCEPT HANDLE READ");
        int newfd = _socket.Accept();
        if (newfd < 0)
        {
            return;
        }
        if (_accept_callback)
            _accept_callback(newfd);
    }
    int CreateServer(int port)
    {
        bool ret = _socket.CreateServer(port);
        assert(ret == true);
        return _socket.Fd();
    }

public:
    /*不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动*/
    /*否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没设置:新链接得不到处理,且资源泄露*/
    Acceptor(EventLoop *loop, int port)
        : _socket(CreateServer(port)),
          _loop(loop),
          _channel(loop, _socket.Fd())
    {
        _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
    }
    void SetAcceptCallBack(const AcceptCallback &cb)
    {
        _accept_callback = cb;
    }
    void Listen()
    {
        _channel.EnableRead();
    }
};

2.13 服务器类TcpServer类的实现

这个模块时一个整体Tcp服务器模块的封装,内部封装了Acceptor模块,LoopThreadPool模块

下面是对其成员变量的解释

uint64_t _next_id; // 这是一个自动增长的连接ID
int _timeout;      // 这是非活跃链接的统计时间---多长时间无通信就是非活跃连接
int _port;
bool _enable_inactive_release;                      // 是否启动了非活跃连接超时销毁的判断标志
Acceptor _acceptor;                                 // 这是监听套接字的管理对象
EventLoop _baseloop;                                // 这是主线程的EventLoop对象,负责监听事件的处理
LoopThreadPool _pool;                               // 这是我们的从属EventLoop线程池
std::unordered_map _conns; // 保存管理所有连接对应的shared_ptr对象

using ConnectedCallback = std::function;
using MessageCallback = std::function;
using ClosedCallback = std::function;
using AnyEventCallback = std::function;
using Functor = std::function;
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
  • TcpServer中包含有一个EventLoop对象:以备在超轻量使用场景中不需要EventLoop线程池,只需要在主线程中完成所有操作的情况
  • TcpServer模块内部包含有一个LoopThreadPool对象:其实就是EventLoop线程池,也就是子Reactor线程池
  • TcpServer模块内部包含有一个Acceptor对象:一个TcpServer服务器,必然对应有一个监听套接字,能够完成获取客户端新链接,并处理任务
  • TcpServer模块内部包含有一个std::shared_ptr的hash表:保存了所有的新建连接对应的Connection。需要注意的是,所有的Connection使用shared_ptr进行管理,这样能够保证在hash表中删除了Connection的信息后,在shared_ptr计数器为0的情况下完成对Connection资源的释放操作


接下来是对其成员函数的解释


首先是对主线程的EventLoop对象添加定时器

void RunAfterInLoop(const Functor &task, int delay)
{
    _next_id++;
    _baseloop.TimerAdd(_next_id, delay, task);
}

为新链接构造Connection进行管理

我们为每一个链接初始化一个Connection类进行管理,然后设置各种回调函数,最后将这个链接加入到unordered_map中

void NewConnection(int fd)
{
    // DBG_LOG("NEWCONNECTION FUNCTION");
    _next_id++;
    PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
    conn->SetMessageCallback(_message_callback);     // 为通行套接字设置可读事件的回调函数
    conn->SetClosedCallback(_closed_callback);       // 关闭事件的回调函数
    conn->SetConnectedCallback(_connected_callback); // 错误事件的回调函数
    conn->SetAnyEventCallback(_event_callback);
    conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
    if (_enable_inactive_release)
        conn->EnableInactiveRelease(_timeout); // 启动非活跃超时销毁
    conn->Established();                       // 就绪初始化
    _conns.insert(std::make_pair(_next_id, conn));
}

移除这个链接,只需要从_conns这个unordered_map中移除即可

void RemoveConnectionInLoop(const PtrConnection &conn)
{
    int id = conn->Id();
    auto it = _conns.find(id);
    if (it != _conns.end())
    {
        _conns.erase(it);
    }
}

把这个移除Connection的任务放到_baseloop中执行避免了线程安全问题

// 从管理Connection的_conns中移除连接信息
void RemoveConnection(const PtrConnection &conn)
{
    _baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn));
}

下面是这个类的完整代码

class TcpServer
{
private:
    uint64_t _next_id; // 这是一个自动增长的连接ID
    int _timeout;      // 这是非活跃链接的统计时间---多长时间无通信就是非活跃连接
    int _port;
    bool _enable_inactive_release;                      // 是否启动了非活跃连接超时销毁的判断标志
    Acceptor _acceptor;                                 // 这是监听套接字的管理对象
    EventLoop _baseloop;                                // 这是主线程的EventLoop对象,负责监听事件的处理
    LoopThreadPool _pool;                               // 这是我们的从属EventLoop线程池
    std::unordered_map _conns; // 保存管理所有连接对应的shared_ptr对象

    using ConnectedCallback = std::function;
    using MessageCallback = std::function;
    using ClosedCallback = std::function;
    using AnyEventCallback = std::function;
    using Functor = std::function;
    ConnectedCallback _connected_callback;
    MessageCallback _message_callback;
    ClosedCallback _closed_callback;
    AnyEventCallback _event_callback;

private:
    void RunAfterInLoop(const Functor &task, int delay)
    {
        _next_id++;
        _baseloop.TimerAdd(_next_id, delay, task);
    }
    // 为新连接构造Connection进行管理
    void NewConnection(int fd)
    {
        // DBG_LOG("NEWCONNECTION FUNCTION");
        _next_id++;
        PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
        conn->SetMessageCallback(_message_callback);     // 为通行套接字设置可读事件的回调函数
        conn->SetClosedCallback(_closed_callback);       // 关闭事件的回调函数
        conn->SetConnectedCallback(_connected_callback); // 错误事件的回调函数
        conn->SetAnyEventCallback(_event_callback);
        conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
        if (_enable_inactive_release)
            conn->EnableInactiveRelease(_timeout); // 启动非活跃超时销毁
        conn->Established();                       // 就绪初始化
        _conns.insert(std::make_pair(_next_id, conn));
    }
    void RemoveConnectionInLoop(const PtrConnection &conn)
    {
        int id = conn->Id();
        auto it = _conns.find(id);
        if (it != _conns.end())
        {
            _conns.erase(it);
        }
    }
    // 从管理Connection的_conns中移除连接信息
    void RemoveConnection(const PtrConnection &conn)
    {
        _baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop, this, conn));
    }

public:
    TcpServer(int port)
        : _port(port),
          _next_id(0),
          _enable_inactive_release(false),
          _acceptor(&_baseloop, port),
          _pool(&_baseloop)
    {
        _acceptor.SetAcceptCallBack(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
        _acceptor.Listen(); // 开始监听,挂到_baseloop上面去
    }
    void SetThreadCount(int count) // 设置线程池的数量
    {
        return _pool.SetThreadCount(count);
    }
    void SetConnectedCallback(const ConnectedCallback &cb)
    {
        _connected_callback = cb;
    }
    void SetMessageCallback(const MessageCallback &cb)
    {
        _message_callback = cb;
    }
    void SetClosedCallback(const ClosedCallback &cb)
    {
        _closed_callback = cb;
    }
    void SetAnyEventCallback(const AnyEventCallback &cb)
    {
        _event_callback = cb;
    }
    void EnableInactiveRelease(int timeout)
    {
        _timeout = timeout;
        _enable_inactive_release = true;
    }
    // 多少秒之后执行一个任务,用于添加一个定时任务
    void RunAfter(const Functor &task, int delay)
    {
        _baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));
    }
    // 需要注意的是,_pool.Create();这个调用函数只能发在start函数之中,
    // 不能放在类的初始化函数中,否则会造成越界访问
    void Start()
    {
        _pool.Create();    // 创建线程池中的从属线程
        _baseloop.Start(); // 开始去处理事件了
    }
};

3.HTTP协议支持模块实现

3.1 Util实用工具类实现

首先我们要写一个字符串分割函数出来,这个函数有三个参数,分别是需要被分割的字符串,被分割的目标字符串,分割后的字符串放入的位置

首先创建一个变量offset表示从0位置开始,然后进入循环,我们使用string里面提供的find函数分别传入需要查找的子串和从哪里开始查找offset,并且接受返回值。当返回值是std::string::npos,表示没有找到。如果pos == offset,说明找到了,但是两个位置之间不存在子串,直接让offset跳过查找的字符串,跳过本次循环。如果pos和offset不相等,就直接把pos与offset之间的子串传入到arry中。经过循环之后,最后返回arry的大小,即子串的数量

static size_t Split(const std::string &src, const std::string &sep, std::vector *arry)
{
    size_t offset = 0;

    while (offset < src.size())
    {
        size_t pos = src.find(sep, offset); // 在src字符串偏移量offset处,开始向后查找sep字符/子串,返回查找到的位置
        if (pos == std::string::npos)
        {
            // 没有找到特定字符
            if (pos == src.size())
                break;
            arry->push_back(src.substr(offset));
            return arry->size();
        }
        if (pos == offset)
        {
            offset = pos + sep.size();
            continue;
        } // 当前子串是空的,没有内容
        arry->push_back(src.substr(offset, pos - offset));
        offset = pos + sep.size();
    }
    return arry->size();
}

然后就是读取文件中的所有内容,将读取到的内容放到buffer中

使用std::ifstream ,传入文件的名字并且以二进制的方式读取

将文件指针跳转到末尾之后,使用tellg函数来找到文件的大小

找到文件大小之后再次使用seekg把文件指针放到起始位置

把读取到的数据放入到buf当中,关闭打开的文件流

static bool ReadFile(const std::string &filename, std::string *buf)
{
    std::ifstream ifs(filename, std::ios::binary);
    if (ifs.is_open() == false)
    {
        ERR_LOG("OPEN %s FILE FAILED!!", filename.c_str());
        return false;
    }
    size_t fsize = 0;
    // 文件指针跳转到末尾
    ifs.seekg(0, ifs.end);
    fsize = ifs.tellg();   // 获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小
    ifs.seekg(0, ifs.beg); // 跳转到起始位置
    buf->resize(fsize);    // 开辟文件大小的空间
    ifs.read(&(*buf)[0], fsize);
    if (ifs.good() == false)
    {
        // 读取失败
        ERR_LOG("READ %s FILE FAILED!!", filename.c_str());
        ifs.close();
        return false;
    }
    ifs.close();
    return true;
}

向文件中写入数据

首先需要打开文件,以二进制和截断方式读取,然后使用write函数将buf中的内容写到这个文件当中,最后关闭文件流

static bool WriteFile(const std::string &filename, const std::string &buf)
{
    std::ofstream ofs(filename, std::ios::binary | std::ios::trunc); // trunc代表截断
    if (ofs.is_open() == false)
    {
        ERR_LOG("READ %s FILE FAILED!!", filename.c_str());
        return false;
    }
    ofs.write(buf.c_str(), buf.size());
    if (ofs.good() == false)
    {
        ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());
        ofs.close();
        return false;
    }
    ofs.close();
    return true;
}

这里简单介绍一下URL编码,它的作用是避免URL中的资源路径与查询字符串中的特殊字符与HTTP请求中的特殊字符产生歧义。

编码格式:将特殊字符的ASCII值,转换为两个16进制字符

不编码的特殊字符 RFC3986文档规定.-_~字母,数字属于绝对不编码字符,编码格式%HH

W3C文档中规定查询字符串中的空格,需要被编码为 +,解码则是 + 转化为空格

比如:

  • 原始字符串:Hello World!
  • 编码后:Hello%20World%21
static std::string UrlEncode(const std::string url, bool convert_space_to_plus)
{
    std::string res;
    for (auto &c : url)
    {
        if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c))
        {
            res += c;
            continue;
        }
        if (c == ' ' && convert_space_to_plus == true)
        {
            res += '+';
            continue;
        }
        // 剩下的字符都是需要编码称为%HH的格式的
        char tmp[4] = {0};
        snprintf(tmp, 4, "%%%02X", c);
        res += tmp;
    }
    return res;
}

将十六进制字符转换为对应的整数值

static char HEXTOI(char c)
{
    if (c >= '0' && c <= '9')
    {
        // 数字字符
        return c - '0';
    }
    else if (c >= 'a' && c <= 'z')
    {
        return c - 'a' + 10;
    }
    else if (c >= 'A' && c <= 'Z')
    {
        return c - 'A' + 10;
    }
    return -1;
}

有URL编码当然也有URL解码,当我们遇到了%,我们需要查看百分号后面的第一个数字和第二个数字,将第一个数字左移4位,相当于直接乘以16,然后加上第二个数字即可

static std::string UrlDecode(const std::string url, bool convert_space_to_space)
{
    // 遇到%,则将紧随其后的两个字符,转化为数字,第一个数字左移四位,然后加上第二个数字
    // eg:%2b = 2 * 2^4 + b(转化为十进制)
    std::string res;
    for (int i = 0; i < url.size(); i++)
    {
        if (url[i] == '+' && convert_space_to_space == true)
        {
            res += ' ';
            continue;
        }
        if (url[i] == '%' && i + 2 < url.size())
        {
            char v1 = HEXTOI(url[i + 1]);
            char v2 = HEXTOI(url[i + 2]);
            char v = (v1 << 4) + v2;
            res += v;
            i += 2;
            continue;
        }
        res += url[i];
    }
    return res;
}

我们也需要通过相应的状态码获取相应的状态描述信息

static std::string StatuDesc(int statu)
{

    auto it = _statu_msg.find(statu);
    if (it != _statu_msg.end())
    {
        return it->second;
    }
    return "Unknown";
}

下面将会列出状态码对应的信息

std::unordered_map _statu_msg = {
    {100, "Continue"},
    {101, "Switching Protocol"},
    {102, "Processing"},
    {103, "Early Hints"},
    {200, "OK"},
    {201, "Created"},
    {202, "Accepted"},
    {203, "Non-Authoritative Information"},
    {204, "No Content"},
    {205, "Reset Content"},
    {206, "Partial Content"},
    {207, "Multi-Status"},
    {208, "Already Reported"},
    {226, "IM Used"},
    {300, "Multiple Choice"},
    {301, "Moved Permanently"},
    {302, "Found"},
    {303, "See Other"},
    {304, "Not Modified"},
    {305, "Use Proxy"},
    {306, "unused"},
    {307, "Temporary Redirect"},
    {308, "Permanent Redirect"},
    {400, "Bad Request"},
    {401, "Unauthorized"},
    {402, "Payment Required"},
    {403, "Forbidden"},
    {404, "Not Found"},
    {405, "Method Not Allowed"},
    {406, "Not Acceptable"},
    {407, "Proxy Authentication Required"},
    {408, "Request Timeout"},
    {409, "Conflict"},
    {410, "Gone"},
    {411, "Length Required"},
    {412, "Precondition Failed"},
    {413, "Payload Too Large"},
    {414, "URI Too Long"},
    {415, "Unsupported Media Type"},
    {416, "Range Not Satisfiable"},
    {417, "Expectation Failed"},
    {418, "I'm a teapot"},
    {421, "Misdirected Request"},
    {422, "Unprocessable Entity"},
    {423, "Locked"},
    {424, "Failed Dependency"},
    {425, "Too Early"},
    {426, "Upgrade Required"},
    {428, "Precondition Required"},
    {429, "Too Many Requests"},
    {431, "Request Header Fields Too Large"},
    {451, "Unavailable For Legal Reasons"},
    {501, "Not Implemented"},
    {502, "Bad Gateway"},
    {503, "Service Unavailable"},
    {504, "Gateway Timeout"},
    {505, "HTTP Version Not Supported"},
    {506, "Variant Also Negotiates"},
    {507, "Insufficient Storage"},
    {508, "Loop Detected"},
    {510, "Not Extended"},
    {511, "Network Authentication Required"}};

然后是根据文件后缀名获取mime,这里解释一下mime是什么,是指通过文件名的扩展名来确定该文件对应的mime类型,mime类型是一种标准化的文件类型标识方法,用于告诉客户端如歌处理或解析收到的文件

std::unordered_map _mime_msg = {
    {".aac", "audio/aac"},
    {".abw", "application/x-abiword"},
    {".arc", "application/x-freearc"},
    {".avi", "video/x-msvideo"},
    {".azw", "application/vnd.amazon.ebook"},
    {".bin", "application/octet-stream"},
    {".bmp", "image/bmp"},
    {".bz", "application/x-bzip"},
    {".bz2", "application/x-bzip2"},
    {".csh", "application/x-csh"},
    {".css", "text/css"},
    {".csv", "text/csv"},
    {".doc", "application/msword"},
    {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
    {".eot", "application/vnd.ms-fontobject"},
    {".epub", "application/epub+zip"},
    {".gif", "image/gif"},
    {".htm", "text/html"},
    {".html", "text/html"},
    {".ico", "image/vnd.microsoft.icon"},
    {".ics", "text/calendar"},
    {".jar", "application/java-archive"},
    {".jpeg", "image/jpeg"},
    {".jpg", "image/jpeg"},
    {".js", "text/javascript"},
    {".json", "application/json"},
    {".jsonld", "application/ld+json"},
    {".mid", "audio/midi"},
    {".midi", "audio/x-midi"},
    {".mjs", "text/javascript"},
    {".mp3", "audio/mpeg"},
    {".mpeg", "video/mpeg"},
    {".mpkg", "application/vnd.apple.installer+xml"},
    {".odp", "application/vnd.oasis.opendocument.presentation"},
    {".ods", "application/vnd.oasis.opendocument.spreadsheet"},
    {".odt", "application/vnd.oasis.opendocument.text"},
    {".oga", "audio/ogg"},
    {".ogv", "video/ogg"},
    {".ogx", "application/ogg"},
    {".otf", "font/otf"},
    {".png", "image/png"},
    {".pdf", "application/pdf"},
    {".ppt", "application/vnd.ms-powerpoint"},
    {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
    {".rar", "application/x-rar-compressed"},
    {".rtf", "application/rtf"},
    {".sh", "application/x-sh"},
    {".svg", "image/svg+xml"},
    {".swf", "application/x-shockwave-flash"},
    {".tar", "application/x-tar"},
    {".tif", "image/tiff"},
    {".tiff", "image/tiff"},
    {".ttf", "font/ttf"},
    {".txt", "text/plain"},
    {".vsd", "application/vnd.visio"},
    {".wav", "audio/wav"},
    {".weba", "audio/webm"},
    {".webm", "video/webm"},
    {".webp", "image/webp"},
    {".woff", "font/woff"},
    {".woff2", "font/woff2"},
    {".xhtml", "application/xhtml+xml"},
    {".xls", "application/vnd.ms-excel"},
    {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
    {".xml", "application/xml"},
    {".xul", "application/vnd.mozilla.xul+xml"},
    {".zip", "application/zip"},
    {".3gp", "video/3gpp"},
    {".3g2", "video/3gpp2"},
    {".7z", "application/x-7z-compressed"}};

根据文件后缀名获取文件mime的代码如下

static std::string ExtMime(const std::string &filename)
{

    // a.b.txt
    size_t pos = filename.find_last_of('.');
    if (pos == std::string::npos)
    {
        return "application/octet-stream";
    }
    std::string ext = filename.substr(pos);
    auto it = _mime_msg.find(ext);
    if (it == _mime_msg.end())
    {
        return "application/octet-stream";
    }
    return it->second;
}

判断一个文件是否是一个普通文件

这里需要使用到系统调用stat来获取文件的状态信息,并且需要通过S_ISREG宏来判断文件是否为普通文件

static bool IsRegular(const std::string &filename)
{
    struct stat st;
    int ret = stat(filename.c_str(), &st);
    if (ret < 0)
    {
        return false;
    }
    return S_ISREG(st.st_mode);
}

检查http请求的资源路径的有效性判断

static bool ValidPath(const std::string &path)
{
    std::vector subdir;
    Split(path, "/", &subdir);
    int level = 0;
    for (auto &dir : subdir)
    {
        if (dir == "..")
        {
            level--;
            if (level < 0)
            {
                return false;
            }
            continue;
        }
        level++;
    }
    return true;
}

下面是Util类的完整代码

class Util
{
public:
    // 字符串分割函数,分割后的子串放到vector里面,最终返回子串的数量
    static size_t Split(const std::string &src, const std::string &sep, std::vector *arry)
    {
        size_t offset = 0;

        while (offset < src.size())
        {
            size_t pos = src.find(sep, offset); // 在src字符串偏移量offset处,开始向后查找sep字符/子串,返回查找到的位置
            if (pos == std::string::npos)
            {
                // 没有找到特定字符
                if (pos == src.size())
                    break;
                arry->push_back(src.substr(offset));
                return arry->size();
            }
            if (pos == offset)
            {
                offset = pos + sep.size();
                continue;
            } // 当前子串是空的,没有内容
            arry->push_back(src.substr(offset, pos - offset));
            offset = pos + sep.size();
        }
        return arry->size();
    }
    // 读取文件所有内容,将读取的内容放到buffer中
    static bool ReadFile(const std::string &filename, std::string *buf)
    {
        std::ifstream ifs(filename, std::ios::binary);
        if (ifs.is_open() == false)
        {
            ERR_LOG("OPEN %s FILE FAILED!!", filename.c_str());
            return false;
        }
        size_t fsize = 0;
        // 文件指针跳转到末尾
        ifs.seekg(0, ifs.end);
        fsize = ifs.tellg();   // 获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小
        ifs.seekg(0, ifs.beg); // 跳转到起始位置
        buf->resize(fsize);    // 开辟文件大小的空间
        ifs.read(&(*buf)[0], fsize);
        if (ifs.good() == false)
        {
            // 读取失败
            ERR_LOG("READ %s FILE FAILED!!", filename.c_str());
            ifs.close();
            return false;
        }
        ifs.close();
        return true;
    }
    // 向文件写入数据
    static bool WriteFile(const std::string &filename, const std::string &buf)
    {
        std::ofstream ofs(filename, std::ios::binary | std::ios::trunc); // trunc代表截断
        if (ofs.is_open() == false)
        {
            ERR_LOG("READ %s FILE FAILED!!", filename.c_str());
            return false;
        }
        ofs.write(buf.c_str(), buf.size());
        if (ofs.good() == false)
        {
            ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());
            ofs.close();
            return false;
        }
        ofs.close();
        return true;
    }
    // URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义
    // 编码格式:将特殊字符的ASCII值,转换为两个16进制字符,C++ -> C%2B%2B
    // 不编码的特殊字符 RFC3986文档规定.-_~字母,数字属于绝对不编码字符,编码格式%HH
    // W3C文档中规定查询字符串中的空格,需要被编码为 +,解码则是 + 转化为空格
    static std::string UrlEncode(const std::string url, bool convert_space_to_plus)
    {
        std::string res;
        for (auto &c : url)
        {
            if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(c))
            {
                res += c;
                continue;
            }
            if (c == ' ' && convert_space_to_plus == true)
            {
                res += '+';
                continue;
            }
            // 剩下的字符都是需要编码称为%HH的格式的
            char tmp[4] = {0};
            snprintf(tmp, 4, "%%%02X", c);
            res += tmp;
        }
        return res;
    }
    static char HEXTOI(char c)
    {
        if (c >= '0' && c <= '9')
        {
            // 数字字符
            return c - '0';
        }
        else if (c >= 'a' && c <= 'z')
        {
            return c - 'a' + 10;
        }
        else if (c >= 'A' && c <= 'Z')
        {
            return c - 'A' + 10;
        }
        return -1;
    }
    // URL解码
    static std::string UrlDecode(const std::string url, bool convert_space_to_space)
    {
        // 遇到%,则将紧随其后的两个字符,转化为数字,第一个数字左移四位,然后加上第二个数字
        // eg:%2b = 2 * 2^4 + b(转化为十进制)
        std::string res;
        for (int i = 0; i < url.size(); i++)
        {
            if (url[i] == '+' && convert_space_to_space == true)
            {
                res += ' ';
                continue;
            }
            if (url[i] == '%' && i + 2 < url.size())
            {
                char v1 = HEXTOI(url[i + 1]);
                char v2 = HEXTOI(url[i + 2]);
                char v = (v1 << 4) + v2;
                res += v;
                i += 2;
                continue;
            }
            res += url[i];
        }
        return res;
    }
    // 相应状态码的描述信息获取
    static std::string StatuDesc(int statu)
    {

        auto it = _statu_msg.find(statu);
        if (it != _statu_msg.end())
        {
            return it->second;
        }
        return "Unknown";
    }
    // 根据文件后缀名获取文件mime
    static std::string ExtMime(const std::string &filename)
    {

        // a.b.txt
        size_t pos = filename.find_last_of('.');
        if (pos == std::string::npos)
        {
            return "application/octet-stream";
        }
        std::string ext = filename.substr(pos);
        auto it = _mime_msg.find(ext);
        if (it == _mime_msg.end())
        {
            return "application/octet-stream";
        }
        return it->second;
    }
    // 判断一个文件是否是一个目录
    static bool IsDirectory(const std::string &filename)
    {
        struct stat st;
        int ret = stat(filename.c_str(), &st);
        if (ret < 0)
        {
            return false;
        }
        return S_ISDIR(st.st_mode);
    }
    // 判断一个文件是否是一个普通文件
    static bool IsRegular(const std::string &filename)
    {
        struct stat st;
        int ret = stat(filename.c_str(), &st);
        if (ret < 0)
        {
            return false;
        }
        return S_ISREG(st.st_mode);
    }
    // http请求的资源路径有效性判断
    // /index.html --- 前面的/也叫做相对目录 映射的是某个服务器上的子目录
    // 想表达的意思就是,客户端只能请求相对根目录中的资源,其他地方就不予理会
    // /../login.html这个路径是在相对根目录之外,不合理
    static bool ValidPath(const std::string &path)
    {
        std::vector subdir;
        Split(path, "/", &subdir);
        int level = 0;
        for (auto &dir : subdir)
        {
            if (dir == "..")
            {
                level--;
                if (level < 0)
                {
                    return false;
                }
                continue;
            }
            level++;
        }
        return true;
    }
};

3.2 HttpRequest 类的实现

这个模块是HTTP请求数据模块,用于保存HTTP请求数据被解析后的各项请求元素信息

先给出一个HTTP请求的例子

GET /index.html HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

根据上面的请求,我们可以设计出如下的成员变量

std::string _method;                                   // 请求方法
std::string _path;                                     // 资源路径
std::string _version;                                  // 协议版本
std::string _body;                                     // 请求正文
std::smatch _matches;                                  // 资源路径的正则提取数据
std::unordered_map _headers; // 头部字段
std::unordered_map _params;  // 查询字符串通常指的是 URL 中的查询参数

然后是对其中的成员函数的解释

关于对这个类的初始化,我们默认将同行协议版本设置为HTTP/1.1

HttpRequest()
    : _version("HTTP/1.1")
{}

对类的成员变量进行重置的函数

void ReSet()
{
    _method.clear();
    _path.clear();
    _version = "HTTP/1.1";
    _body.clear();
    std::smatch match;
    _matches.swap(match);
    _headers.clear();
    _params.clear();
}

_headers是一个哈希表,我们设计一个函数SetHeader来插入头部字段

void SetHeader(std::string &key, std::string &val)
{
    _headers.insert(std::make_pair(key, val));
}

HasHeader是用来判断是否存在指定的头部字段,实现它很简单,利用unordered_map中的find即可

bool HasHeader(const std::string &key) const
{
    auto it = _headers.find(key);
    if (it == _headers.end())
    {
        return false;
    }
    return true;
}

Content-Length是头部字段的一种,表示正文长度,通过ContentLength函数来获取正文的长度

size_t ContentLength() const
{
    bool ret = HasHeader("Content-Length");
    if (ret == false)
    {
        return 0;
    }
    std::string clen = GetHeader("Content-Length");
    return std::stol(clen);
}

下面是这个类的全部代码

class HttpRequest
{
public:
    std::string _method;                                   // 请求方法
    std::string _path;                                     // 资源路径
    std::string _version;                                  // 协议版本
    std::string _body;                                     // 请求正文
    std::smatch _matches;                                  // 资源路径的正则提取数据
    std::unordered_map _headers; // 头部字段
    std::unordered_map _params;  // 查询字符串通常指的是 URL 中的查询参数
public:
    HttpRequest()
        : _version("HTTP/1.1")
    {
    }
    void ReSet()
    {
        _method.clear();
        _path.clear();
        _version = "HTTP/1.1";
        _body.clear();
        std::smatch match;
        _matches.swap(match);
        _headers.clear();
        _params.clear();
    }
    // 插入头部字段
    void SetHeader(std::string &key, std::string &val)
    {
        _headers.insert(std::make_pair(key, val));
    }
    // 判断是否存在指定头部字段
    bool HasHeader(const std::string &key) const
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return false;
        }
        return true;
    }
    // 获取指定头部字段的值
    std::string GetHeader(const std::string &key) const
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return "";
        }
        return it->second;
    }
    // 插入查询字符串
    void SetParam(const std::string &key, const std::string &val)
    {
        _params.insert(std::make_pair(key, val));
    }
    // 判断是否有某个指定的查询字符串
    bool HasParam(const std::string &key) const
    {
        auto it = _params.find(key);
        if (it == _params.end())
        {
            return false;
        }
        return true;
    }
    // 获取指定的查询字符串
    std::string GetParam(const std::string &key) const
    {
        auto it = _params.find(key);
        if (it == _params.end())
        {
            return "";
        }
        return it->second;
    }
    // 获取正文长度
    size_t ContentLength() const
    {
        bool ret = HasHeader("Content-Length");
        if (ret == false)
        {
            return 0;
        }
        std::string clen = GetHeader("Content-Length");
        return std::stol(clen);
    }
    // 判断是否为短连接
    bool Close() const
    {
        // 没有Connection字段或者有Connection但是值是false,则都是短连接,否则是长连接
        // DBG_LOG("%d", HasHeader("Connection") == true);
        // DBG_LOG("%d", GetHeader("Connection") == "keep-alive");
        // DBG_LOG("[%s]", GetHeader("Connection").c_str());
        if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
        {
            return false;
        }
        return true;
    }
};

3.3 HttpResponse类的实现

这个模块是HTTP响应数据模块,用于业务处理后设置并保存HTTp响应数据的各项元素信息,最终会被按照HTTP协议响应格式成为响应信息发送给客户端

下面是一个HTTP响应的例子

HTTP/1.1 200 OK
Date: Sun, 18 May 2025 05:37:15 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
Content-Length: 1256
Connection: keep-alive




    Example Page


    

Welcome to the Example Page

This is a simple HTML page returned by the server.

下面的成员变量就是用来保存上面示例报文的信息

int _statu;
bool _redirect_flag;
std::string _body;
std::string _redirect_url;
std::unordered_map _headers;

这里有一个成员变量需要解释一下

_redirect_flag:这是一个表示是否重定向的变量,在Web开发中,HTTP重定向是指服务器返回一个特殊的响应(比如状态码3XX),表示指示客户端访问另一个URL


然后就是对其成员函数的解释


有时候我们需要重定向连接SetRedirect

void SetRedirect(const std::string &url, int statu = 302)
{
    _statu = statu;
    _redirect_flag = true;
    _redirect_url = url;
}

然后就是查看这个链接是长连接还是短连接,我们只需要查看Connection字段,如果这个字段的value值是keep-alive表示是长连接

bool Close()
{
    // 没有Connection字段或者有Connection但是值是false,则都是短连接,否则是长连接
    if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
    {
        return false;
    }
    return true;
}

下面是这个类的全部代码

class HttpResponse
{
public:
    int _statu;
    bool _redirect_flag;
    std::string _body;
    // 重定向
    // 在Web开发中,HTTP 重定向是指服务器返回一个特殊的响应(状态码3xx),
    // 指示客户端(如浏览器)访问另一个URL。
    std::string _redirect_url;
    std::unordered_map _headers;

public:
    HttpResponse()
        : _redirect_flag(false),
          _statu(200)
    {
    }
    HttpResponse(int statu)
        : _redirect_flag(false),
          _statu(statu)
    {
    }
    void ReSet()
    {
        _statu = 200;
        _redirect_flag = false;
        _body.clear();
        _redirect_url.clear();
        _headers.clear();
    }
    // 插入头部字段
    void SetHeader(const std::string &key, const std::string &val)
    {
        _headers.insert(std::make_pair(key, val));
    }
    // 判断是否存在指定头部字段
    bool HasHeader(const std::string &key)
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return false;
        }
        return true;
    }
    // 获取指定头部字段的值
    std::string GetHeader(const std::string &key)
    {
        auto it = _headers.find(key);
        if (it == _headers.end())
        {
            return "";
        }
        return it->second;
    }
    void SetContent(const std::string &body, const std::string &type = "text/html")
    {
        _body = body;
        SetHeader("Content-Type", type);
    }
    void SetRedirect(const std::string &url, int statu = 302)
    {
        _statu = statu;
        _redirect_flag = true;
        _redirect_url = url;
    }
    bool Close()
    {
        // 没有Connection字段或者有Connection但是值是false,则都是短连接,否则是长连接
        if (HasHeader("Connection") == true && GetHeader("Connection") == "keep-alive")
        {
            return false;
        }
        return true;
    }
};

3.4 HttpContext类的实现

这个模块是一个HTTP请求接受的上下文模块,主要是为了防止再一次接收的数据中,不是一个完整的HTTP请求,则解析过程并未完成,无法进行完整的请求处理,需要在下次接受到新数据后继续根据上下文进行解析,最终得到一个HttpRequest请求信息对象,因此在请求数据的接受以及解析部分需要一个上下文来进行控制接收和处理节奏

首先接收对应多种状态码,这里准备了五种

typedef enum
{
    RECV_HTTP_ERROR,
    RECV_HTTP_LINE,
    RECV_HTTP_HEAD,
    RECV_HTTP_BODY,
    RECV_HTTP_OVER
} HttpRecvStatu;

然后就是这个类对应的成员变量

int _resp_statu;           // 相应状态码
HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态
HttpRequest _request;      // 已经解析得到的请求信息

下面是其对应的成员函数


我们需要对请求行进行解析,使用ParseHttpLine函数,传入的参数为string类型的请求行

  1. 使用std::smatch来存储正则表达式匹配结果
  2. 设置正则表达式的匹配规则,前文已经介绍了如何去匹配请求行
  3. 使用regex_match去匹配,把结果存储在matches中
  4. matches[0]存储的是原始字符串,所以获取其请求方法、资源路径、查询字符串、协议版本分别要在matches的1 2 3 4获取
bool ParseHttpLine(const std::string &line)
{
    // std::cout << "string " << line << std::endl;
    std::smatch matches;
    std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:?(.*))? (HTTP/1.[01])(?:
|
)?", std::regex::icase);
    bool ret = std::regex_match(line, matches, e);
    if (ret == false)
    {
        std::cout << line << std::endl;
        // std::cout << "ParseHttpLine 1 error" << std::endl;
        _recv_statu = RECV_HTTP_ERROR;
        _resp_statu = 400;
        return false;
    }
    // 请求方法的获取
    _request._method = matches[1];
    std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
    // 资源路径的获取,需要进行URL解码操作,但是不需要+转空格
    _request._path = Util::UrlDecode(matches[2], false);
    // 协议版本的获取
    _request._version = matches[4];
    // 查询字符串的读取与处理
    std::vector query_string_arry;
    std::string query_string = matches[3];
    // 查询字符串的格式 key=value&key=value... ,先以&符号进行分割,得到各个子串
    Util::Split(query_string, "&", &query_string_arry);
    // 针对各个子串,以 = 符号进行分割,得到key 和 val,得到之后也需要进行URL解码
    for (auto &str : query_string_arry)
    {
        size_t pos = str.find("=");
        if (pos == std::string::npos)
        {
            // 没有找到
            // std::cout << "ParseHttpLine 2 error" << std::endl;
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 400;
            return false;
        }
        std::string key = Util::UrlDecode(str.substr(0, pos), true);
        std::string val = Util::UrlDecode(str.substr(pos + 1), true);
        _request.SetParam(key, val);
    }
    return true;
}

接收请求行的逻辑较为简单,不多赘述

bool RecvHttpLine(Buffer *buf)
{
    if (_recv_statu != RECV_HTTP_LINE)
        return false;
    // 1.获取一行数据
    std::string line = buf->GetLineAndPop();
    // std::cout << line << " " << "RecvHttpLine" << std::endl;
    //  2.需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
    if (line.size() == 0)
    {
        // 缓冲区中的数据不足一行,则需要判断缓冲区中的可读数据长度,如果很长了都不足一行,这是有问题的
        if (buf->ReadAbleSize() > MAX_LINE)
        {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 414; // URI TOO LONG
            return false;
        }
        // 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
        return true;
    }
    if (line.size() > MAX_LINE)
    {
        _recv_statu = RECV_HTTP_ERROR;
        _resp_statu = 414; // URI TOO LONG
        return false;
    }
    bool ret = ParseHttpLine(line);
    if (ret == false)
    {
        return false;
    }
    // buf->MoveReadOffset(line.size());
    _recv_statu = RECV_HTTP_HEAD;
    return true;
    // return ParseHttpLine(line);
}

接受请求头,一行一行地取出key:value类型的数据,直到遇到空行停止

bool RecvHttpHead(Buffer *buf)
{
    if (_recv_statu != RECV_HTTP_HEAD)
        return false;
    // 一行一行取出数据,直到遇到空行为止
    // 并且头部的格式为key:val
key:val

    while (1)
    {
        //  1.获取一行数据
        std::string line = buf->GetLineAndPop();
        // std::cout << "RecvHttpHead " << line << std::endl;
        //  2.需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
        if (line.size() == 0)
        {
            // 缓冲区中的数据不足一行,则需要判断缓冲区中的可读数据长度,如果很长了都不足一行,这是有问题的
            if (buf->ReadAbleSize() > MAX_LINE)
            {
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 414; // URI TOO LONG
                return false;
            }
            // 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
            return true;
        }
        if (line.size() > MAX_LINE)
        {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 414; // URI TOO LONG
            return false;
        }
        if (line == "
" || line == "
")
        {
            break;
        }
        bool ret = ParseHttpHead(line);
        if (ret == false)
        {
            return false;
        }
    }
    _recv_statu = RECV_HTTP_BODY;
    return true;
}

解析请求头,把冒号加空格(: )作为分隔符去找其key和value值,并进行存储

bool ParseHttpHead(std::string &line)
{
    if (line.back() == '
')
        line.pop_back(); // 末尾是换行,则去掉
    if (line.back() == '
')
        line.pop_back(); // 有回车,则去掉
    size_t pos = line.find(": ");
    if (pos == std::string::npos)
    {
        // std::cout << "ParseHttpHead error" << std::endl;
        _recv_statu = RECV_HTTP_ERROR;
        _resp_statu = 400;
        return false;
    }
    std::string key = line.substr(0, pos);
    std::string val = line.substr(pos + 2);
    _request.SetHeader(key, val);
    return true;
}

接收正文部分,主要分为三步

  1. 接收正文长度
  2. 获取当前已经接收了多少正文
  3. 接收正文放到body中
bool ParseHttpHead(std::string &line)
{
    if (line.back() == '
')
        line.pop_back(); // 末尾是换行,则去掉
    if (line.back() == '
')
        line.pop_back(); // 有回车,则去掉
    size_t pos = line.find(": ");
    if (pos == std::string::npos)
    {
        // std::cout << "ParseHttpHead error" << std::endl;
        _recv_statu = RECV_HTTP_ERROR;
        _resp_statu = 400;
        return false;
    }
    std::string key = line.substr(0, pos);
    std::string val = line.substr(pos + 2);
    _request.SetHeader(key, val);
    return true;
}
bool RecvHttpBody(Buffer *buf)
{
    if (_recv_statu != RECV_HTTP_BODY)
        return false;
    // 1.获取正文长度
    size_t content_length = _request.ContentLength();
    if (content_length == 0)
    {
        // 没有正文,则请求接收请求完毕
        _recv_statu = RECV_HTTP_OVER;
        return true;
    }
    // 2.当前已经接收了多少正文,其实就是往_request._body中放了多少数据
    size_t real_len = content_length - _request._body.size(); // 实际还需要接受的正文长度
    // 3.接收正文放到body中,但是也要考虑当前缓冲区中的数据是否是全部的正文
    // 3.1缓冲区中的数据,包含了当前请求的所有正文,取出所需的数据
    if (buf->ReadAbleSize() >= real_len)
    {
        _request._body.append(buf->ReadPosition(), real_len);
        buf->MoveReadOffset(real_len);
        _recv_statu = RECV_HTTP_OVER;
        return true;
    }

    // 3.2缓冲区中的数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来
    _request._body.append(buf->ReadPosition(), buf->ReadAbleSize());
    buf->MoveReadOffset(buf->ReadAbleSize());
    return true;
}

接收并解析Http请求,我们使用RecvHttpRequest函数,然后使用switch关键字,通过状态码来判断做什么事情

void RecvHttpRequest(Buffer *buf)
{
    // buf->TestPrint();
    //  不同的状态,做不同的事情
    //  不需要加入break,完成一步后直接进行下一步
    switch (_recv_statu)
    {
    case RECV_HTTP_LINE:
        RecvHttpLine(buf);
    case RECV_HTTP_HEAD:
        RecvHttpHead(buf);
    case RECV_HTTP_BODY:
        RecvHttpBody(buf);
    }
    return;
}

下面是全部代码

typedef enum
{
    RECV_HTTP_ERROR,
    RECV_HTTP_LINE,
    RECV_HTTP_HEAD,
    RECV_HTTP_BODY,
    RECV_HTTP_OVER
} HttpRecvStatu;

#define MAX_LINE 8192 // 通常设置为8kb
class HttpContext
{
private:
    int _resp_statu;           // 相应状态码
    HttpRecvStatu _recv_statu; // 当前接收及解析的阶段状态
    HttpRequest _request;      // 已经解析得到的请求信息
private:
    // 解析请求行
    bool ParseHttpLine(const std::string &line)
    {
        // std::cout << "string " << line << std::endl;
        std::smatch matches;
        std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:?(.*))? (HTTP/1.[01])(?:
|
)?", std::regex::icase);
        bool ret = std::regex_match(line, matches, e);
        if (ret == false)
        {
            std::cout << line << std::endl;
            // std::cout << "ParseHttpLine 1 error" << std::endl;
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 400;
            return false;
        }
        // 请求方法的获取
        _request._method = matches[1];
        std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper);
        // 资源路径的获取,需要进行URL解码操作,但是不需要+转空格
        _request._path = Util::UrlDecode(matches[2], false);
        // 协议版本的获取
        _request._version = matches[4];
        // 查询字符串的读取与处理
        std::vector query_string_arry;
        std::string query_string = matches[3];
        // 查询字符串的格式 key=value&key=value... ,先以&符号进行分割,得到各个子串
        Util::Split(query_string, "&", &query_string_arry);
        // 针对各个子串,以 = 符号进行分割,得到key 和 val,得到之后也需要进行URL解码
        for (auto &str : query_string_arry)
        {
            size_t pos = str.find("=");
            if (pos == std::string::npos)
            {
                // 没有找到
                // std::cout << "ParseHttpLine 2 error" << std::endl;
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 400;
                return false;
            }
            std::string key = Util::UrlDecode(str.substr(0, pos), true);
            std::string val = Util::UrlDecode(str.substr(pos + 1), true);
            _request.SetParam(key, val);
        }
        return true;
    }
    // 接收请求行
    bool RecvHttpLine(Buffer *buf)
    {
        if (_recv_statu != RECV_HTTP_LINE)
            return false;
        // 1.获取一行数据
        std::string line = buf->GetLineAndPop();
        // std::cout << line << " " << "RecvHttpLine" << std::endl;
        //  2.需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
        if (line.size() == 0)
        {
            // 缓冲区中的数据不足一行,则需要判断缓冲区中的可读数据长度,如果很长了都不足一行,这是有问题的
            if (buf->ReadAbleSize() > MAX_LINE)
            {
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 414; // URI TOO LONG
                return false;
            }
            // 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
            return true;
        }
        if (line.size() > MAX_LINE)
        {
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 414; // URI TOO LONG
            return false;
        }
        bool ret = ParseHttpLine(line);
        if (ret == false)
        {
            return false;
        }
        // buf->MoveReadOffset(line.size());
        _recv_statu = RECV_HTTP_HEAD;
        return true;
        // return ParseHttpLine(line);
    }
    bool RecvHttpHead(Buffer *buf)
    {
        if (_recv_statu != RECV_HTTP_HEAD)
            return false;
        // 一行一行取出数据,直到遇到空行为止
        // 并且头部的格式为key:val
key:val

        while (1)
        {
            //  1.获取一行数据
            std::string line = buf->GetLineAndPop();
            // std::cout << "RecvHttpHead " << line << std::endl;
            //  2.需要考虑的一些要素:缓冲区中的数据不足一行,获取的一行数据超大
            if (line.size() == 0)
            {
                // 缓冲区中的数据不足一行,则需要判断缓冲区中的可读数据长度,如果很长了都不足一行,这是有问题的
                if (buf->ReadAbleSize() > MAX_LINE)
                {
                    _recv_statu = RECV_HTTP_ERROR;
                    _resp_statu = 414; // URI TOO LONG
                    return false;
                }
                // 缓冲区中数据不足一行,但是也不多,就等等新数据的到来
                return true;
            }
            if (line.size() > MAX_LINE)
            {
                _recv_statu = RECV_HTTP_ERROR;
                _resp_statu = 414; // URI TOO LONG
                return false;
            }
            if (line == "
" || line == "
")
            {
                break;
            }
            bool ret = ParseHttpHead(line);
            if (ret == false)
            {
                return false;
            }
        }
        _recv_statu = RECV_HTTP_BODY;
        return true;
    }
    bool ParseHttpHead(std::string &line)
    {
        if (line.back() == '
')
            line.pop_back(); // 末尾是换行,则去掉
        if (line.back() == '
')
            line.pop_back(); // 有回车,则去掉
        size_t pos = line.find(": ");
        if (pos == std::string::npos)
        {
            // std::cout << "ParseHttpHead error" << std::endl;
            _recv_statu = RECV_HTTP_ERROR;
            _resp_statu = 400;
            return false;
        }
        std::string key = line.substr(0, pos);
        std::string val = line.substr(pos + 2);
        _request.SetHeader(key, val);
        return true;
    }
    bool RecvHttpBody(Buffer *buf)
    {
        if (_recv_statu != RECV_HTTP_BODY)
            return false;
        // 1.获取正文长度
        size_t content_length = _request.ContentLength();
        if (content_length == 0)
        {
            // 没有正文,则请求接收请求完毕
            _recv_statu = RECV_HTTP_OVER;
            return true;
        }
        // 2.当前已经接收了多少正文,其实就是往_request._body中放了多少数据
        size_t real_len = content_length - _request._body.size(); // 实际还需要接受的正文长度
        // 3.接收正文放到body中,但是也要考虑当前缓冲区中的数据是否是全部的正文
        // 3.1缓冲区中的数据,包含了当前请求的所有正文,取出所需的数据
        if (buf->ReadAbleSize() >= real_len)
        {
            _request._body.append(buf->ReadPosition(), real_len);
            buf->MoveReadOffset(real_len);
            _recv_statu = RECV_HTTP_OVER;
            return true;
        }

        // 3.2缓冲区中的数据,无法满足当前正文的需要,数据不足,取出数据,然后等待新数据到来
        _request._body.append(buf->ReadPosition(), buf->ReadAbleSize());
        buf->MoveReadOffset(buf->ReadAbleSize());
        return true;
    }

public:
    HttpContext()
        : _resp_statu(200),
          _recv_statu(RECV_HTTP_LINE)
    {
    }
    void ReSet()
    {
        _resp_statu = 200;
        _recv_statu = RECV_HTTP_LINE;
        _request.ReSet();
    }
    int RespStatu()
    {
        return _resp_statu;
    }
    HttpRecvStatu RecvStatu()
    {
        return _recv_statu;
    }
    HttpRequest &Request()
    {
        return _request;
    }
    // 接收并解析Http请求
    void RecvHttpRequest(Buffer *buf)
    {
        // buf->TestPrint();
        //  不同的状态,做不同的事情
        //  不需要加入break,完成一步后直接进行下一步
        switch (_recv_statu)
        {
        case RECV_HTTP_LINE:
            RecvHttpLine(buf);
        case RECV_HTTP_HEAD:
            RecvHttpHead(buf);
        case RECV_HTTP_BODY:
            RecvHttpBody(buf);
        }
        return;
    }
};

3.5 HttpServer类的实现

  • 这个模块时最终给组件使用者提供的HTTP服务器模块,用简单的接口实现HTTP服务器的搭建
  • HttpServer模块内部包含有一个TcpServer对象:TcpServer对象实现服务器的搭建
  • HttpServer模块内部包含有两个提供给TcpServer对象的接口:连接建立成功设置上下文接口,数据处理接口
  • HttpServer模块内部包含有一个hash-map表存储请求于处理函数的映射表:组件使用者向HttpServer设置那些请求应该使用那些函数进行处理,等TcpServer收到对应的请求就会使用对应的函数进行实现

首先是其成员变量

using Handler = std::function;
using Handlers = std::vector>;
Handlers _get_route;
Handlers _post_route;
Handlers _put_route;
Handlers _delete_route;
std::string _basedir; // 静态资源根目录
TcpServer _server;

然后就是其成员函数


关于对错误的处理ErrorHandler,我们使用简单的Html写一个页面放到string中,然后放入rsp即可

void ErrorHandler(const HttpRequest &req, HttpResponse *rsp)
{
    // 1.组织一个错误展示页面
    std::string body;
    body += "";
    body += "";
    body += "";
    body += "";
    body += "";
    body += "

"; body += std::to_string(rsp->_statu); body += " "; body += Util::StatuDesc(rsp->_statu); body += "

"; body += ""; body += ""; // 2.将页面数据,当做响应正文,放入rsp中 rsp->SetContent(body, "text/html"); }

将HttpResponse中的要素按照http协议格式进行组织发送

void WriteResponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp)
{
    // 1.先完善头部字段
    // req是const,调用的Close也必须是const类型
    if (req.Close() == true)
    {
        rsp.SetHeader("Connection", "close");
    }
    else
    {
        rsp.SetHeader("Connection", "keep-alive");
    }
    if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false)
    {
        rsp.SetHeader("Content-Length", std::to_string(rsp._body.size()));
    }
    if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false)
    {
        rsp.SetHeader("Content-Type", "application/octet-stream"); // 二进制流
    }
    if (rsp._redirect_flag == true)
    {
        rsp.SetHeader("Location", rsp._redirect_url);
    }
    // 2.将rsp的要素,按照http协议进行组织
    std::stringstream rsp_str;
    rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << "
";
    for (auto &head : rsp._headers)
    {
        rsp_str << head.first << ":" << head.second << "
";
    }
    rsp_str << "
";
    rsp_str << rsp._body;
    // 3.发送数据
    conn->Send(rsp_str.str().c_str(), rsp_str.str().size());
}

判断是否为静态资源,分为四个步骤

bool IsFileHandler(const HttpRequest &req)
{
    // 1.必须设置了静态资源根目录
    if (_basedir.empty())
    {
        return false;
    }
    // 2.请求方法必须是GET或者HEAD
    if (req._method != "GET" && req._method != "HEAD")
    {
        return false;
    }
    // 3.请求的资源路径必须是一个合法路径
    if (Util::ValidPath(req._path) == false)
    {
        return false;
    }
    // 4.请求的资源必须存在
    // 直接请求根目录是比较特殊的,此时直接追加到首页
    // 不要忘记前缀的相对根目录
    std::string req_path = _basedir + req._path; // 避免直接修改请求的资源路径
    if (req_path.back() == '/')
    {
        req_path += "index.html";
    }
    if (Util::IsRegular(req_path) == false)
    {
        return false;
    }
    // req._path = req_path; //如果请求就是静态资源请求,则有可能需要追加index.html
    return true;
}

静态资源请求处理函数,将静态资源文件的数据读取出来,放到rsp的_body,并设置mime

void FileHandler(const HttpRequest &req, HttpResponse *rsp)
{
    std::string req_path = _basedir + req._path; // 避免直接修改请求的资源路径
    if (req_path.back() == '/')
    {
        req_path += "index.html";
    }
    bool ret = Util::ReadFile(req_path, &rsp->_body);
    if (ret == false)
    {
        return;
    }
    std::string mime = Util::ExtMime(req_path);
    rsp->SetHeader("Content-Type", mime);
    return;
}

请求当然也有功能性请求

void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers)
{
    // 在对应请求方法的路由表中,查找是哦否含有对应资源请求的处理函数,有则调用,没有则返回404
    // 思想:路由表存储的是键值对 -- 正则表达式 & 处理函数
    // 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就是有对应函数进行处理
    for (auto &handler : handlers)
    {
        const std::regex &re = handler.first;
        const Handler &functor = handler.second;
        // std::cout << "进入了这里" << std::endl;
        // std::cout << "req._path" << req._path << "req._matches" << req._matches[1] << std::endl;
        // std::cout << "req._path" << req._path <_statu = 404;
}

路由查找匹配函数,主要是用来进行查找时静态资源还是动态资源,然后执行对应的处理,如果都不是则返回404

// 路由查找匹配
void Route(HttpRequest &req, HttpResponse *rsp)
{
    // 1.对请求资源进行分辨,是一个静态资源请求,还是一个功能性请求
    // 静态资源请求,则进行静态资源处理
    // 功能性请求,则需要通过几个请求路由表来确定是否有处理函数
    // 既不是静态资源请求,也不是功能性处理请求,则返回404
    if (IsFileHandler(req) == true)
    {
        // 是一个静态资源请求,则进行静态资源请求的处理
        // std::cout << "进入了这里" << std::endl;
        return FileHandler(req, rsp);
    }
    // HEAD与GET类似,只不过不要响应正文
    // std::cout << "req._method" << req._method << std::endl;
    if (req._method == "GET" || req._method == "HEAD")
    {
        // std::cout << "使用了GET调用函数添加处理" << std::endl;
        return Dispatcher(req, rsp, _get_route);
    }
    else if (req._method == "POST")
    {
        return Dispatcher(req, rsp, _post_route);
    }
    else if (req._method == "PUT")
    {
        return Dispatcher(req, rsp, _put_route);
    }
    else if (req._method == "DELETE")
    {
        return Dispatcher(req, rsp, _delete_route);
    }
    rsp->_statu = 405; // METHOD NOT ALLOWED
}

设置上下文的函数OnConnected

void OnConnected(const PtrConnection &conn)
{
    conn->SetContext(HttpContext());
    DBG_LOG("NEW CONNECTION %p", conn.get());
}
// 缓冲区数据解析 + 处理
void OnMessage(const PtrConnection &conn, Buffer *buffer)
{
    while (buffer->ReadAbleSize() > 0) // 有数据就继续处理
    {
        // 1.获取上下文
        HttpContext *context = conn->GetContext()->get();
        // 2.通过上下文对缓冲区数据进行解析,得到HttpRequest对象
        // 1.如果缓冲区的数据解析出错,就直接回复出错响应
        // 2.如果解析正常,且请求已经获取完毕,才开始去进行处理
        context->RecvHttpRequest(buffer);
        HttpRequest &req = context->Request();
        HttpResponse rsp(context->RespStatu());
        if (context->RespStatu() >= 400)
        {
            // 出错了,关闭连接
            ErrorHandler(req, &rsp);       // 填充一个错误显示页面数据到rsp中
            WriteResponse(conn, req, rsp); // 组织响应发送给客户端

            context->ReSet(); //!!!重要代码行!!!如果不清空状态的话,他的状态码会一直在400多,导致不断给客户端发送出错信息
            buffer->MoveReadOffset(buffer->ReadAbleSize()); //出错了把缓冲区清空不再处理

            conn->Shutdown(); // 关闭连接
            return;
        }
        if (context->RecvStatu() != RECV_HTTP_OVER)
        {
            // 当前请求还没有接受完整,则退出,等到新数据到来再重新处理
            return;
        }

        // 3.请求路由 + 业务处理
        Route(req, &rsp);
        // 4.对HttpResponse进行组织发送
        //DBG_LOG("%s", rsp._body.c_str());
        WriteResponse(conn, req, rsp);
        // 5.重置上下文
        context->ReSet();
        // 5.根据长短连接判断是否连接或者继续处理
        if (rsp.Close() == true) // 短连接
        {
            conn->Shutdown();
        }
    }
    return;
}

下面是这个类的完整代码

class HttpServer
{
public:
    using Handler = std::function;
    using Handlers = std::vector>;
    Handlers _get_route;
    Handlers _post_route;
    Handlers _put_route;
    Handlers _delete_route;
    std::string _basedir; // 静态资源根目录
    TcpServer _server;

private:
    void ErrorHandler(const HttpRequest &req, HttpResponse *rsp)
    {
        // 1.组织一个错误展示页面
        std::string body;
        body += "";
        body += "";
        body += "";
        body += "";
        body += "";
        body += "

"; body += std::to_string(rsp->_statu); body += " "; body += Util::StatuDesc(rsp->_statu); body += "

"; body += ""; body += ""; // 2.将页面数据,当做响应正文,放入rsp中 rsp->SetContent(body, "text/html"); } // 将HttpResponse中的要素按照http协议格式进行组织,发送 void WriteResponse(const PtrConnection &conn, const HttpRequest &req, HttpResponse &rsp) { // 1.先完善头部字段 // req是const,调用的Close也必须是const类型 if (req.Close() == true) { rsp.SetHeader("Connection", "close"); } else { rsp.SetHeader("Connection", "keep-alive"); } if (rsp._body.empty() == false && rsp.HasHeader("Content-Length") == false) { rsp.SetHeader("Content-Length", std::to_string(rsp._body.size())); } if (rsp._body.empty() == false && rsp.HasHeader("Content-Type") == false) { rsp.SetHeader("Content-Type", "application/octet-stream"); // 二进制流 } if (rsp._redirect_flag == true) { rsp.SetHeader("Location", rsp._redirect_url); } // 2.将rsp的要素,按照http协议进行组织 std::stringstream rsp_str; rsp_str << req._version << " " << std::to_string(rsp._statu) << " " << Util::StatuDesc(rsp._statu) << " "; for (auto &head : rsp._headers) { rsp_str << head.first << ":" << head.second << " "; } rsp_str << " "; rsp_str << rsp._body; // 3.发送数据 conn->Send(rsp_str.str().c_str(), rsp_str.str().size()); } // 判断是否为静态资源 bool IsFileHandler(const HttpRequest &req) { // 1.必须设置了静态资源根目录 if (_basedir.empty()) { return false; } // 2.请求方法必须是GET或者HEAD if (req._method != "GET" && req._method != "HEAD") { return false; } // 3.请求的资源路径必须是一个合法路径 if (Util::ValidPath(req._path) == false) { return false; } // 4.请求的资源必须存在 // 直接请求根目录是比较特殊的,此时直接追加到首页 // 不要忘记前缀的相对根目录 std::string req_path = _basedir + req._path; // 避免直接修改请求的资源路径 if (req_path.back() == '/') { req_path += "index.html"; } if (Util::IsRegular(req_path) == false) { return false; } // req._path = req_path; //如果请求就是静态资源请求,则有可能需要追加index.html return true; } // 静态资源请求处理 -- 将静态资源文件的数据读取出来,放到rsp的_body,并设置mime void FileHandler(const HttpRequest &req, HttpResponse *rsp) { std::string req_path = _basedir + req._path; // 避免直接修改请求的资源路径 if (req_path.back() == '/') { req_path += "index.html"; } bool ret = Util::ReadFile(req_path, &rsp->_body); if (ret == false) { return; } std::string mime = Util::ExtMime(req_path); rsp->SetHeader("Content-Type", mime); return; } // 功能性请求的分类处理 void Dispatcher(HttpRequest &req, HttpResponse *rsp, Handlers &handlers) { // 在对应请求方法的路由表中,查找是哦否含有对应资源请求的处理函数,有则调用,没有则返回404 // 思想:路由表存储的是键值对 -- 正则表达式 & 处理函数 // 使用正则表达式,对请求的资源路径进行正则匹配,匹配成功就是有对应函数进行处理 for (auto &handler : handlers) { const std::regex &re = handler.first; const Handler &functor = handler.second; // std::cout << "进入了这里" << std::endl; // std::cout << "req._path" << req._path << "req._matches" << req._matches[1] << std::endl; // std::cout << "req._path" << req._path <_statu = 404; } // 路由查找匹配 void Route(HttpRequest &req, HttpResponse *rsp) { // 1.对请求资源进行分辨,是一个静态资源请求,还是一个功能性请求 // 静态资源请求,则进行静态资源处理 // 功能性请求,则需要通过几个请求路由表来确定是否有处理函数 // 既不是静态资源请求,也不是功能性处理请求,则返回404 if (IsFileHandler(req) == true) { // 是一个静态资源请求,则进行静态资源请求的处理 // std::cout << "进入了这里" << std::endl; return FileHandler(req, rsp); } // HEAD与GET类似,只不过不要响应正文 // std::cout << "req._method" << req._method << std::endl; if (req._method == "GET" || req._method == "HEAD") { // std::cout << "使用了GET调用函数添加处理" << std::endl; return Dispatcher(req, rsp, _get_route); } else if (req._method == "POST") { return Dispatcher(req, rsp, _post_route); } else if (req._method == "PUT") { return Dispatcher(req, rsp, _put_route); } else if (req._method == "DELETE") { return Dispatcher(req, rsp, _delete_route); } rsp->_statu = 405; // METHOD NOT ALLOWED } // 设置上下文 void OnConnected(const PtrConnection &conn) { conn->SetContext(HttpContext()); DBG_LOG("NEW CONNECTION %p", conn.get()); } // 缓冲区数据解析 + 处理 void OnMessage(const PtrConnection &conn, Buffer *buffer) { while (buffer->ReadAbleSize() > 0) // 有数据就继续处理 { // 1.获取上下文 HttpContext *context = conn->GetContext()->get(); // 2.通过上下文对缓冲区数据进行解析,得到HttpRequest对象 // 1.如果缓冲区的数据解析出错,就直接回复出错响应 // 2.如果解析正常,且请求已经获取完毕,才开始去进行处理 context->RecvHttpRequest(buffer); HttpRequest &req = context->Request(); HttpResponse rsp(context->RespStatu()); if (context->RespStatu() >= 400) { // 出错了,关闭连接 ErrorHandler(req, &rsp); // 填充一个错误显示页面数据到rsp中 WriteResponse(conn, req, rsp); // 组织响应发送给客户端 context->ReSet(); //!!!重要代码行!!!如果不清空状态的话,他的状态码会一直在400多,导致不断给客户端发送出错信息 buffer->MoveReadOffset(buffer->ReadAbleSize()); //出错了把缓冲区清空不再处理 conn->Shutdown(); // 关闭连接 return; } if (context->RecvStatu() != RECV_HTTP_OVER) { // 当前请求还没有接受完整,则退出,等到新数据到来再重新处理 return; } // 3.请求路由 + 业务处理 Route(req, &rsp); // 4.对HttpResponse进行组织发送 //DBG_LOG("%s", rsp._body.c_str()); WriteResponse(conn, req, rsp); // 5.重置上下文 context->ReSet(); // 5.根据长短连接判断是否连接或者继续处理 if (rsp.Close() == true) // 短连接 { conn->Shutdown(); } } return; } public: HttpServer(int port, int timeout = DEFAULT_TIMEOUT) : _server(port) { _server.EnableInactiveRelease(timeout); _server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1)); _server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2)); } void SetBaseDir(const std::string &path) { int ret = Util::IsDirectory(path); assert(ret == true); _basedir = path; } /*设置/添加请求(请求的正则表达式)与处理函数的映射关系*/ void Get(const std::string &patten, const Handler &handler) { _get_route.push_back(std::make_pair(std::regex(patten), handler)); } void Post(const std::string &patten, const Handler &handler) { _post_route.push_back(std::make_pair(std::regex(patten), handler)); } void Put(const std::string &patten, const Handler &handler) { _put_route.push_back(std::make_pair(std::regex(patten), handler)); } void Delete(const std::string &patten, const Handler &handler) { _delete_route.push_back(std::make_pair(std::regex(patten), handler)); } void SetThreadCount(int count) { _server.SetThreadCount(count); } void Listen() { _server.Start(); } };

4.结语

代码连接如下lesson41 · 张家兴/Linux - 码云 - 开源中国

有需要自取,博客不精准地方请多谅解

本文地址:https://www.vps345.com/12200.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 docker 容器 运维 java-rabbitmq java 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP ubuntu 无人机 机器人 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 人工智能 自动化 centos python linux 机器学习 服务器 深度学习 macos MacMini Mac 迷你主机 mini Apple javascript 前端 chrome edge ai nlp Deepseek Deepseek-R1 大模型 私有化部署 推理模型 数据库 oracle 关系型 安全 分布式 ddos android 网络安全 web安全 网络工程师 网络管理 软考 2024 2024年上半年 下午真题 答案 ROS 自动驾驶 ssh jvm 虚拟机 windows 游戏 harmonyos 华为 计算机外设 bug 物联网 redis 云计算 ssh漏洞 ssh9.9p2 CVE-2025-23419 AIGC 人工智能生成内容 Dify FTP服务器 deepseek DeepSeek-R1 API接口 http 数据库系统 网络 ssl django https c++ conda 开发语言 vscode 音视频 程序 编程 内存 性能分析 CH340 单片机 嵌入式硬件 串口驱动 CH341 uart 485 DNS 经验分享 react.js 前端面试题 node.js 持续部署 stm32 神经网络 计算机视觉 卷积神经网络 vite vue3 YOLO 目标检测 udp 语言模型 excel 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 github git vue.js spring boot nginx 自然语言处理 华为云 华为od arm开发 架构 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 notepad pycharm ide gnu 缓存 权限 学习 gitee tcp/ip 网络协议 ip协议 kubernetes prometheus grafana 云原生 负载均衡 ui opengl 游戏引擎 k8s ansible pytorch yolov5 镜像源 远程桌面 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 Ollama 模型联网 API CherryStudio 学习方法 程序人生 rust腐蚀 iventoy VmWare OpenEuler mcu c语言 kafka flutter Google pay Apple pay uni-app llama deepseek-r1 大模型本地部署 进程 操作系统 进程控制 Ubuntu 笔记 C 环境变量 进程地址空间 wsl USB转串口 Qwen2.5-coder 离线部署 linux环境变量 opencv 数据分析 svn mysql 远程工作 Ubuntu20.04 GLIBC 2.35 php gcc centos 7 7z eureka dify 知识库 本地化部署 tomcat Alexnet qt arm dash json 正则表达式 springsecurity6 oauth2 授权服务器 前后端分离 jdk 后端 智能体开发 AI gitlab unity 嵌入式 debian unix 部署 RTSP xop RTP RTSPServer 推流 视频 audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 gitea npm rsyslog golang go file server http server web server IIS服务器 IIS性能 日志监控 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 oracle fusion oracle中间件 MCP 性能优化 电脑 NVML nvidia-smi mcp Cursor adb ubuntu20.04 Linux 开机黑屏 UEFI Legacy MBR GPT U盘安装操作系统 本地环回 bind 命令 AI编程 maxkb ARG Linux 维护模式 ArkUI 鸿蒙系统 ArkTS WSL2 IP 地址 pygame 策略模式 eclipse iftop 网络流量监控 vnc Python Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 ip DevOps 大数据 软件交付 数据驱动 应用场景 数据安全 devops apache spring cloud compose nvm 微服务 阿里云 腾讯云 vue 防火墙 端口号 开放端口 访问列表 ollama 压力测试 大模型压力测试 EvalScope ACL 流量控制 基本ACL 规则配置 算法 回显服务器 UDP的API使用 小程序 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 实时音视频 RAID RAID技术 磁盘 存储 cursor 系统安全 串口服务器 kylin websocket 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 jar microsoft mysql离线安装 ubuntu22.04 mysql8.0 typescript 计算机网络 mac 私有化 本地部署 鸿蒙 网络结构图 面试 vim hadoop 并查集 leetcode 英语 开发环境 list 数据结构 jellyfin nas python2 ubuntu24.04 智能路由器 intellij-idea 智能手机 open webui 移动端开发 gpu算力 pip arkUI arkTs 1024程序员节 nohup 异步执行 MobaXterm 远程连接 文件传输 elasticsearch 快捷键 旋转屏幕 自动操作 bash zip unzip cuda webpack selenium firewall perf 大模型应用 virtualenv webdav 指令 ffmpeg 视频编解码 ubuntu 18.04 安装教程 虚拟现实 国产数据库 瀚高数据库 数据迁移 下载安装 spring 多线程 axure 富文本编辑器 ros ros1 Noetic 20.04 apt 安装 cron crontab日志 fstab 开源 milvus centos-root /dev/mapper yum clean all df -h / du -sh JDK Java LInux Windows 微信 k8s部署 MySQL8.0 高可用集群(1主2从) 命名管道 客户端与服务端通信 信息与通信 agi mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 dell服务器 ipython react next.js 部署next.js 服务器配置 生物信息学 rpc YOLOv8 NPU Atlas800 A300I pro asi_bench filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 IMM 反向代理 adobe PVE chatgpt asm jupyter 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 vu大文件秒传跨域报错cors 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 职场和发展 镜像下载 freebsd DeepSeek spark hive outlook 错误代码2603 无网络连接 2603 c# asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 es6 qt6.3 g726 交互 云计算面试题 多层架构 解耦 灵办AI Redis Desktop 虚拟显示器 远程控制 jenkins 高级IO epoll openvpn server openvpn配置教程 centos安装openvpn visual studio code vmware 卡死 matplotlib fonts-noto-cjk 编辑器 QT 5.12.12 QT开发环境 Ubuntu18.04 shell脚本免交互 expect linux免交互 隐藏文件 程序员创富 VMware Tools vmware tools安装 vmwaretools安装步骤 vmwaretools安装失败 vmware tool安装步骤 vm tools安装步骤 vm tools安装后不能拖 vmware tools安装步骤 虚拟局域网 教程 环境搭建 Maven linux驱动开发 sql burpsuite 安全工具 mac安全工具 burp安装教程 渗透工具 diskgenius chromium dpi pdf 图像处理 python3.11 冯诺依曼体系 测试工具 Python教程 pycharm安装 tensorflow maven visualstudio Maxkb RAG技术 本地知识库 腾讯云大模型知识引擎 glibc ue4 着色器 ue5 虚幻 爬虫 网络爬虫 目标跟踪 OpenVINO 推理应用 网络药理学 生信 gromacs 分子动力学模拟 MD 动力学模拟 openssl 框架搭建 flash-attention 报错 AI大模型 大模型技术 本地部署大模型 计算生物学 生物信息 基因组 burp suite 抓包 html 工具分享 AI-native gpt CPU 使用率 系统监控工具 linux 命令 qps 高并发 VMware rdp 远程服务 rocketmq SRS 流媒体 直播 C语言 交换机 硬件 设备 GPU PCI-Express 科技 个人开发 自定义客户端 SAS 雨云 NPS oneapi AutoDL live555 rtsp rtp flask xrdp 银河麒麟 环境迁移 fastapi web3.py 大语言模型 LLMs EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 外网访问 内网穿透 端口映射 银河麒麟操作系统 国产化 html5 firefox transformer grub 版本升级 扩容 rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK 媒体 IM即时通讯 QQ 企业微信 剪切板对通 HTML FORMAT webrtc rustdesk jmeter Chatbox HP Anyware 虚拟化 信号处理 tcpdump 硬件工程 华为OD 华为OD机试真题 可以组成网络的服务器 css css3 区块链 密码学 智能合约 哈希算法 shell chatbox Agent 前端框架 big data ECS服务器 HarmonyOS Next 系统 黑苹果 安卓 软件需求 ios Linux PID 产品经理 protobuf 序列化和反序列化 安装 iperf3 带宽测试 PX4 MAVROS 四旋翼无人机 进程优先级 调度队列 进程切换 sublime text 运维开发 linux内核 智慧农业 开源鸿蒙 团队开发 mysql安装报错 windows拒绝安装 .net xcode kotlin android studio iphone GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 iNode Macos 驱动开发 mongodb export env 变量 链表 打不开xxx软件 无法检查其是否包含恶意软件 Ubuntu 22.04 MySql 算家云 算力租赁 photoshop vm 图论 离线部署dify JAVA docker run 数据卷挂载 交互模式 开发 课程设计 Docker Desktop onlyoffice 实时互动 C++ 政务 分布式系统 监控运维 Prometheus Grafana efficientVIT YOLOv8替换主干网络 TOLOv8 LLM CrewAI etcd RBAC 监控k8s集群 集群内prometheus numpy 聚类 微信小程序 miniapp 真机调试 调试 debug 断点 网络API请求调试方法 读写锁 集成学习 集成测试 llm postgresql dubbo 思科 tcp rag ragflow 大模型部署 scikit-learn 图形化界面 5G 网易邮箱大师 Linux Vim tar 一切皆文件 ci/cd 显卡驱动持久化 GPU持久化 RDP n8n 工作流 xml UFW WSL2 上安装 Ubuntu 计算机学习路线 编程语言选择 ranger MySQL8.0 京东云 华为认证 mcp-proxy mcp-inspector fastapi-mcp agent sse FTP 服务器 升级 CVE-2024-7347 漏洞 大模型微调 阻塞队列 生产者消费者模型 服务器崩坏原因 aws ping++ 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 数据挖掘 opcua opcda KEPServer安装 孤岛惊魂4 ESP32 NAS Termux Samba Hyper-V WinRM TrustedHosts vSphere vCenter 软件定义数据中心 sddc 源码剖析 rtsp实现步骤 流媒体开发 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap WebRTC express opensearch helm Netty 即时通信 NIO SSH Unity Dedicated Server Host Client 无头主机 sysctl.conf vm.nr_hugepages 测试用例 功能测试 鲲鹏 火绒安全 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 rust gateway Clion Nova ResharperC++引擎 Centos7 远程开发 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 iot C# MQTTS 双向认证 emqx TCP WebServer 话题通信 服务通信 windows 服务器安装 samba 浪潮信息 AI服务器 lighttpd安装 Ubuntu配置 Windows安装 性能测试 服务器优化 postman Qualcomm WoS QNN AppBuilder ftp服务 文件上传 ECT转485串口服务器 ECT转Modbus485协议 ECT转Modbus串口服务器 锁屏不生效 p2p 昇腾 大模型训练/推理 推理问题 mindie 行情服务器 股票交易 速度慢 切换 股票量化接口 股票API接口 matlab 券商 股票交易接口api 类型 特点 zabbix mariadb 统信 NFS 匿名FTP 邮件传输代理 SSL支持 chroot监狱技术 教育电商 游戏程序 Claude 文件系统 路径解析 宠物 毕业设计 免费学习 宠物领养 宠物平台 webgl AI写作 VMware安装Ubuntu Ubuntu安装k8s MacOS录屏软件 微信分享 Image wxopensdk fpga开发 cnn GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 VMware安装mocOS macOS系统安装 DevEco Studio java-ee conda配置 conda镜像源 键盘 匿名管道 Mac内存不够用怎么办 软件工程 软件构建 信息可视化 echarts yum docker-compose docker compose Reactor Reactor反应堆 隐藏目录 管理器 通配符 安卓模拟器 工具 elk langchain deep learning 桌面环境 nohup后台启动 其他 具身智能 强化学习 HarmonyOS Tabs组件 TabContent TabBar TabsController 导航页签栏 滚动导航栏 frp iTerm2 vmamba wps 终端工具 远程工具 Office Ardupilot 系统架构 sublime text3 IP配置 netplan ollama api ollama外网访问 grep 进程信号 GPU训练 termux openjdk Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 图文教程 VMware虚拟机 macOS系统安装教程 macOS最新版 虚拟机安装macOS Sequoia systemctl composer 创业创新 管道 js 流程图 mermaid 数据集 深度求索 私域 环境配置 SSH 密钥生成 SSH 公钥 私钥 生成 minicom 串口调试工具 firewalld 设计模式 Linux的权限 vr 自动化运维 流水线 脚本式流水线 ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 springboot 实战案例 WebUI DeepSeek V3 CLion IDE 主从复制 ip命令 新增网卡 新增IP 启动网卡 设置代理 实用教程 监控k8s 监控kubernetes powerpoint WLAN edge浏览器 rancher database nftables rabbitmq 单例模式 Ubuntu22.04 开发人员主页 kali 共享文件夹 云原生开发 K8S k8s管理系统 ROS2 Mermaid 可视化图表 自动化生成 3d 数学建模 kernel 电子信息 通信工程 毕业 sdkman 换源 国内源 Debian 免密 登录 公钥 私钥 远程过程调用 Windows环境 宝塔面板无法访问 ufw 硬件架构 计算机科学与技术 remote-ssh 进程程序替换 execl函数 execv函数 execvp函数 execvpe函数 putenv函数 linuxdeployqt 打包部署程序 appimagetool docker desktop 镜像 Kali 渗透 Claude Desktop Claude MCP Windows Cli MCP 磁盘挂载 新盘添加 partedUtil gru 桥接模式 windows虚拟机 虚拟机联网 DrissionPage prompt AI提示词优化 SSE 超级终端 多任务操作 提高工作效率 RAGflow dns oceanbase 传统数据库升级 银行 多线程服务器 Linux网络编程 IPMI HistoryServer Spark YARN jobhistory stm32项目 Xterminal MS Materials ruoyi zotero WebDAV 同步失败 代理模式 pillow RustDesk自建服务器 rustdesk服务器 docker rustdesk 常用命令 文本命令 目录命令 NAT转发 NAT Server openEuler 云电竞 云电脑 todesk IIS .net core Hosting Bundle .NET Framework vs2022 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 LDAP VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 MQTT 消息队列 cpu 实时 使用 odoo 服务器动作 Server action 知识图谱 大模型教程 ssrf 失效的访问控制 安全威胁分析 国产操作系统 虚拟机安装 宝塔面板 同步 备份 建站 监控 sqlserver windows日志 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 Kali Linux 黑客 渗透测试 信息收集 磁盘监控 直播推流 大文件秒传跨域报错cors r语言 数据可视化 KingBase 项目部署到linux服务器 项目部署过程 Dell R750XS 群晖 文件分享 java-rocketmq 创意 社区 执法记录仪 智能安全帽 smarteye mybatis playbook 剧本 博客 sqlite3 CDN VMware创建虚拟机 .netcore 矩阵 GCC aarch64 编译安装 HPC 域名服务 DHCP 符号链接 配置 llama.cpp db KylinV10 麒麟操作系统 Vmware CPU 主板 电源 网卡 TrueLicense MQTT协议 消息服务器 代码 云桌面 微软 AD域控 证书服务器 互信 clickhouse 硅基流动 ChatBox CNNs 图像分类 gunicorn 高效I/O 安全漏洞 信息安全 pyside6 界面 web ecmascript 支持向量机 商用密码产品体系 localhost web开发 fpga 源代码管理 医疗APP开发 app开发 僵尸进程 远程 执行 sshpass 操作 linux安装配置 harmonyOS面试题 alias unalias 别名 chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 低代码 ArcTS ArcUI GridItem 中兴光猫 换光猫 网络桥接 自己换光猫 kvm 在线office bcompare Beyond Compare mamba Vmamba Hive环境搭建 hive3环境 Hive远程模式 c keepalived threejs 3D Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 cmos 嵌入式系统开发 SenseVoice safari CentOS Stream CentOS ESXi Ark-TS语言 重启 排查 系统重启 日志 原因 模拟实现 Ubuntu共享文件夹 共享目录 Linux共享文件夹 webstorm c/c++ 串口 系统开发 binder 车载系统 framework 源码环境 强制清理 强制删除 mac废纸篓 okhttp Xinference top Linux top top命令详解 top命令重点 top常用参数 ArkTs uv 沙盒 软链接 硬链接 Linux权限 权限命令 特殊权限 安装MySQL 磁盘清理 IO curl wget 信号 内核 fork wait waitpid exit 华为证书 HarmonyOS认证 华为证书考试 ebpf uprobe 多产物 双系统 跨平台 大屏端 lvm 磁盘分区 宝塔 su sudo sudo原理 su切换 linux上传下载 Isaac Sim 虚拟仿真 wpf 用户管理 yolov8 动静态库 retry 重试机制 高德地图 鸿蒙接入高德地图 HarmonyOS5.0 空间 查错 搜索引擎 设备树 毕设 进程间通信 macbook Linux find grep SecureCRT hdc Bug解决 Qt platform OpenCV mac设置host node 向量数据库 安装部署 milvus安装 CUPS 打印机 Qt5 wsl2 vsxsrv OpenCore Github加速 Mac上Github加速 Chrome浏览器插件 需求分析 提示词 切换root RockyLinux nano AppLinking 应用间跳转 risc-v 蓝耘科技 元生代平台工作流 ComfyUI YOLOv12 Docker Compose fd 文件描述符 eNSP 网络规划 VLAN 企业网络 热榜 workflow 健康医疗 互联网医院 产测工具框架 IMX6ULL 管理框架 中间件 可信计算技术 安全架构 网络攻击模型 3GPP 卫星通信 基础环境 AnythingLLM AnythingLLM安装 Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 openstack Xen KVM DocFlow 拓扑图 seatunnel 网络用户购物行为分析可视化平台 大数据毕业设计 P2P HDLC HiCar CarLife+ CarPlay QT RK3588 k8s集群资源管理 yum源切换 更换国内yum源 软件测试 DenseNet cudnn anaconda docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos Docker Hub docker pull daemon.json ragflow 源码启动 Cline 自动化编程 RAGFLOW ros2 moveit 机器人运动 springcloud log4j rime 飞牛nas fnos docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 mq Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer openwrt WebVM 文心一言 selete Typore wireshark Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 redhat 版本 copilot 程序员 GameFramework HybridCLR Unity编辑器扩展 自动化工具 生活 ubuntu安装 linux入门小白 make命令 makefile文件 esp32 蓝牙 网络编程 WireGuard 异地组网 ajax trae LVM lvresize 磁盘扩容 pvcreate 考试 相机 kubeless autoware rpa Alist rclone mount 挂载 网盘 client-go k8s二次开发 智能体 autogen openai coze linux子系统 忘记密码 Trae叒更新了? Masshunter 质谱采集分析软件 使用教程 科研软件 卸载 软件 列表 Docker Kubernetes 数据采集 Crawlee Playwright hosts hosts文件管理工具 开源软件 UOS 开机自启动 桌面快捷方式 LLaMA-Factory NLP isaacgym 企业风控系统 互联网反欺诈 DDoS攻击 SQL注入攻击 恶意软件和病毒攻击 实验 Trae AI 原生集成开发环境 Trae AI MCP server C/S 王者荣耀 Kylin-Server 服务器安装 嵌入式实习 EMQX 通信协议 华为机试 AD域 H3C VSCode v10 Invalid Host allowedHosts ftp 大模型入门 WSL win11 无法解析服务器的名称或地址 vasp安装 kylin v10 麒麟 v10 intellij idea ocr 做raid 装系统 BMC rc.local 开机自启 systemd 麒麟 单一职责原则 nextjs reactjs unity3d 大模型推理 大模型学习 压测 ECS 多个客户端访问 IO多路复用 TCP相关API 我的世界 我的世界联机 数码 统信操作系统 移动云 kamailio sip VoIP ukui 麒麟kylinos openeuler googlecloud 内网环境 iis X11 Xming 聊天服务器 套接字 客户端 Socket DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 ux cpp-httplib 统信UOS bonding 链路聚合 IPMITOOL 硬件管理 大数据平台 致远OA OA服务器 服务器磁盘扩容 温湿度数据上传到服务器 Arduino HTTP 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 代码调试 ipdb W5500 OLED u8g2 TCP服务器 LORA camera Arduino 半虚拟化 硬件虚拟化 Hypervisor 网站搭建 serv00 视觉检测 田俊楠 毕昇JDK 工业4.0 iBMC UltraISO 服务器管理 配置教程 网站管理 迁移指南 Windsurf gradle 智能电视 安防软件 自动化测试 云耀服务器 浏览器自动化 React Next.js 开源框架 视频平台 录像 视频转发 视频流 机柜 1U 2U Web服务器 多线程下载工具 PYTHON 服务器扩容没有扩容成功 visual studio Carla 智能驾驶 代码规范 录音麦克风权限判断检测 录音功能 录音文件mp3播放 小程序实现录音及播放功能 RecorderManager 解决录音报错播放没声音问题 联网 easyconnect 代理 蓝桥杯C++组 蓝桥杯 arcgis 视频监控 网站 netty 内网渗透 靶机渗透 小智 VPN wireguard 显示器 c/s web3 区块链项目 软件开发 信任链 日志分析 系统取证 海康 word 框架 规格说明书 设计规范 Webserver 异步 技术共享 机架式服务器 1U工控机 国产工控机 计算机系统 shard finebi bigdata gitee go notepad++ issue 多端开发 智慧分发 应用生态 鸿蒙OS av1 电视盒子 机顶盒ROM 魔百盒刷机 小游戏 五子棋 VR手套 数据手套 动捕手套 动捕数据手套 neo4j 数据仓库 数据库开发 deekseek 图形渲染 chrome devtools chromedriver nvidia 小艺 Pura X navicat 单元测试 crosstool-ng 鸿蒙开发 移动开发 元服务 应用上架 命令行 基础入门 sonoma 自动更新 模拟器 RAG 检索增强生成 文档解析 大模型垂直应用 飞牛NAS 飞牛OS MacBook Pro 输入法 Ubuntu Server Ubuntu 22.04.5 OpenHarmony 邮件APP 免费软件 Linux24.04 deepin 混合开发 环境安装 源码 分析解读 prometheus数据采集 prometheus数据模型 prometheus特点 Attention 考研 Linux的基础指令 交叉编译 向日葵 trea idea AList fnOS 历史版本 下载 边缘计算 cocoapods 用户缓冲区 本地部署AI大模型 HarmonyOS NEXT 原生鸿蒙 稳定性 看门狗 支付 微信支付 开放平台 etl can 线程池 查看显卡进程 fuser x64 SIGSEGV xmm0 Cookie MacOS vpn DIFY MAC CosyVoice ELF加载 网络文件系统 cmake Mac软件 perl MVS 海康威视相机 stable diffusion AI作画 xfce g++ g++13 MDK 嵌入式开发工具 论文笔记 pyicu initramfs Linux内核 Grub 电脑桌面出现linux图标 电脑桌面linux图标删除不了 电脑桌面Liunx图标删不掉 linux图标删不掉 gpt-3 推荐算法 zookeeper 玩游戏 实时内核 大版本升 升级Ubuntu系统 llamafactory 微调 Qwen Xshell Echarts图表 折线图 柱状图 异步动态数据 可视化效果 lio-sam SLAM resolv.conf 星河版 NVM Node Yarn PM2 mybase 裸机装机 linux磁盘分区 裸机安装linux 裸机安装ubuntu 裸机安装kali 裸机 材料工程 Charles Obsidian Dataview 可执行程序 react native STL csrf cpolar 欧拉系统 mvc harmonyosnext 顽固图标 启动台 devmem 互联网实用编程指南 appium 树莓派项目 多路转接 npu 技能大赛 ceph GoogLeNet TRAE 影刀 #影刀RPA# minio ollama下载加速 nac 802.1 portal SSH 服务 SSH Server OpenSSH Server RAGFlow 本地知识库部署 DeepSeek R1 模型 jina 跨域 ubuntu24.04.1 yaml Ultralytics 可视化 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm fast 实习 ShenTong AI代码编辑器 线程 金仓数据库 2025 征文 数据库平替用金仓 Open WebUI 办公自动化 pdf教程 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 apt pyautogui cd 目录切换 Logstash 日志采集 ssh远程登录 xpath定位元素 IPv4 子网掩码 公网IP 私有IP bot docker命令大全 浏览器开发 AI浏览器 大模型面经 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 服务器繁忙 ubuntu24 vivado24 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 rnn 序列化反序列化 PPI String Cytoscape CytoHubba linux 命令 sed 命令 自动化任务管理 计算机 springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 显卡驱动 虚幻引擎 searxng dity make 金融 telnet 远程登录 Node-Red 编程工具 流编程 GRUB引导 Linux技巧 easyui 论文阅读 qemu libvirt 网络建设与运维 网络搭建 神州数码 神州数码云平台 云平台 免费域名 域名解析 OpenManus 显示过滤器 ICMP Wireshark安装 容器技术 钉钉 DeepSeek r1 cfssl 自学笔记 小米 澎湃OS Android 端口聚合 windows11 System V共享内存 进程通信 DBeaver virtualbox NLP模型 电视剧收视率分析与可视化平台 lb 协议 import save load 迁移镜像 图片增强 增强数据 scapy Helm k8s集群 Docker快速入门 netlink libnl3 Apache Beam 批流统一 案例展示 数据分区 容错机制 软件卸载 系统清理 ShapeFile GeoJSON Nginx GeneCards OMIM TTD 底层实现 分子对接 autodock mgltools PDB PubChem fiddler 音乐服务器 Navidrome 音流 全文检索 图搜索算法 内存管理 EVE-NG GRE IPv4/IPv6双栈 双栈技术 网路规划设计 ensp综合实验 IPv4过渡IPv6 IPv4与IPv6 postgres Dify重启后重新初始化 rtc pthread Qwen3 qwen3 32b vllm 可用性测试 d3d12 android-studio 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? deepseek-v3 ktransformers RagFlow fabric 集成 python高级编程 Ansible elk stack 网卡的名称修改 eth0 ens33 物联网开发 kind 云服务 vscode1.86 1.86版本 ssh远程连接 个人博客 armbian u-boot LLM Web APP Streamlit 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 junit HCIE 数通 豆瓣 追剧助手 迅雷 云服务器 hibernate 游戏机 pyqt 游戏服务器 TrinityCore 魔兽世界 恒源云 Wi-Fi bootstrap 飞书 银河麒麟服务器操作系统 系统激活 深度优先 并集查找 换根法 树上倍增 无桌面 XFS xfs文件系统损坏 I_O error SSL 域名 Spring Security MNN vscode 1.86 api VPS SEO 社交电子 iDRAC R720xd 直流充电桩 充电桩 相差8小时 UTC 时间 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 token sas Nuxt.js nuxt3 服务器部署ai模型 llama3 Chatglm 开源大模型 三级等保 服务器审计日志备份 远程看看 远程协助 显示管理器 lightdm gdm 模拟退火算法 gaussdb 弹性计算 裸金属服务器 弹性裸金属服务器 FunASR ASR mosquitto DOIT 四博智联 业界资讯 思科模拟器 Cisco asp.net上传文件夹 asp.net上传大文件 .net core断点续传 能力提升 面试宝典 技术 IT信息化 算力 AISphereButler Minecraft 网络穿透 SWAT 配置文件 服务管理 网络共享 加解密 Yakit yaklang 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 chfs ubuntu 16.04 状态管理的 UDP 服务器 Arduino RTOS micropython mqtt wsgiref Web 服务器网关接口 SysBench 基准测试 服务器主板 AI芯片 skynet Dell HPE 联想 浪潮 XCC Lenovo idm embedding 树莓派 VNC .net mvc断点续传 OD机试真题 服务器能耗统计 繁忙 解决办法 替代网站 汇总推荐 AI推理 pgpool lua 微信开放平台 微信公众平台 微信公众号配置 智能音箱 智能家居 音乐库 飞牛 dba 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 超融合 端口测试 less Jellyfin UOS1070e saltstack 代码托管服务 banner powerbi WINCC Web应用服务器 服务器部署 本地拉取打包 联机 僵尸毁灭工程 游戏联机 开服 语音识别 zerotier 服务器ssl异常解决 弹性服务器 笔灵AI AI工具 Pyppeteer nacos 配置原理 glm4 负载测试 deepseak 豆包 KIMI 腾讯元宝 充电桩平台 充电桩开源平台 solidworks安装 umeditor粘贴word ueditor粘贴word ueditor复制word ueditor上传word图片 ueditor导入word ueditor导入pdf ueditor导入ppt 电路仿真 multisim 硬件工程师 硬件工程师学习 电路图 电路分析 仪器仪表 PyQt PySide6 laravel AimRT wordpress java-zookeeper 飞腾处理器 文件存储服务器组件 Python 视频爬取教程 Python 视频爬取 Python 视频教程 RK3568 paddle rsync qwen2vl Putty 花生壳 OpenSSH 程序化交易 PTrade QMT 量化交易 量化股票 servlet OpenManage 概率论 vue在线预览excel和编辑 vue2打开解析xls电子表格 浏览器新开页签或弹框内加载预览 文件url地址或接口二进制文档 解决网页打不开白屏报错问题 静态NAT linq 分布式数据库 集中式数据库 业务需求 选型误 GPU状态 粘包问题 语法 移动魔百盒 服务网格 istio 捆绑 链接 谷歌浏览器 youtube google gmail sequoiaDB deepseek r1 regedit 开机启动 玩机技巧 软件分享 软件图标 EMUI 回退 降级 风扇控制软件 嵌入式Linux IPC ruby xshell termius iterm2 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 Linux环境 代理服务器 ArtTS 我的世界服务器搭建 minecraft 极限编程 IO模型 csrutil mac恢复模式进入方法 SIP 恢复模式 终端 archlinux kde plasma beautifulsoup 子系统 brew comfyui comfyui教程 物理地址 页表 虚拟地址 对比 meld DiffMerge win向maOS迁移数据 Linux的基础开发工具 接口返回 导航栏 swift miniconda Bandizip Mac解压 Mac压缩 压缩菜单 SPI 端口开放 lvs GKI KMI IP地址 IPv6 计算机基础 yum换源 系统内核 Linux版本 function address 函数 地址 Bluetooth 配对 broadcom MySQL direct12 libreoffice word转pdf IMX317 MIPI H265 VCU 企业网络规划 华为eNSP Python基础 Python技巧 kerberos TCP协议 USB网络共享 运维监控 docker部署Python docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 多进程 抗锯齿 烟花代码 烟花 元旦 增强现实 沉浸式体验 技术实现 案例分析 AR dock 加速 bat 软负载 AI Agent 字节智能运维 seleium 端口 查看 ss 项目部署 proxy模式 deployment daemonset statefulset cronjob 问题解决 性能调优 安全代理 抓包工具 image Sealos 集群管理 网页服务器 web服务器 AzureDataStudio qt5 客户端开发 Zoertier 内网组网 VGG网络 卷积层 池化层 SystemV 机械臂 IPv6测试 IPv6测速 IPv6检测 IPv6查询 脚本 post.io 企业邮箱 搭建邮箱 静态IP 命令模式 软考设计师 中级设计师 SQL 软件设计师 源代码 watchtower 线程同步 线程互斥 条件变量 finalsheel MinIO SoC 医院门诊管理系统 轮播图 MLLMs VLM gpt-4v 百度云 springboot容器部署 springboot容器化部署 微服务容器化负载均衡配置 微服务容器多节点部署 微服务多节点部署配置负载均衡 基本指令 模板 scrapy 通用环境搭建 热键 dns是什么 如何设置电脑dns dns应该如何设置 转换 pppoe radius Radius Kylin OS SVN Server tortoise svn UDP autodl Ubuntu 24.04.1 轻量级服务器 EasyConnect SSL证书 code-server RTMP 应用层 流式接口 网页设计 DeepSeek行业应用 Heroku 网站部署 ecm bpm CORS 根服务器 银河麒麟桌面操作系统 服务器时间 内网服务器 内网代理 内网通信 es open Euler dde h.264 Headless Linux 黑客技术 AI agent ISO镜像作为本地源 ldap jetty undertow 线性代数 电商平台 网工 国标28181 监控接入 语音广播 流程 SDP qt项目 qt项目实战 qt教程 佛山戴尔服务器维修 佛山三水服务器维修 MI300x uniapp thingsboard 聊天室 联想开天P90Z装win10 崖山数据库 YashanDB 雨云服务器 URL 宕机切换 服务器宕机 nfs 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 Erlang OTP gen_server 热代码交换 事务语义 tailscale derp derper 中转 备选 调用 示例 xss C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 hugo Anolis nginx安装 linux插件下载 游戏开发 sentinel flink 干货分享 黑客工具 密码爆破 流量运营 分布式训练 midjourney 架构与原理 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 IDEA hexo 搭建个人相关服务器 存储维护 NetApp存储 EMC存储 VS Code 欧标 OCPP AD 域管理 状态模式 小智AI服务端 xiaozhi TTS 西门子PLC 通讯 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 阿里云ECS 信创 信创终端 中科方德 win服务器架设 windows server Qwen2.5-VL 服务器正确解析请求体 nosql AI员工 mcp服务器 client close 授时服务 北斗授时 NFC 近场通讯 智能门锁 动态规划 跨域请求 网络原理 免费 高频交易 金仓数据库概述 金仓数据库的产品优化提案 分布式账本 共识算法 massa sui aptos sei 7-zip element-ui 上传视频并预览视频 vue上传本地视频及进度条功能 vue2选择视频上传到服务器 upload上传视频组件插件 批量上传视频 限制单个上传视频 三次握手 #STC8 #STM32 logstash 矩池云 数据下载 数据传输 pxe pyscenic 生信教程 OpenGL 制造 动态域名 LVS rtcp CPU架构 服务器cpu h.265 腾讯云服务器 轻量应用服务器 linux系统入门 linux命令 风扇散热策略 曙光 海光 宁畅 中科可控 gitlab服务器 HTTP状态码 客户端错误 服务器端错误 API设计 合成模型 扩散模型 图像生成 whistle macOS 搜狗输入法 中文输入法 鸿蒙NEXT 华为鸿蒙系统 ArkTS语言 Component 生命周期 条件渲染 Image图片组件 NVIDIA 数据库管理 GRANT REVOKE qtcreator bert PostgreSQL15数据库 软件商店 livecd systemtools 实时日志 logs NTP服务器 wifi驱动 jdk11安装 jdk安装 openjdk11 openjdk11安装 ANDROID_HOME zshrc 苹果电脑装windows系统 mac安装windows系统 mac装双系统 macbook安装win10双 mac安装win10双系统 苹果电脑上安装双系统 mac air安装win 影视app easyTier 组网 自定义shell当中管道的实现 匿名和命名管道 进程池实现 文件共享 截图 录屏 gif 开发工具 基础指令 ps命令 figma 李心怡 达梦 DM8 解决方案 接口优化 零售 输入系统 webview solr VM虚拟机 docker search 知行EDI 电子数据交换 知行之桥 EDI homeassistant anythingllm open-webui docker国内镜像 智能问答 Spring AI Milvus 漏洞报告生成 mapreduce 定义 核心特点 优缺点 适用场景 红黑树封装map和set 动态库 GCC编译器 -fPIC -shared web环境 LSTM Python学习 Python编程 Web3 Telegram GPUGEEK 5分钟快速学 docker入门 高可用 代理配置 企业级DevOps openresty 仓库 影刀证书 分享 GIS 遥感 WebGIS 僵尸世界大战 游戏服务器搭建 HAProxy 查询数据库服务IP地址 SQL Server muduo 银河麒麟高级服务器 外接硬盘 Kylin HTTP 服务器控制 ESP32 DeepSeek triton 模型分析 计算虚拟化 弹性裸金属 sqlite swoole ai工具 Unity插件 ardunio BLE tidb 北亚数据恢复 oracle数据恢复 ABAP 数字证书 签署证书 yashandb Qt QModbus vCenter服务器 ESXi主机 监控与管理 故障排除 日志记录 AWS lrzsz caddy 火山引擎 authing pythonai PlaywrightMCP 网络库 cocos2d 3dcoat aac mcp协议 go-zero 泛型编程 工厂方法模式 accept 自定义登录信息展示 motd 美化登录 实时云渲染 云渲染 3D推流 openvino 环境 非root 能源 惠普服务器 惠普ML310e Gen8 惠普ML310e Gen8V2 站群 多IP 流量 socket sse_starlette Starlette Server-Sent Eve 服务器推送事件 证书 签名 排序算法 选择排序 RNG 状态 可复现性 随机数生成 大厂程序员 硅基计算 碳基计算 认知计算 生物计算 AGI 系统架构设计 软件哲学 程序员实现财富自由 站群服务器 物理服务器 服务器租用 云服务器租用 物理机租用 网络接口 时间间隔 所有接口 多网口 事件分析 边缘服务器 利旧 AI识别 5090 显卡 AI性能 DELL R730XD维修 全国服务器故障维修 科勘海洋 数据采集浮标 浮标数据采集模块 Metastore Catalog mujoco OS Ubuntu 24.04 搜狗输入法闪屏 Ubuntu中文输入法 orbslam2 能效分析 nginx默认共享目录 ubantu 青少年编程 MateBook latex 更换镜像源 红黑树 tar.gz tar.xz linux压缩 手动分区 mac安装软件 mac卸载软件 mac book clipboard 剪贴板 剪贴板增强 CAD瓦片化 栅格瓦片 矢量瓦片 Web可视化 DWG解析 金字塔模型 Multi-Agent 蜂窝网络 频率复用 射频单元 无线协议接口RAN 主同步信号PSS 手机 Modbus TCP 学习路线 4 - 分布式通信、分布式张量 烟雾检测 yolo检测 消防检测 nmcli 网络配置 Linux的进程概念 Apache Flume 配置优化 高级功能 大数据工具集成 containerd 访问公司内网 C/C++ 恒玄BES 共享 设置 软路由 Linux指令 大大通 第三代半导体 碳化硅 带外管理 Async注解 事件驱动 access blocked 破解 vmware tools anonymous janus jQuery 光电器件 LED rtmp http状态码 请求协议 机器人仿真 模拟仿真 地平线5 asp.net 电子信息工程 EF Core 客户端与服务器评估 查询优化 数据传输对象 查询对象模式 多媒体 java毕业设计 微信小程序医院预约挂号 医院预约 医院预约挂号 小程序挂号 pipe函数 管道的大小 匿名管道的四种情况 SFTP 本地不受DeepSeek charles 数字比特流 模拟信号 将二进制数据映射到模拟波形上 频谱资源 振幅频率相位 载波高频正弦波 容器化 Serverless 代码复审 BCLinux dockerfile MobileNetV3 服务注册与发现 华为昇腾910b3 MCP 服务器 JADX-AI 插件 客户端-服务器架构 点对点网络 服务协议 网络虚拟化 网络安全防御 Java进程管理 DevOps自动化 脚本执行 跨平台开发 远程运维 Apache Exec JSch 迁移 CodeBuddy首席试玩官 A2A Svelte 零日漏洞 CVE OpenAI 全栈 机架式 IDC aiohttp asyncio 独立服务器 IT 护眼模式 nacos容器环境变量 docker启动nacos参数 nacos镜像下载 性能监控 实时传输 chrome历史版本下载 chrominum下载 亲测 线程安全 低成本 openssh linux cpu负载异常 服务器托管 云托管 数据中心 idc机房