最新资讯

  • 【高并发服务器:HTTP应用】十六、HttpContext上下文模块 && HttpServer服务器模块&& 服务器测试

【高并发服务器:HTTP应用】十六、HttpContext上下文模块 && HttpServer服务器模块&& 服务器测试

2026-01-30 15:02:45 栏目:最新资讯 3 阅读


文章目录

  • Ⅰ. `HttpContext`上下文模块设计
  • Ⅱ. 接口实现
    • 1、几个核心接口
    • 2、接收 && 解析请求行
    • 3、接收 && 解析请求头部
    • 4、接收正文
  • Ⅲ. `HttpServer`服务器模块设计
  • Ⅳ. 接口实现
    • 1、核心接口实现
    • 2、连接建立完成后的回调处理
    • 3、收到消息后的回调处理
    • 4、辅助函数
      • ① 路由查找函数
      • ② 静态资源处理函数
      • ③ 功能性请求函数处理
      • ④ 组织和响应处理函数
      • ⑤ 错误信息处理
    • 5、简单首页登录页面
    • 6、简单的错误页面
  • Ⅴ. 服务器主函数
  • Ⅵ. 测试代码
    • 1、超时连接释放测试一
    • 2、超时连接释放测试二
    • 3、超时连接释放测试三
    • 4、数据中多条请求处理测试
    • 5、`put`请求大文件上传测试
    • 6、性能测试

Ⅰ. HttpContext上下文模块设计

​ 这个模块用于记录 HTTP 请求的接收和处理进度,因为有可能出现一种情况,就是接收的数据并不是一条完整的 HTTP 请求数据,也就是请求的处理需要在多次收到数据后才能处理完成,所以在每次处理的时候,就需要将处理进度记录起来,以便于下次从当前进度继续向下处理,最终得到一个完整 HttpRequest 请求信息对象,因此 在请求数据的接收以及解析部分需要一个上下文来进行控制接收和处理的节奏

​ 这个模块要关心的要素如下所示:

  1. 已经接收并处理的请求信息
  2. 接收状态:(即当前处于何种接收阶段,方便根据不同的阶段调用不同的函数)
    • 接收请求行(当前处于接收并处理请求行的阶段)
    • 接收请求头部(表示请求头部的接收还没有完毕)
    • 接收正文(表示还有正文没有接收完毕)
    • 接收数据完毕(这是一个接收完毕,可以对请求进行处理的阶段)
    • 接收处理请求出错
  3. 响应状态码:
    • 在请求的接收并处理中,有可能会出现各种不同的问题,比如解析出错、访问的资源不对、没有权限等等,而这些错误的响应状态码都是不一样的。

所以需要提供以下接口:

  1. 接收并处理请求数据:
    • 接收请求行
    • 解析请求行
    • 接收头部
    • 解析头部
    • 接收正文
  2. 获取解析完毕的请求信息
  3. 获取响应状态码
  4. 获取接收解析状态
// RECV_LINE:接收请求行(当前处于接收并处理请求行的阶段)
// RECV_HEADER:接收请求头部(表示请求头部的接收还没有完毕)
// RECV_BODY:接收正文(表示还有正文没有接收完毕)
// RECV_DONED:接收数据完毕(这是一个接收完毕,可以对请求进行处理的阶段)
// RECV_ERROR:接收处理请求出错
typedef enum {
    RECV_LINE,      
    RECV_HEADER,    
    RECV_BODY,      
    RECV_DONED,     
    RECV_ERROR      
} HTTP_RECV_STATUS;

const int MAX_LINE_SIZE = 8192;
class HttpContext
{
private:
    int _response_status;          // 响应状态码
    HTTP_RECV_STATUS _recv_status; // 当前接收的阶段
    HttpRequest _request;          // 存放已经接收并处理的请求信息
public:
    HttpContext();

    // 获取响应状态码
    int get_response_status();
    
    // 获取接收解析状态
    HTTP_RECV_STATUS get_recv_status();

    // 获取解析完毕的请求信息
    HttpRequest& get_request();

    // 接收并处理请求数据
    void recv_and_handle_request(Buffer* buffer);

    void reset();
private:
    // 接收请求行
    bool recv_line(Buffer* buffer);

    // 解析请求行(内部会解析完将各字段放到请求模块对象中)
    bool parse_line(const std::string& line);

    // 接收头部(大部分和上面的接收请求行是重合的,注意不同的地方即可)
    bool recv_header(Buffer* buffer);

    // 解析头部
    bool parse_header(std::string& line);

    // 接收正文
    bool recv_body(Buffer* buffer);
};

Ⅱ. 接口实现

1、几个核心接口

​ 几个提供给服务器模块使用接口实现起来并不难,如下所示:

// RECV_LINE:接收请求行(当前处于接收并处理请求行的阶段)
// RECV_HEADER:接收请求头部(表示请求头部的接收还没有完毕)
// RECV_BODY:接收正文(表示还有正文没有接收完毕)
// RECV_DONED:接收数据完毕(这是一个接收完毕,可以对请求进行处理的阶段)
// RECV_ERROR:接收处理请求出错
typedef enum {
    RECV_LINE,      
    RECV_HEADER,    
    RECV_BODY,      
    RECV_DONED,     
    RECV_ERROR      
} HTTP_RECV_STATUS;

const int MAX_LINE_SIZE = 8192;
class HttpContext
{
private:
    int _response_status;          // 响应状态码
    HTTP_RECV_STATUS _recv_status; // 当前接收的阶段
    HttpRequest _request;          // 存放已经接收并处理的请求信息
public:
    HttpContext()
        : _response_status(200)
        , _recv_status(RECV_LINE)
    {}

    // 获取响应状态码
    int get_response_status() { return _response_status; }
    
    // 获取接收解析状态
    HTTP_RECV_STATUS get_recv_status() { return _recv_status; }

    // 获取解析完毕的请求信息
    HttpRequest& get_request() { return _request; }

    // 接收并处理请求数据
    void recv_and_handle_request(Buffer* buffer)
    {
        // 不同的状态,做不同的事情,但是这里不能break,因为处理完请求行后,应该立即处理头部,而不是退出等新数据
        switch(_recv_status)
        {
            case RECV_LINE: recv_line(buffer);
            case RECV_HEADER: recv_header(buffer);
            case RECV_BODY: recv_body(buffer);
        }
    }
    
    void reset()
    {
        _response_status = 200;
        _recv_status = RECV_LINE;
        _request.reset();
    }
};

2、接收 && 解析请求行

​ 下面几个接口都是私有的,就是内部的处理函数,其实就是分三个阶段进行处理:请求行阶段、头部阶段、正文阶段。这里我们先来介绍请求行阶段的两个处理函数:接收以及解析请求行!

​ 首先就是接收请求行的操作,无非就是利用 Buffer 模块中的获取一行的接口 get_line() 获取请求行,然后判断两种特殊情况:一行没有读取完毕、一行数据超过服务器规定大小(一般是8K),做这个判断主要是防止恶意的传入过长请求而导致消耗过多服务器资源!

​ 获取成功的话则调用解析请求行函数 parse_line() 开始解析请求行,在其内部解析完请求行后将各字段存放到请求模块对象中进行保存!

​ 最后别忘了要移动 buffer 的读指针,还要将所处状态改为接收头部状态!

// 接收请求行
bool recv_line(Buffer* buffer)
{
    // 1. 接收请求行之前,判断当前是否处于接收请求行的阶段
    if(_recv_status != RECV_LINE)
        return false;

    // 2. 获取缓冲区中的一行
    std::string line = buffer->get_line_andMove();

    // 3. 判断两种特殊情况:请求行没有读取完毕、请求行超过服务器规定(一般是8K)
    if(line.size() == 0)
    {
        /* 如果此时请求行没有读取完毕,而且缓冲区中的数据是超过MAX_LINE_SIZE的,
           说明数据很长都不足一行,这已经是有问题的了,那么请求行肯定是超过MAX_LINE_SIZE了 */
        if(buffer->get_sizeof_read() > MAX_LINE_SIZE)
        {
            _recv_status = RECV_ERROR;
            _response_status = 414; // 414表示URI太长了
            return false;
        }
        return true; // 返回true表示没有读取完毕,不算错误
    }

    if(line.size() > MAX_LINE_SIZE)
    {
        _recv_status = RECV_ERROR;
        _response_status = 414; // 414表示URI太长了
        return false;
    }

    // 4. 获取成功的话则调用parse_line()开始解析请求行(其内部会解析完将各字段放到请求模块对象中)
    bool ret = parse_line(line);
    if(ret == false)
        return false;

    // 5. 将所处状态改为接收头部状态
    _recv_status = RECV_HEADER;
    return true;
}

​ 而对于解析请求行函数 parse_line(),它是由 recv_line() 调用的,此时拿到的是请求行的字符串,该函数的功能就是将该字符串中的各个字段解析出来,存放到请求模块对象中去,这里就用到了我们之前讲过的正则表达式来进行解析字段!

​ 通过正则表达式解析完后结果放到了 std::smatch 类型的结果集中去,我们可以通过 [] 操作来获取它的结果,然后将它们各自放到请求模块中的对应字段去!

// 解析请求行(内部会解析完将各字段放到请求模块对象中)
bool parse_line(const std::string& line)
{
    // (GET|POST|HEAD|PUT|DELETE)   表示匹配并提取其中任意一个字符串
    // [^?]*                        [^?] 匹配非问号字符,后边的*表示 0次或多次
    // ?(.*)                      ? 表示原始的 ? 字符,(.*)表示提取 ? 之后的任意字符 0 次或多次,直到遇到空格
    // (?:?(.*))?          (?: ...) 表示匹配某个格式字符串,但是不提取,所以就是表示匹配了上一行注释 0 次或 1 次,并且不获取该内容
    // HTTP/1.[01]                表示匹配以 HTTP/1. 开始,后边有个 0 或 1 的字符串
    // (?:
|
)?                 (?: ...) 表示匹配某个格式字符串,但是不提取,而最后的 ? 表示的是匹配前边的表达式 0 次或 1 次
    
    // 注意要使用icase表示忽略请求方法的大小写
    std::regex rule("(GET|POST|HEAD|PUT|DELETE) ([^?]*)(?:?(.*))? (HTTP/1.[01])(?:
|
)?", std::regex::icase); 
    std::smatch matches; // 结果集

    bool ret = std::regex_match(line, matches, rule); // 进行表达式匹配,将匹配结果放到结果集中
    if (ret == false) 
    {
        _recv_status = RECV_ERROR;
        _response_status = 400; // BAD REQUEST
        return false;
    }

    // 举个例子,此时"GET /liren/login?user=xiaoming&pass=123123 HTTP/1.1
" 的结果如下所示:
    //      0 : GET /liren/login?user=xiaoming&pass=123123 HTTP/1.1
    //      1 : GET
    //      2 : /liren/login
    //      3 : user=xiaoming&pass=123123
    //      4 : HTTP/1.1

    // 1. 请求方法的获取
    _request._method = matches[1];
    std::transform(_request._method.begin(), _request._method.end(), _request._method.begin(), ::toupper); // 注意要将方法转化为大写

    // 2. 资源路径的获取,需要对其进行url解码,但是不需要将+转化为空格
    _request._path = Util::url_decode(matches[2], false);

    // 3. 协议版本的获取
    _request._version = matches[4];

    // 4.1 查询字符串的获取,先获取每个key=val的结构也就是键值对组合
    std::vector<std::string> strs;
    int size = Util::split(matches[3], "&", &strs);

    // 4.2 然后再分解获取每个key和val
    for(int i = 0; i < size; ++i)
    {
        std::vector<std::string> key_val;
        int n = Util::split(strs[i], "=", &key_val);

        // 此时如果只有key没有val的话则是错误的
        if(n == 1)
        {
            _recv_status = RECV_ERROR;
            _response_status = 400;
            return false;
        }

        // 正确获取的话则对key和val先进行url解析,此时就需要将+转化为空格,然后将它们设置进请求对象中保存
        std::string key = Util::url_decode(key_val[0], true);
        std::string val = Util::url_decode(key_val[1], true);
        _request.set_queryString(key, val);
    }
    return true;
}

3、接收 && 解析请求头部

​ 其实这里请求部分的接收和请求行的处理是大体相同的,不同之处在于 头部有多行,所以要用死循环,直到当前头部读到的字符串是 或者 的时候才退出循环,其它的操作都是类似的!

​ 如果不是结束行的话,则会在获取到结果后调用解析头部函数 parse_header() 进行处理!

// 接收头部(大部分和上面的接收请求行是重合的,注意不同的地方即可)
bool recv_header(Buffer* buffer)
{
    // 1. 接收头部之前,判断当前是否处于接收头部的阶段
    if(_recv_status != RECV_HEADER)
        return false;

    // 因为头部有多行,所以要用死循环
    while(true)
    {
        // 2. 获取缓冲区中的一行
        std::string line = buffer->get_line_andMove();

        // 3. 判断两种特殊情况:一行没有读取完毕、请求行超过服务器规定(一般是8K)
        if(line.size() == 0)
        {
            /* 如果此时请求行没有读取完毕,而且缓冲区中的数据是超过MAX_LINE_SIZE的,
            说明数据很长都不足一行,这已经是有问题的了,那么请求行肯定是超过MAX_LINE_SIZE了 */
            if(buffer->get_sizeof_read() > MAX_LINE_SIZE)
            {
                _recv_status = RECV_ERROR;
                _response_status = 414; // 表示一行数据太多
                return false;
            }
            return true; // 返回true表示没有读取完毕,不算错误
        }

        if(line.size() > MAX_LINE_SIZE)
        {
            _recv_status = RECV_ERROR;
            _response_status = 414; // 表示一行数据太多
            return false;
        }

        // 4. 如果读的头部是
或者
的话,表示头部接收结束了,则将所处状态改为接收正文状态,然后退出循环,
        if(line == "
" || line == "
")
            break;

        // 5. 获取成功的话则调用parse_line()开始解析请求行(其内部会解析完将各字段放到请求模块对象中)
        bool ret = parse_header(line);
        if(ret == false)
            return false;
    }
    _recv_status = RECV_BODY;
    return true;
}

​ 而解析头部相比解析请求行要简单,因为头部是一个固定的 key: val 的格式,很好分割获得键值对!要注意的是因为我们接收头部的时候,调用的接收数据接口 get_line() 是将 或者 都读取上来了,但是它不应该被包含在键值对中,所以 在解析头部之前,应该先将 去掉

// 解析头部
bool parse_header(std::string& line)
{
    // 1. 末尾是
或者
换行则要去掉
    if (line.back() == '
') line.pop_back(); 
    if (line.back() == '
') line.pop_back(); 

    // 2. 根据key: val的格式,进行分割获取头部的key和val
    std::vector<std::string> key_val;
    int size = Util::split(line, ": ", &key_val);
    if(size <= 1)
    {
        _recv_status = RECV_ERROR;
        _response_status = 400; 
        return false;
    }

    // 3. 将key和val设置进请求对象中保存
    _request.set_header(key_val[0], key_val[1]);
    return true;
}

4、接收正文

​ 接收正文并不需要进行解析,只需要拿到正文后设置到请求模块对象中的正文字段中即可!

​ 要注意的是因为当前读取到的正文还不是完整的,所以我们需要先计算还需要接收的正文长度 real_length,然后判断一下当前缓冲区数据是否大于等于 real_length,是的话则获取 real_length 部分的数据,就是当前正文的数据!

​ 如果缓冲区数据小于 real_length 的话,那就只能先获取当前缓冲区的数据,然后直接返回 true,等待服务器模块再次调用该接收正文函数读取剩余的正文,而 服务器判断是否需要再次调用该接收正文函数的依据就是根据当前的接收状态来判断如果是接收完毕了,则状态为 RECV_DONED,如果不是的话则依然还是处于 RECV_BODY 状态

// 接收正文
bool recv_body(Buffer* buffer)
{
    // 1. 接收正文之前,判断当前是否处于接收正文的阶段
    if(_recv_status != RECV_BODY)
        return false;

    // 2. 从头部中获取正文长度
    size_t size = _request.get_body_length();
    if(size == 0)
    {
        // 没有正文,则请求解析完毕
        _recv_status = RECV_DONED;
        return true;
    }

    // 3. 计算还需要接收的正文长度(因为可能前面因为数据只接收了部分)
    size_t real_length = size - _request._body.size();
    if(buffer->get_sizeof_read() >= real_length)
    {
        // 3.1 若缓冲区中的数据包含了当前请求的所有正文,则取出所需的数据,然后设置状态为接收完毕即可
        _request._body.append(buffer->start_of_read(), real_length);
        buffer->push_reader_back(real_length);
        _recv_status = RECV_DONED;
        return true;
    }

    // 3.2 若缓冲区中的数据无法满足当前正文的需要,也就是数据不足,则取出数据,然后等待新数据到来,不用修改接收状态
    _request._body.append(buffer->start_of_read(), buffer->get_sizeof_read());
    buffer->push_reader_back(buffer->get_sizeof_read());
    return true;
}

Ⅲ. HttpServer服务器模块设计

这个模块是最终给组件使用者提供的,用于以简单的接口实现 HTTP 服务器的搭建的模块。

该模块内部包括如下元素:

  • 一个 TcpServer 对象

    • 负责连接的 IO 处理。
  • 静态资源根目录

    • 其实就是一个字符串,表示当前服务器中想被外部访问的目标资源的根目录!
  • 几张哈希表存储请求方法与其处理函数的映射表

    • 组件使用者向 HttpServer 模块设置哪些请求应该使用哪些函数进行处理,比如说 GETPOSTDELETE 等方法要如何处理。这样子等 TcpServer 模块收到对应的请求之后就会使用对应的函数进行处理。

    • 不同的方法,所对应的处理函数是不同的。对于 GETHEAD 来说,它们的处理函数就是要获取静态资源,则需要用到上面的静态资源根目录成员;而对于 POSTDELETEPUT 等方法来说,它们的处理函数更多的是功能性函数,也就是需要进行具体的业务处理!

    • 不过 实际上我们用的是数组来存储映射表,因为我们在操作的时候其实需要遍历去比对正则表达式,不用到快速索引,所以用数组来代替!

先来梳理一下 HTTP 服务器的一个处理流程,方便我们了解后面需要实现什么接口:

  1. 首先要从 socket 中接收数据,放到接收缓冲区中。
  2. 调用 message_handle() 函数也就是消息的回调函数,进行业务处理。
  3. 对请求进行解析,得到一个 HttpRequest 对象,其内部就包含了该请求的所有要素。
  4. 然后根据将该 HttpRequest 对象进行请求的路由查找,即从上面提到的映射表中找到对应的处理函数。此时根据请求方法,分为两种情况:
    • 静态资源请求
      • 比如说 GETHEAD 方法,就是单纯为了请求文件的资源,那么直接将静态资源文件的数据读取出来即可,填充到 HttpResponse 模块中!
    • 功能性请求
      • 比如说 PUTPOST 方法,对于这类方法我们要在请求映射表中查找对应的处理函数,进行业务处理,然后再进行填充 HttpResponse 模块!
  5. 将上一步得到的 HttpResponse 模块组织成 HTTP 格式响应,进行发送即可!

所以根据上面的流程可以知道,我们需要实现以下给使用者使用的核心接口:

  • 添加请求-处理函数的映射信息接口
    • 这里要特别注意的是,因为资源路径是有多种的,但是对应的处理函数可能只有一种,因为存在多对一的关系,所以对于 传入的资源路径则要求必须是一个正则表达式,这样子我们在解析资源路径的时候,可以把这个对应的处理函数与的多个资源路径进行统一,就不用说搞很多重复功能但不同名的处理函数了。
    • 举个例子,如果资源路径是 /hello 的话,此时我们需要调用一个 Hello() 执行函数,而有种情况就是资源路径为 /hello/123,此时我们还是需要调用 Hello(),但是可能该路径下还有别的文件比如资源路径 /hello/456,此时我们不可能说再搞个 Hello456() 专门给它执行,所以我们要求传入的资源路径是一个正则表达式,则上面两个资源路径就变成 /hello/d+,则统一调用的都是 Hello() 函数,并且只需要在映射表种记录该正则表达式与该执行函数的关系即可,而不会出现多对一关系的情况!
  • 设置静态资源根目录接口
  • 设置是否启动非活跃连接超时关闭接口
  • 设置线程池中线程数量接口
  • 启动服务器接口

然后还需要一些内部私有成员函数,来完成 http 服务器的处理流程:

  • 两个提供给 TcpServer 对象的接口:
    1. 连接建立完成后的回调处理
      • 该回调处理很简单,就是给 TcpServer 设置协议的上下文即可。(即设置通用类型 Any 的成员变量,用于记录请求数据的处理过程,才能在线程被切走然后又重新切回来之后知道之前处理到了哪里)
    2. 收到消息后的回调处理
      • 收到消息之后就要从缓冲区中获取上下文,然后进行解析处理,判断请求方法是静态资源请求还是功能性请求类型的,根据两种类型的不同进行不同的处理(这个过程我们叫做请求的路由查找,下面也会专门搞个函数来负责处理),最后就是组织 http 响应格式进行返回!
  • 辅助消息回调处理的函数:
    1. 路由查找函数

      • 该函数其实就是为下面两个函数进行选择,根据请求方法的不同选择下面不同的处理函数!
    2. 静态资源请求处理函数

    3. 功能性请求处理函数

    4. 组织协议格式进行返回的函数

using handle_t = std::function<void(const HttpRequest&, HttpResponse*)>;
class HttpServer
{
private:
    TcpServer _server;             // 高性能服务器对象
    std::string _static_directory; // 静态资源根目录

    std::vector<std::pair<std::regex, handle_t>> get_route;    // get方法的执行函数路由表
    std::vector<std::pair<std::regex, handle_t>> post_route;   // post方法的执行函数路由表
    std::vector<std::pair<std::regex, handle_t>> put_route;    // put方法的执行函数路由表
    std::vector<std::pair<std::regex, handle_t>> delete_route; // delete方法的执行函数路由表
public:
    						/* 对外提供的核心接口 */
    HttpServer();

    // 添加请求-处理函数的映射信息接口(注意这里key不是字符串,而是一个正则表达式)
    void add_get(const std::string& pattern, const handle_t& handler);
    void add_post(const std::string& pattern, const handle_t& handler);
    void add_put(const std::string& pattern, const handle_t& handler);
    void add_delete(const std::string& pattern, const handle_t& handler);

    // 设置静态资源根目录接口
    void set_static_directory(const std::string& path);

    // 设置是否启动非活跃连接超时关闭接口
    void enable_inactive_release(int timeout);

    // 设置线程池中线程数量接口
    void set_nums_of_thread(int count);

    // 启动服务器接口
    void start_httpserver();
    
private:
    				   /* 服务器内部的两个回调处理函数,以及其所需的一些辅助函数 */
    // 连接建立完成后的回调处理
    void connected_handle(const ConnectionPtr& cptr);

    // 收到消息后的回调处理
    void message_handle(const ConnectionPtr& cptr, Buffer* buffer);

    // 路由查找函数
    void route(HttpRequest& request, HttpResponse* response);

    // 判断是否为正确的静态资源请求
    bool is_static_resource_request(const HttpRequest& request);

    // 静态资源请求处理函数
    void static_resource_request(const HttpRequest& request, HttpResponse* response);
 
    // 功能性请求函数的分类处理
    void functional_request(HttpRequest& request, HttpResponse* response, 
                            std::vector<std::pair<std::regex, handle_t>>& route);

    // 组织协议格式进行返回的函数
    void organize_and_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response);

    // 响应错误信息
    void error_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response);
};

Ⅳ. 接口实现

1、核心接口实现

using handle_t = std::function<void(const HttpRequest&, HttpResponse*)>;
class HttpServer
{
private:
    TcpServer _server;             // 高性能服务器对象
    std::string _static_directory; // 静态资源根目录

    std::vector<std::pair<std::regex, handle_t>> get_route;    // get方法的执行函数路由表
    std::vector<std::pair<std::regex, handle_t>> post_route;   // post方法的执行函数路由表
    std::vector<std::pair<std::regex, handle_t>> put_route;    // put方法的执行函数路由表
    std::vector<std::pair<std::regex, handle_t>> delete_route; // delete方法的执行函数路由表
public:
    HttpServer(uint16_t port, int timeout = DEFAULT_TIMEOUT)
        : _server(port)
    {
        // 这里只设置两个回调处理
        _server.set_connected_callback(std::bind(&HttpServer::connected_handle, this, std::placeholders::_1));
        _server.set_message_callback(std::bind(&HttpServer::message_handle, this, std::placeholders::_1,
                                               std::placeholders::_2));
        _server.enable_inactive_release(timeout);
    }

    // 添加请求-处理函数的映射信息接口(注意这里key不是字符串,而是一个正则表达式)
    void add_get(const std::string& pattern, const handle_t& handler) 
    { 
        get_route.push_back(std::make_pair(std::regex(pattern), handler)); 
    }
    void add_post(const std::string& pattern, const handle_t& handler) 
    { 
        post_route.push_back(std::make_pair(std::regex(pattern), handler)); 
    }
    void add_put(const std::string& pattern, const handle_t& handler) 
    { 
        put_route.push_back(std::make_pair(std::regex(pattern), handler)); 
    }
    void add_delete(const std::string& pattern, const handle_t& handler) 
    { 
        delete_route.push_back(std::make_pair(std::regex(pattern), handler)); 
    }

    // 设置静态资源根目录接口
    void set_static_directory(const std::string& path) 
    {
        assert(Util::is_directory(path) == true);
        _static_directory = path; 
    }

    // 设置是否启动非活跃连接超时关闭接口
    void enable_inactive_release(int timeout) { _server.enable_inactive_release(timeout); }

    // 设置线程池中线程数量接口
    void set_nums_of_thread(int count) { _server.set_nums_of_subthread(count); }

    // 启动服务器接口
    void start_httpserver() { _server.start_server(); }
};

2、连接建立完成后的回调处理

​ 这个我们简单处理,就是设置一下该连接的上下文,然后进行日志打印即可!

// 连接建立完成后的回调处理
void connected_handle(const ConnectionPtr& cptr) 
{ 
    cptr->set_context(HttpContext()); 
    DLOG("new connection: %p", cptr.get());
}

3、收到消息后的回调处理

​ 这个函数是进行业务处理的核心函数,因为当该连接对应的文件描述符有可读事件触发了,或者当连接要关闭的时候,我们先进行业务处理!

​ 因为这个函数是在 socket 接收数据之后放到缓冲区后调用的,所以我们要先读取缓冲区中的数据,通过我们前面封装的上下文 HttpContext 进行读取,这样子能保证处理时候上下文一致!

​ 接收完之后就将接收到的上下文放到请求中,即放到 HttpRequest 对象中去,然后再创建一个空的响应对象即 HttpResponse 一起进行路由查找,路由查找函数就是帮我们根据传入的请求结构,与用户设置的执行函数映射表进行匹配,如果找到对应请求的执行函数,则进行处理,找不到的话其内部会进行错误码的设置!

​ 路由查找结束之后就是根据 HttpResponse 对象的结构进行组织成响应报文,返回给客户端!

​ 发送结束之后还需要进行上下文的重置,因为我们这个回调函数是一个循环体,如果缓冲区有数据的话就会进行持续的处理,为了防止下一条请求被影响(比如说状态码不正确的情况),我们就得重置上下文!

​ 最后判断是否为短连接,是的话直接关闭连接即可,如果不是的话则不需要关心!

// 收到消息后的回调处理
void message_handle(const ConnectionPtr& cptr, Buffer* buffer)
{
    // 如果缓冲区有数据的话就进行持续的处理
    while(buffer->get_sizeof_read() > 0)
    {
        // 1. 获取上下文
        HttpContext* context = cptr->get_context()->get<HttpContext>();

        // 2. 通过上下文对缓冲区数据进行解析,得到HttpRequest对象(如果缓冲区数据解析成功,且请求已经获取完毕了,才开始去路由查找和处理)
        context->recv_and_handle_request(buffer);

        //  2.1 如果缓冲区数据解析失败,则直接响应错误信息然后关闭连接即可
        HttpRequest& request = context->get_request();
        HttpResponse response(context->get_response_status());
        if(context->get_response_status() >= 400)
        {
            // 即填充错误信息页面数据到响应中,然后返回该错误页面响应
            error_response(cptr, request, &response);

            // 出错了就把当前连接的缓冲区数据清空,不然会和下面的shutdown函数形成死循环。最后顺便把状态也清空一下
            request.reset();        
            buffer->clear_buffer(); 

            cptr->shutdown();
            return;
        }

        //  2.2 如果缓冲区数据解析成功,但是请求还没获取完整,则退出该函数,等待新数据的到来后再继续处理
        if(context->get_recv_status() != RECV_DONED)
            return;

        // 3. 进行路由查找(在其内部进行对应请求的处理)
        route(request, &response);

        // 4. 组织HttpResponse进行返回
        organize_and_response(cptr, request, &response);

        // 5. 重置上下文,防止下一条请求被影响(比如状态码什么的)
        context->reset();

        // 6. 判断是否为短连接,是的话直接关闭连接
        if(request.is_short_connection())
            cptr->shutdown();
	}
}

4、辅助函数

① 路由查找函数

​ 这个函数不难,就是判断请求的是什么资源,如果是静态资源请求的话,则进行静态资源请求处理;如果是功能性请求的话,则根据请求方法来将不同功能性请求派发到不同作用的函数中去!

​ 如果既不是静态资源请求,也不是功能性请求的话,则设置状态码为 405 表示请求方法未找到即可!

// 路由查找函数
void route(HttpRequest& request, HttpResponse* response)
{
    // 1. 如果是静态资源请求的话,则进行静态资源请求处理
    if(is_static_resource_request(request) == true)
        return static_resource_request(request, response);

    // 2. 如果是功能性请求的话,则根据请求方法来将不同功能性请求派发到不同作用的函数中去
    if(request._method == "GET" || request._method == "HEAD")
        return functional_request(request, response, get_route);
    else if(request._method == "POST")
        return functional_request(request, response, post_route);
    else if(request._method == "PUT")
        return functional_request(request, response, put_route);
    else if(request._method == "DELETE")
        return functional_request(request, response, delete_route);

    // 3. 如果既不是静态资源请求,也不是功能性请求的话,则设置状态码为405表示请求方法未找到
    response->_status = 405;
}

② 静态资源处理函数

​ 首先得先判断当前请求的是否为静态资源,如果是的话再进行静态资源请求处理!具体判断步骤和处理过程在下面代码中有注释:

// 判断是否为正确的静态资源请求
bool is_static_resource_request(const HttpRequest& request)
{
    // 1. 要求必须设置了静态资源根目录
    if(_static_directory.empty())
        return false;

    // 2. 要求请求方法GET或者HEAD
    if(request._method != "GET" && request._method != "HEAD")
        return false;

    // 3. 判断请求路径是否合法
    if(Util::is_path_valid(request._path) == false)
        return false;

    // 4. 请求的资源必须存在,并且是一个普通文件
    //    如果是请求资源是目录的话,那么就在其后面加上默认页面文件index.html即可
    std::string path = _static_directory + request._path; // 为了避免直接修改请求的资源路径,因此定义一个临时对象
    if(path.back() == '/')
        path += "index.html";
    if(Util::is_regular_file(path) == false)
        return false;
    return true;
}

// 静态资源请求处理函数
void static_resource_request(const HttpRequest& request, HttpResponse* response)
{
    // 就是将要请求的静态资源读取出来后,放到response的正文中,然后设置资源的类型Content-Type即可
    std::string path = _static_directory + request._path;
    if(path.back() == '/')
        path += "index.html";

    bool ret = Util::read_file(path, &response->_body);
    if(ret == false)
        return;
    response->set_header("Content-Type", Util::get_mime_from_suffix(path));
}

③ 功能性请求函数处理

​ 这个函数也不难,就是在对应请求方法的路由表中,查找是否存在该请求的处理方法,如果有的话则调用,没有的话则设置 404 状态码返回即可!不过要注意的就是因为 std::regex_match 中要求正则表达式是一个 const 对象,所以我们要单独搞一个变量出来进行传递,不然的话会报错!

// 功能性请求函数的分类处理
void functional_request(HttpRequest& request, HttpResponse* response, std::vector<std::pair<std::regex, handle_t>>& route)
{
    // 在对应请求方法的路由表中,查找是否存在该请求的处理方法,如果有的话则调用,没有的话则设置404状态码
    //   思路:使用路由表中的每个正则表达式与请求路径进行匹配,匹配成功则使用对应函数进行处理
    //         所以路由表中的key最好存放的是正则表达式,如果是字符串的话则需要去编译成正则表达式,比较费时间
    for(auto& handler : route)
    {
        const std::regex& regex = handler.first;
        bool ret = std::regex_match(request._path, request._matches, regex);
        if(ret == false)
            continue;

        return handler.second(request, response); // 传入请求信息和空的response,执行对应的处理函数
    }

    // 如果走到这里的话,说明上面的功能性请求没找到对应的处理方法,则设置404状态码即可
    response->_status = 404;
}

④ 组织和响应处理函数

​ 在组织响应内容之前,要先完善一些响应的头部字段,比如长短连接、正文长度、请求资源类型、重定向等等,如下所示:

// 组织协议格式进行返回的函数
void organize_and_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response)
{
    // 1. 完善一些头部字段(比如长短连接、正文长度、请求资源类型、重定向等等)
    if(request.is_short_connection() == true)
        response->set_header("Connection", "close");
    else
        response->set_header("Connection", "keep-alive");

    if(!response->_body.empty() && response->has_header("Content-Length") == false)
        response->set_header("Content-Length", std::to_string(response->_body.size()));

    if(!response->_body.empty() && response->has_header("Content-Type") == false)
        response->set_header("Content-Type", "application/octet-stream");

    if(response->_is_redirect == true)
        response->set_header("Location", response->_redirect_path);

    // 2. 根据response组织响应内容(状态行、响应报头、空行、响应正文)
    std::stringstream sstr;
    sstr << request._version << " " << std::to_string(response->_status) 
        << " " << Util::get_information_from_status(response->_status) << "
";
    for(auto& e : response->_header)
        sstr << e.first << ": " << e.second << "
";
    sstr << "
" << response->_body;

    // 3. 发送数据
    cptr->send_data(sstr.str().c_str(), sstr.str().size());
}

⑤ 错误信息处理

​ 因为有可能接收到的内容是错误的,或者服务器处理错误,此时返回一个错误页面即可!

// 响应错误信息
void error_response(const ConnectionPtr& cptr, const HttpRequest& request, HttpResponse* response)
{
    // 1. 读取错误页面文件
    std::string buffer;
    bool ret = Util::read_file("./wwwroot/error.html", &buffer);
    if(ret == false)
    {
        ELOG("响应错误信息操作失败!");
        return;
    }

    // 2. 将数据设置为响应正文,然后进行组织发送
    response->set_content(buffer, "text/html");
    organize_and_response(cptr, request, response);
}

5、简单首页登录页面

DOCTYPE html>
<html>
<head>
    <title>登录页面title>
    <style>
        body {
            background: #f1f1f1;
            font-family: Arial, sans-serif;
        }
        .container {
            width: 300px;
            margin: 0 auto;
            padding: 20px;
            background: #fff;
            border-radius: 5px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        h2 {
            text-align: center;
            color: #333;
        }
        input[type="text"],
        input[type="password"] {
            width: 100%;
            padding: 10px;
            margin-bottom: 15px;
            border-radius: 3px;
            border: 1px solid #ccc;
        }
        input[type="submit"] {
            width: 100%;
            padding: 10px;
            background: #4CAF50;
            color: #fff;
            border: none;
            border-radius: 3px;
            cursor: pointer;
        }
        input[type="submit"]:hover {
            background: #45a049;
        }
        .error {
            color: red;
            margin-bottom: 10px;
        }
    style>
head>
<body>
    <div class="container">
        <h2>用户登录h2>
        <form action="/login" method="post">
            <input type="text" name="username" placeholder="用户名">
            <input type="password" name="password" placeholder="密码">
            <div class="error">错误消息显示在这里(如果有)div>
            <input type="submit" value="登录">
        form>
    div>
body>
html>

6、简单的错误页面

DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>出错了title>
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #f1f1f1;
      }
      .error-container {
        margin: 10% auto;
        text-align: center;
      }
      h1 {
        font-size: 3rem;
        color: #333;
      }
      p {
        font-size: 1.2rem;
        color: #777;
        margin-bottom: 2rem;
      }
      a {
        display: inline-block;
        padding: 0.5rem 1rem;
        margin-top: 2rem;
        background-color: #007bff;
        color: #fff;
        border-radius: 4px;
        text-decoration: none;
        transition: background-color 0.2s ease;
      }
      a:hover {
        background-color: #0062cc;
      }
    style>
  head>
  <body>
    <div class="error-container">
      <h1>出错了h1>
      <p>抱歉,您访问的页面暂时无法显示。p>
      <a href="./index.html">返回首页a>
    div>
  body>
html>

Ⅴ. 服务器主函数

​ 先展示一下整个工程的路径结构,如下图所示:

​ 接下来我们就编写一下 server.cc 也就是服务器的主函数,简单的给出几个执行函数映射表,然后启动服务器,代码如下所示:

#include "httpserver.hpp"

const std::string source = "./wwwroot";

std::string RequestStr(const HttpRequest &req) 
{
    // 将请求报文结构转化为字符串格式
    std::stringstream ss;
    ss << req._method << " " << req._path << " " << req._version << "
";
    for (auto &it : req._queryString) 
        ss << it.first << ": " << it.second << "
";
    for (auto &it : req._header) 
        ss << it.first << ": " << it.second << "
";
    ss << "
" << req._body;
    return ss.str();
}

// 进行简单的请求报文的回显(text/plain表示文本内容)
void Hello(const HttpRequest &req, HttpResponse *rsp) 
{
    rsp->set_content(RequestStr(req), "text/plain");
}

// 进行简单的请求报文的回显
void Login(const HttpRequest &req, HttpResponse *rsp) 
{
    rsp->set_content(RequestStr(req), "text/plain");
}

// 接收请求的文件内容,然后放到当前资源根目录下
void PutFile(const HttpRequest &req, HttpResponse *rsp) 
{
    std::string pathname = source + req._path;
    Util::write_file(pathname, req._body);
}

// 进行简单的请求报文的回显
void DelFile(const HttpRequest &req, HttpResponse *rsp) 
{
    rsp->set_content(RequestStr(req), "text/plain");
}

int main()
{
    HttpServer server(8080, 10); // 设置端口和超时事件
    server.set_nums_of_thread(4);
    server.set_static_directory(source);
    
    // 设置执行函数映射表
    server.add_get("/hello", Hello);
    server.add_post("/login", Login);
    server.add_put("/1234.txt", PutFile);
    server.add_delete("/1234.txt", DelFile);
    
    // 启动服务器
    server.start_httpserver();
    return 0;
}

makefile 文件如下所示:

CXX = g++
DEBUG = -g
THREAD = -lpthread
HIGH_VERSION = -std=c++17
CXXFLAGS = -c #-Wall

TARGET = server
OBJ = $(patsubst %.cc, %.o, $(wildcard $(TARGET).cc))
HEADERS = httpserver.hpp ../server.hpp

$(TARGET) : $(OBJ)
	$(CXX) -o $@ $^ $(HIGH_VERSION) $(THREAD) $(DEBUG)

%.o : %.cc $(HEADERS)
	$(CXX) $(CXXFLAGS) -o $@ $< $(DEBUG)

.PHONY:clean
clean:
	rm -rf $(TARGET) *.o

Ⅵ. 测试代码

​ 首先先给出 makefile 文件,我们只需要在该 makefile 文件中改一下目标生成文件即可生成不同的目标文件,如下所示:

CXX = g++
TARGET = client1
OBJ = $(patsubst %.cc, %.o, $(wildcard $(TARGET).cc))

CXXFLAGS = -c #-Wall

$(TARGET) : $(OBJ)
	$(CXX) -o $@ $^ -std=c++11

%.o : %.cc
	$(CXX) $(CXXFLAGS) -o $@ $<

.PHONY:clean
clean:
	rm -f $(OBJ) $(TARGET)

1、超时连接释放测试一

​ 因为前面服务器的超时时间设置为 10 秒,所以我们让当前连接的处理休眠 15 秒进行测试,预期结果:10s 后连接被释放。

#include "../source/server.hpp"

// 测试长连接的demo
int main()
{
    // 创建客户端套接字
    Socket client_sock;
    client_sock.create_client(8080, "81.71.97.127");

    std::string str = "GET /hello HTTP/1.1
Connection: close
Content-Length: 0

";
    while(true)
    { 
        assert(client_sock.Send(str.c_str(), str.size()) != -1);
        char buf[1024] = { 0 };
        client_sock.Recv(buf, sizeof(buf) - 1);
        DLOG("%s", buf);
        sleep(15);
    }
    return 0;
}

​ 可以看到结果确实会超时释放:

2、超时连接释放测试二

给服务器发送一个数据,告诉服务器要发送 1024 字节的数据,但是实际发送的数据不足 1024,查看服务器处理结果,有以下两种情况:

  1. 如果数据只发送一次,服务器将得不到完整请求,就没有再进行业务处理,客户端也就得不到响应,最终非活跃后超时关闭连接
  2. 连着给服务器发送了多次小的请求,服务器会将后边的请求当作前边请求的正文进行处理,而后处理的时候有可能就会因为处理错误而关闭连接

​ 预期结果:服务器第一次接收请求不完整,会将后边的请求当作第一次请求的正文进行处理。最终对剩下的数据处理的时候处理出错,关闭连接。

#include "../source/server.hpp"
/* 
    给服务器发送一个数据,告诉服务器要发送1024字节的数据,但是实际发送的数据不足1024,查看服务器处理结果,有以下两种情况:
      1. 如果数据只发送一次,服务器将得不到完整请求,就没有再进行业务处理,客户端也就得不到响应,最终非活跃后超时关闭连接
      2. 连着给服务器发送了多次小的请求,服务器会将后边的请求当作前边请求的正文进行处理,而后处理的时候有可能就会因为处理错误而关闭连接
*/
int main()
{
    // 创建客户端套接字
    Socket client_sock;
    client_sock.create_client(8080, "81.71.97.127");

    std::string str = "GET /hello HTTP/1.1
Connection: keep-alive

lirendada";
    while(true)
    { 
        assert(client_sock.Send(str.c_str(), str.size()) != -1);
        //assert(client_sock.Send(str.c_str(), str.size()) != -1);        
        //assert(client_sock.Send(str.c_str(), str.size()) != -1);

        char buf[1024] = { 0 };
        client_sock.Recv(buf, sizeof(buf) - 1);
        DLOG("%s", buf);
        sleep(1);
    }
    return 0;
}

3、超时连接释放测试三

​ 当业务处理超时,查看服务器的处理情况:因为当服务器达到了一个性能瓶颈,如果在一次业务处理中花费了太长的时间(有可能超过了服务器设置的非活跃超时时间),则有可能导致其他的连接也被连累超时,其他的连接有可能会被拖累超时释放!

​ 假设现在 12345 描述符就绪了且非活跃超时时间为 30s,在处理 1 的时候花费了 30s 处理后,导致 2345 描述符因为长时间没有刷新活跃度,此时有两种情况:

  1. 如果接下来的 2345 描述符都是通信连接描述符,如果都就绪了,则没有太大的影响,因为接下来就会进行处理并刷新活跃度
  2. 如果接下来的 2 号描述符是定时器事件描述符,此时定时器触发超时,执行定时任务,就会将 345 描述符给释放掉
    • 这时候一旦 345 描述符对应的连接被释放,接下来在处理 345 事件的时候就会导致程序崩溃(内存访问错误)

​ 因此在本次事件处理中,并不能直接对连接进行释放,而应该将释放操作压入到任务池中,等到事件处理完了执行任务池中的任务的时候,再去释放。

​ 不过这种测试是比较偶然的,不一定能测出来,下面我们就创建多个子进程客户端,向服务器发送数据,然后服务器在处理 hello 执行函数中进行 15s 的等待,此时就会超过非活跃连接销毁时间!

#include "../source/server.hpp"
int main()
{
    // 创建多进程客户端套接字
    signal(SIGCHLD, SIG_IGN);
    for(int i = 0; i < 10; ++i)
    {
        pid_t id = fork();
        if(id < 0)
        {
            ELOG("fork error");
            return -1;
        }
        else if(id == 0)
        {
            // 子进程进行数据发送
            Socket client_sock;
            client_sock.create_client(8080, "81.71.97.127");
            std::string str = "GET /hello HTTP/1.1
Connection: keep-alive

";
            while(true)
            { 
                assert(client_sock.Send(str.c_str(), str.size()) != -1);
                char buf[1024] = { 0 };
                client_sock.Recv(buf, sizeof(buf) - 1);
                DLOG("%s", buf);
                sleep(1);
            }
            exit(0);
        }
    }
    while(true); // 父进程进行死循环
    return 0;
}

4、数据中多条请求处理测试

​ 一次性给服务器发送多条数据,然后查看服务器的处理结果。预期结果:每一条请求都应该得到正常处理。

#include "../source/server.hpp"

/* 一次性给服务器发送多条数据,然后查看服务器的处理结果 */
/* 每一条请求都应该得到正常处理 */

int main()
{
    // 创建客户端套接字
    Socket client_sock;
    client_sock.create_client(8080, "81.71.97.127");

    std::string str = "GET /hello HTTP/1.1
Connection: keep-alive

";
    str += "GET /hello HTTP/1.1
Connection: keep-alive

";
    str += "GET /hello HTTP/1.1
Connection: keep-alive

";
    while(true)
    { 
        assert(client_sock.Send(str.c_str(), str.size()) != -1);
        char buf[1024] = { 0 };
        client_sock.Recv(buf, sizeof(buf) - 1);
        DLOG("%s", buf);
        sleep(1);
    }
    return 0;
}

5、put请求大文件上传测试

​ 首先我们先用命令行创建一个大文件,如下所示:

[liren@VM-8-7-centos test]$ dd if=/dev/zero of=./test.txt bs=300M count=1
1+0 records in
1+0 records out
314572800 bytes (315 MB) copied, 0.495342 s, 635 MB/s

​ 然后客户端进行发送:

#include "../source/http/httpserver.hpp"

/* 大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果(上传的文件,应该和服务器保存的文件一致才对) */
int main()
{
    // 创建客户端套接字
    Socket client_sock;
    client_sock.create_client(8080, "127.0.0.1");

    std::string str = "put /1234.txt HTTP/1.1
Connection: keep-alive
";
    std::string body;
    Util::read_file("./test.txt", &body);
    str += "Content-Length: " + std::to_string(body.size()) + "

";
    assert(client_sock.Send(str.c_str(), str.size()) != -1);
    assert(client_sock.Send(body.c_str(), body.size()) != -1);

    char buf[4096] = { 0 };
    client_sock.Recv(buf, sizeof(buf) - 1);
    DLOG("%s", buf);
    sleep(3);
    return 0;
}

​ 可以看到它们的 md5 值都是相同的,说明是同一个文件!

6、性能测试

​ 这里采用 webbench 进行服务器性能测试。webbench 是知名的网站压力测试工具,它是由 Lionbridge 公司(http://www.lionbridge.com)开发的。webbench 的标准测试可以向我们展示服务器的两项内容: 每秒钟相应请求数每秒钟传输数据量

webbench 测试原理是:创建指定数量的进程,在每个进程中不断创建套接字向服务器发送请求,并通过管道最终将每个进程的结果返回给主进程进行数据统计。所以性能测试的两个重点衡量标准:吞吐量 && QPS(即每秒钟处理的包的数量)。

​ 测试环境:

​ 任何不说明测试环境的测试都是无知的,这里服务器环境:22G 云服务器,服务器程序采用 13 从的 reactor 模式。

​ 而 webbench 客户端环境:在同一台云服务器上。(测试的意义不大,因为同主机会造成互相的 cpu 争抢,但是这里目前没办法,毕竟服务器的带宽和资源太低了)

[liren@VM-8-7-centos WebBench]$ ./webbench -c 5000 http://127.0.0.1:8080/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.

Benchmarking: GET http://127.0.0.1:8080/
5000 clients, running 30 sec.

Speed=19040 pages/min, 718584 bytes/sec.
Requests: 9519 susceed, 1 failed.

本文地址:https://www.yitenyun.com/3343.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 香港站群服务器 多IP服务器 香港站群 站群服务器 #kubernetes #笔记 #平面 #容器 #linux #学习方法 #运维 #进程控制 #docker #后端 #数据库 #开发语言 #云原生 #iventoy #VmWare #OpenEuler #cpolar #fastapi #html #css #Trae #IDE #AI 原生集成开发环境 #Trae AI #人工智能 #node.js #MobaXterm #ubuntu #Conda # 私有索引 # 包管理 #低代码 #爬虫 #音视频 #物联网 #websocket #内网穿透 #网络 #学习 #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #算法 #大数据 #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #vscode #mobaxterm #深度学习 #计算机视觉 #开源 #我的世界 #android #腾讯云 #c# #unity #游戏引擎 #云计算 #windows #web安全 #安全 #kylin #nginx #tcp/ip #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #todesk #ssh #qt #c++ #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #架构 #面试 #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #华为 #ModelEngine #mvp #个人开发 #设计模式 #java #C++ #单元测试 #集成测试 #oracle #DisM++ # GLM-4.6V # 系统维护 #金融 #大模型 #mcp #金融投资Agent #Agent #我的世界服务器搭建 #minecraft #AIGC #ida #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #国产化 #vue.js #前端 #http #性能优化 #ollama #ai #llm #swagger #Android #Bluedroid #压力测试 #gpu算力 #claude #openlayers #bmap #tile #server #vue #udp #c语言 #网络协议 #jenkins #自动化 #maven #gitlab #ide #jar #1024程序员节 #需求分析 #github #git #apache #cpp #项目 #高并发 #n8n #本地部署 #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #hadoop #hbase #hive #zookeeper #spark #kafka #flink #缓存 #黑群晖 #虚拟机 #无U盘 #纯小白 #银河麒麟 #系统升级 #信创 #东方仙盟 #AI编程 #蓝湖 #Axure原型发布 #pycharm #mamba #stm32 #嵌入式硬件 #凤希AI伴侣 #编辑器 #京东云 #SAP #ebs #metaerp #oracle ebs #搜索引擎 #PyTorch # Triton # 高并发部署 #DeepSeek #MCP #蓝耘智算 #pytorch #Ansible #Playbook #AI服务器 #Anaconda配置云虚拟环境 #openHiTLS #TLCP #DTLCP #密码学 #商用密码算法 #RAID #RAID技术 #磁盘 #存储 #json #rustdesk #p2p #unity3d #游戏 #服务器框架 #Fantasy #YOLOFuse # Base64编码 # 多模态检测 #SPA #单页应用 #django #flask #web3.py #高级IO #select #计算机网络 #麒麟OS #macos #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #智能手机 #mcu #MCP服务器 #sqlite #epoll #debian #电气工程 #C# #PLC #php #科技 #自然语言处理 #神经网络 #libosinfo #centos #单片机 #TCP #客户端 #嵌入式 #DIY机器人工房 #NPU #CANN #chatgpt #codex #C2000 #TI #实时控制MCU #AI服务器电源 #阿里云 #远程桌面 #远程控制 #JumpServer #堡垒机 #react.js #CVE-2025-61686 #网络安全 #漏洞 #路径遍历高危漏洞 #idm #振镜 #振镜焊接 #scala #测试用例 #测试工具 #spring boot #web服务器 #微信小程序 #小程序 #微信 #健身房预约系统 #健身房管理系统 #健身管理系统 #asp.net #sqlserver #jvm #SRS #流媒体 #直播 #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #个人博客 #分布式 #risc-v #守护进程 #复用 #screen #部署 #golang #redis #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #screen 命令 #mysql #iBMC #UltraISO #翻译 #支付 #spring cloud #spring #驱动开发 #数据结构 #Gunicorn #WSGI #Flask #并发模型 #容器化 #Python #性能调优 #YOLOv8 # 目标检测 # Docker镜像 #Emby #视频 #文件管理 #NAS #文件服务器 #jetty #harmonyos #小艺 #鸿蒙 #搜索 #llama #opencv #语言模型 #管道Pipe #system V #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #aws #时序数据库 #电脑 #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #910B #昇腾 #C语言 #vivado license #html5 #AI #大模型学习 #https #tomcat #java-ee #svn #fabric #postgresql #可信计算技术 #CPU #华为云 #测评 #CCE #Dify-LLM #Flexus #负载均衡 #sql #毕设 #智能路由器 #intellij-idea #Dify #ARM架构 #鲲鹏 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #cursor #ssl #adb #mybatis #elasticsearch #eBPF #Puppet # IndexTTS2 # TTS #EMC存储 #存储维护 #NetApp存储 #进程 #操作系统 #进程创建与终止 #shell #Harbor #语音识别 #说话人验证 #声纹识别 #CAM++ #PTP_1588 #gPTP #RustDesk #IndexTTS 2.0 #本地化部署 #Termux #Samba #Linux #信息与通信 #信号处理 #tcpdump #ansible #经验分享 #gitea #Java #毕业设计 #车辆排放 #rust #ms-swift # 大模型 # 模型训练 #Windows #fpga开发 #进程等待 #wait #waitpid #LangGraph #CLI #JavaScript #langgraph.json #程序员 #大模型教程 #AI大模型 #结构体 #制造 #Spring AI #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #pve #ping通服务器 #读不了内网数据库 #bug菌问答团队 #GPU #AutoDL ##租显卡 #wsl #transformer #javascript #flutter #数码相机 #工具集 #CTF #deepseek #openEuler #欧拉 #SSE # AI翻译机 # 实时翻译 #VMware #VMWare Tool #聊天小程序 #wordpress #雨云 #rdp #YOLO #目标检测 #langchain #大模型开发 #大模型部署 #mindie #大模型推理 #交互 #SSH反向隧道 # Miniconda # Jupyter远程访问 #arm开发 #GPU服务器 #8U #硬件架构 #简单数论 #埃氏筛法 #idea #intellij idea #课程设计 #serverless #windows11 #microsoft #系统修复 #vllm #Streamlit #Qwen #AI聊天机器人 #ui #cosmic #5G #yum #三维 #3D #三维重建 #信令服务器 #Janus #MediaSoup #Llama-Factory # 树莓派 # ARM架构 #微服务 #rtsp #转发 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #处理器 #mariadb #webrtc #智能体来了 #智能体对传统行业冲击 #行业转型 #AI赋能 #万悟 #联通元景 #智能体 #镜像 #bash #notepad++ # GPU租赁 # 自建服务器 #RAG #LLM #chat #Miniconda #SSH #远程开发 #机器人 #muduo #TcpServer #accept #高并发服务器 #milvus #springboot #知识库 #web server #请求处理流程 #uni-app #H5 #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #rocketmq #selenium #scrapy #系统架构 #SSH公钥认证 # PyTorch # 安全加固 #node #政务 #Nacos #web #chrome #嵌入式编译 #ccache #distcc #昇腾300I DUO #fiddler #PowerBI #企业 #vnstat #监控 #数据分析 #攻防演练 #Java web #红队 #仙盟创梦IDE #POC #问答 #交付 #运维开发 #文心一言 #AI智能体 #源码 #闲置物品交易系统 #TTS私有化 # IndexTTS # 音色克隆 #anaconda #虚拟环境 #nfs #iscsi #SSH跳板机 # Python3.11 #WT-2026-0001 #QVD-2026-4572 #smartermail #LVDS #高速ADC #DDR #视频去字幕 #API限流 # 频率限制 # 令牌桶算法 #okhttp #前端框架 #计算机外设 #screen命令 #其他 #prompt #azure #树莓派4b安装系统 #scanf #printf #getchar #putchar #cin #cout #门禁 #梯控 #智能一卡通 #门禁一卡通 #消费一卡通 #智能梯控 #一卡通 #源代码管理 #超时设置 #客户端/服务器 #网络编程 #挖矿 #Linux病毒 #ai编程 #Beidou #北斗 #SSR #react native #KMS激活 #算力一体机 #ai算力服务器 # GPU集群 #taro #排序算法 #jdk #排序 #框架搭建 #状态模式 #AI-native #dba #ddos #国产化OS #vps #MQTT协议 #CSDN #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #机器学习 #aiohttp #asyncio #异步 #论文阅读 #软件工程 #weston #x11 #x11显示服务器 #信息安全 #信息收集 #数据仓库 #计算几何 #斜率 #方向归一化 #叉积 # 批量管理 #RSO #机器人操作系统 #ASR #SenseVoice #星图GPU #glibc #中间件 #winscp #ONLYOFFICE #MCP 服务器 #后端框架 #LoRA # lora-scripts # 模型微调 #laravel #AI论文写作工具 #学术写作辅助 #论文创作效率提升 #AI写论文实测 #深度优先 #DFS #集成学习 #prometheus #grafana #证书 #.netcore #数字化转型 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #微PE # GLM-4.6V-Flash-WEB # AI部署 #AB包 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #HeyGem # 数字人系统 # 远程部署 # 双因素认证 # TensorFlow #服务器繁忙 #连接数据库报错 #媒体 #Go并发 #高并发架构 #Goroutine #系统设计 #Docker #Zabbix #CosyVoice3 #语音合成 #Clawdbot #vuejs #运维工具 #硬件工程 #智能家居 #pyqt #DNS #Discord机器人 #云部署 #程序那些事 #STDIO传输 #SSE传输 #WebMVC #WebFlux #bootstrap #企业微信 #ipmitool #BMC #C #kmeans #聚类 #文件IO #输入输出流 #unix #C/C++ #编程 #c++高并发 #百万并发 #SSH别名 # CUDA #CS2 #debian13 #IndexTTS2 # 阿里云安骑士 # 木马查杀 #visual studio code #excel #uip #多进程 #python技巧 #paddleocr #企业级存储 #网络设备 #word #pdf #大语言模型 #推荐算法 #SMTP # 内容安全 # Qwen3Guard #汽车 #zotero #WebDAV #同步失败 #代理模式 #渗透测试 #黑客技术 #计算机 #文件上传漏洞 #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #Android16 #音频性能实战 #音频进阶 #平板 #零售 #交通物流 #智能硬件 #A2A #GenAI #numpy #LobeChat #vLLM #GPU加速 #无人机 #Deepoc #具身模型 #开发板 #未来 # 水冷服务器 # 风冷服务器 # IndexTTS 2.0 # 自动化运维 #插件 #开源软件 #pjsip #openresty #lua #FHSS #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #NFC #智能公交 #服务器计费 #数据挖掘 #FP-增长 #AI生成 # outputs目录 # 自动化 #海外服务器安装宝塔面板 #开源工具 #tdengine #涛思数据 #SSH保活 #DAG #nodejs #能源 #人脸识别sdk #视频编解码 #人脸识别 #SSH密钥 #练习 #基础练习 #数组 #循环 #九九乘法表 #计算机实现 #论文笔记 #dynadot #域名 #ETL管道 #向量存储 #数据预处理 #DocumentReader #esb接口 #走处理类报异常 #创业创新 #业界资讯 #ffmpeg #模拟退火算法 #spring ai #oauth2 #网路编程 #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 # 语音合成 #内存接口 # 澜起科技 # 服务器主板 #大模型入门 #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #eureka #mongodb #VibeVoice # 高温监控 #广播 #组播 #并发服务器 #x86_64 #数字人系统 #uvicorn #uvloop #asgi #event #gpu #nvcc #cuda #nvidia #leetcode #TensorRT # 推理优化 #AI 推理 #NV #Jetty # CosyVoice3 # 嵌入式服务器 #UDP的API使用 #ESP32 # OTA升级 # 黄山派 #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #SQL注入主机 #neo4j #NoSQL #SQL # WebUI # 网络延迟 #ranger #MySQL8.0 #teamviewer #rsync # 数据同步 #ThingsBoard MCP #Socket网络编程 #sql注入 #LangFlow # 智能运维 # 性能瓶颈分析 #devops #戴尔服务器 #戴尔730 #装系统 #遛狗 #bug #clickhouse #Modbus # 串口服务器 # NPort5630 #代理 #YOLO26 # 服务器IP访问 # 端口映射 #arm64 #串口服务器 #MOXA # 批量部署 # 一锤定音 # 大模型微调 #散列表 #哈希算法 #远程连接 #数据安全 #注入漏洞 #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #WinSCP 下载安装教程 #SFTP #FTP工具 #服务器文件传输 # RTX 3090 #CUDA #Triton #AI部署 # ms-swift #硬件 #dify #蓝牙 #LE Audio #BAP #个人助理 #数字员工 #Node.js # child_process #puppeteer #KMS #slmgr #内存治理 #googlecloud #链表 #Qwen3-14B # 大模型部署 # 私有化AI #动态规划 #xlwings #Excel #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #vp9 #安全威胁分析 #Minecraft #Minecraft服务器 #PaperMC #我的世界服务器 #飞牛nas #fnos #前端开发 #EN4FE #指针 #自由表达演说平台 #演说 #程序员创富 #程序人生 #GB28181 #SIP信令 #SpringBoot #视频监控 # GLM-TTS # 数据安全 #xshell #host key #3d #蓝桥杯 #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #范式 #ip #Modbus-TCP # ARM服务器 # 大模型推理 #Karalon #AI Test #编程助手 #系统管理 #服务 #流程图 #图论 #国产开源制品管理工具 #Hadess #一文上手 # GLM # 服务连通性 #健康医疗 #ceph #ambari #arm #turn #网安应急响应 # 高并发 #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #工程实践 #AI应用 #CMake #Make #图像识别 #OpenAI #高可用 #故障 #优化 #高考 #多模态 #微调 #超参 #LLamafactory #国产操作系统 #麒麟 #V11 #kylinos #SSH跳转 #rabbitmq #TTS #go #gpt #API #AI写作 #Gateway #认证服务器集成详解 #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #wps #Linux多线程 #Tokio #Java程序员 #Java面试 #后端开发 #Spring源码 #Spring #华为od #华为机试 #通信 #simulink #matlab #Socket #套接字 #I/O多路复用 #字节序 #研发管理 #禅道 #禅道云端部署 #软件 #本地生活 #电商系统 #商城 #samba #poll #webpack #传统行业 #信息可视化 #zabbix #飞牛NAS #NVR #EasyNVR #.net #JNI #材料工程 #智能电视 #pxe #VMware创建虚拟机 #挖漏洞 #攻击溯源 #blender #warp #STUN # TURN # NAT穿透 #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #防毒面罩 #防尘面罩 #net core #kestrel #web-server #asp.net-core #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #Prometheus #日志分析 #free #vmstat #sar #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #FASTMCP #DooTask #r语言 #postman #产品运营 #spine #TRO #TRO侵权 #TRO和解 #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #网络攻击模型 #交换机 #三层交换机 #云服务器 #个人电脑 #KMS 激活 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #MC #MC群组服务器 #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #系统安全 # 黑屏模式 # TTS服务器 #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #云开发 #领域驱动 #漏洞挖掘 #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #BoringSSL #embedding #云计算运维 #入侵 #日志排查 #asp.net上传大文件 #群晖 # 鲲鹏 #FTP服务器 #http头信息 #人大金仓 #Kingbase #ci/cd #k8s #VMware Workstation16 #服务器操作系统 #Spring AOP # 公钥认证 # 权限修复 #ICE #信创国产化 #达梦数据库 #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL # HiChatBox # 离线AI #TCP服务器 #开发实战 #全文检索 #iot #银河麒麟服务器系统 #生信 #Smokeping #网站 #截图工具 #批量处理图片 #图片格式转换 #图片裁剪 #策略模式 #鸿蒙PC #租显卡 #训练推理 #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #bigtop #hdp #hue #kerberos #pencil #pencil.dev #设计 #新人首发 #轻量化 #低配服务器 #Anything-LLM #IDC服务器 #私有化部署 #X11转发 #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman #Kylin-Server #服务器安装 #raid #raid阵列 #短剧 #短剧小程序 #短剧系统 #微剧 #hibernate #nosql #Langchain-Chatchat # 国产化服务器 # 信创 #vncdotool #链接VNC服务器 #如何隐藏光标 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #PyCharm # 远程调试 # YOLOFuse #journalctl #docker安装seata #CNAS #CMA #程序文件 #Syslog #系统日志 #日志监控 #生产服务器问题查询 #日志过滤 #Autodl私有云 #深度服务器配置 #IO #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #wireshark #网络安全大赛 #database #儿童AI #图像生成 #r-tree #outlook #错误代码2603 #无网络连接 #2603 #算力建设 #stl #漏洞修复 #IIS Crypto #实时检测 #卷积神经网络 #服务器解析漏洞 #云服务器选购 #Saas #线程 #everything #具身智能 #elk #esp32 arduino #决策树 #HistoryServer #Spark #YARN #jobhistory #HarmonyOS APP #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #ComfyUI # 推理服务器 #n8n解惑 # 显卡驱动备份 #计算机毕业设计 #程序定制 #毕设代做 #课设 #数据可视化 #Hadoop #rtmp #AI电商客服 #tensorflow #ROS #homelab #Lattepanda #Jellyfin #Plex #Kodi # 局域网访问 # 批量处理 #yolov12 #研究生life #开关电源 #热敏电阻 #PTC热敏电阻 #nacos #银河麒麟aarch64 #fs7TF # 远程访问 # 服务器IP # 服务器迁移 # 回滚方案 #npu #memcache #大剑师 #nodejs面试题 #企业存储 #RustFS #对象存储 #ServBay #es安装 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #远程软件 #游戏机 #log4j #模块 #内网 # 跳板机 #ansys #ansys问题解决办法 #音乐 #IntelliJ IDEA #Spring Boot #Coturn #TURN # Connection refused # 代理转发 #echarts #代理服务器 # 端口7860 #建筑缺陷 #红外 #数据集 #设计师 #图像处理 #游戏美术 #技术美术 #SMARC #ARM #多线程 #claudeCode #content7 #跳槽 #工作 #Reactor #odoo #win11 #空间计算 #原型模式 # 云服务器 #HarmonyOS #雨云服务器 #教程 #MCSM面板 #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #junit #I/O模型 #并发 #水平触发、边缘触发 #多路复用 # 服务器配置 # GPU #MinIO服务器启动与配置详解 #appche #Ubuntu #数据访问 #自动化运维 #ftp #sftp #DHCP #C++ UA Server #SDK #跨平台开发 #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #agent #ai大模型 # 轻量化镜像 # 边缘计算 #OpenHarmony #eclipse #servlet #Python办公自动化 #Python办公 #版本控制 #Git入门 #开发工具 #代码托管 #实时音视频 #SSH复用 # 远程开发 #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 #GATT服务器 #蓝牙低功耗 #lucene #硬盘克隆 #DiskGenius # 键鼠锁定 #opc #opc ua #opc模拟服务器 #机器视觉 #6D位姿 #jupyter #UOS #海光K100 #统信 #mssql #cpu #wpf #量子计算 #报表制作 #职场 #用数据讲故事 #Fun-ASR # 语音识别 #语音生成 #密码 #firefox #safari #b树 #PN 结 #ArkUI #ArkTS #鸿蒙开发 #服务器线程 # SSL通信 # 动态结构体 # ControlMaster #RWK35xx #语音流 #实时传输 #超算中心 #PBS #lsf #lvs #adobe #windbg分析蓝屏教程 #数据迁移 #powerbi #le audio #低功耗音频 #连接 #nmodbus4类库使用教程 #docker-compose #目标跟踪 #express #cherry studio #gmssh #宝塔 #1panel #IFix #c++20 # 远程连接 #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #系统安装 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #Buck #NVIDIA #算力 #交错并联 #DGX #可再生能源 #绿色算力 #风电 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #IPv6 #gerrit #AI应用编程 # 环境迁移 #scikit-learn #随机森林 #matplotlib #若依 #安全架构 #AI Agent #开发者工具 #边缘AI # Kontron # SMARC-sAMX8 #ET模式 #非阻塞 #remote-ssh #产品经理 #就业 #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #MinIO #sentinel #高斯溅射 #UEFI #BIOS #Legacy BIOS #AI智能棋盘 #Rock Pi S #边缘计算 #改行学it #全链路优化 #实战教程 #Proxmox VE #虚拟化 #sglang #smtp #smtp服务器 #PHP #声源定位 #MUSIC #SSH Agent Forwarding # 容器化 #性能 #RAM #分布式数据库 #集中式数据库 #业务需求 #选型误 #gateway #Comate #copilot #反向代理 #参数估计 #矩估计 #概率论 #memory mcp #Cursor #Exchange #静脉曲张 #腿部健康 #运动