1. 框架梳理

在这里插入图片描述

在这里插入图片描述
我们在muduo框架中主要解决的是C100K的问题,是传输层中用户空间所要做的事情。
传输层内核空间做的就是TCP,UDP哪些子深奥的东西。即使是陈硕也只是写用户空间的东西哦。

2.HTTP报文解析封装模块

2.1 了解一些基础的知识更方便学习

HTTP(超文本传输协议),客户端浏览器 与 服务端交流的文本
这个是请求报文(和HTTP请求类不是同一个东西)
在这里插入图片描述
访问127.0.0.1这个服务器的8080端口。
这个8080端口就像muduo框架中的acceptor,监听accpetorchannel只有一个,连接conchannel有N个。(意思就是端口只有一个,但是可以接受C100K)

请添加图片描述【8080端口】是一种存在于【传输层的一种概念】,在传输层得到http请求报文后,os根据这个8080把该数据送到相应的应用那去(qq,卫星,自己写的HTTP服务器 )

2.2 HttpRequest类(HTTP请求)

resquest.send(“Hello”),我的框架在底层其实做了这些苦力活:

  1. 帮你自动计算内容的长度,填入 Content-Length。
  2. 帮你生成 HTTP/1.1 这行字。
  3. 把hello写成Json字符串格式。
    将http这个超文本写进类中,可以用成员变量一 一对应请求行呀,请求头呀,请求体呀。非常方便。

2.3 HttpContext 上下文类

**干什么的:**它记住了当前解析到了哪个阶段。
在相应的阶段,把buffer中的http格式的字符串存到HTTPRequest类中。
在这里插入图片描述

它是一个状态机StateMechine。这个状态机就像一个进度条,用来显示HTTPRequest的填装在第几步。
为什么需要这样干:因为tcp报文(http报文)是流式传输的,不能精准获得一个http报文大小的数据。只有状态机才能避免数据的半包和沾包。
该状态机有三个状态: 我正在解析请求行,我正在解析请求头,我正在解析请求体。遇见/r/n/r/n就切换状态。
在这里插入图片描述
也是httpcontext把httprespone对象解析为http格式的字符串的吗?
答案:不是的。是httprespone自己来,因为他不像HTTPRequest需要有
状态机
辅助自己的
非阻塞IO
!!!!

首先 我们是非阻塞IO+Reactor的模式。tcp又是流式传输,一下传一点。如果没有HttpContext 的状态机,我们根本不知道tcp在传哪一部分的东西。
在这里插入图片描述
在这里插入图片描述

2.4 HttpResponse类(HTTP响应)

在这里插入图片描述

2.5 HttpServer类

这个类 涉及事件驱动机制。

功能一:新连接建立时回调(onConnection):回调函数(这个函数是回调函数,如果它被调用,会实现一些功能)

给新连接设置一个HttpContext对象,填好HTTPRequst的表格 哈哈哈

功能二:服务端接收数据的消息回调(onMessage):回调函数
服务端程序在接收到客户端的数据时,会调用该函数。
该函数的作用是:
请添加图片描述
功能三:当接收到消息回调(OnMessage)后调用的处理
在填好HttpRequest的表格后,服务器开始研究起这个表格

  1. 这个HttpRequest是长连接还是短链接呀。
  2. 这个HttpRequest能否跨域啊?
  3. 能跨域的话,就开始翻路由表(onHttpCallback()是专门翻路由表的)。
  4. 有相应的路由,就要执行
  5. 执行完,就回复HttpResponse

功能四:在业务层上进行回调函数的注册
注册静态路由处理器
server.Get(“/path”, cb),把这条路由信息写入Router 内部的那个哈希表(handlers_)中。
当URI为/path时,触发这个cb

3.Router类(路由模块)

在这里插入图片描述
在这里插入图片描述

3.1路由(Router)模块

route接口 存贮路由信息,将URL与一个回调函数映射

/users        ->  getAllUsers()
/users/count  ->  getUsersCount()

router中有成员变量:
有一个负责静态路由的哈希表。
有一个负责动态路由的数组。
将路由信息注册到【静态路由表 unordered_map】 或者【动态路由表vector】中。

3.2 静态路由注册

使用哈希表。key是get http//:127.0.0.1:8080/user/1
value是处理器或者回调函数

3.3动态路由注册

在这里插入图片描述
所以使用addRegexHandler和addRegexCallback将1,2,3,4.。。。变为正则符号user/([^/]+)

4.会话管理模块

分辨cookie与Session的区别。
Cookie是客户端的,Cookie中有SessionID
Session是服务器的,根据Cookie中的SessionID,服务器挑选

http本来是无状态的,当需要有状态时(比如维持一个登入信息),就需要用到会话模块。例:这个请求是“张三”发的,他已经登录了。

请添加图片描述

4.1SessionManager(会话管理器)

使得 会话数据可以存储在内存中或持久化到数据库中,以便在服务器重启后恢复。

它持有一个 SessionStorage 的指针。这个 SessionStorage就是session要存的地方

class SessionManager {
private:
    // 经理手里握着一把仓库的钥匙(指针)
    std::unique_ptr<SessionStorage> storage_; 
};

4.2SessionStorage(会话存储)

会话数据可以存储在内存中或持久化到数据库中,以便在服务器重启后恢复。

  • save():

  • load():有一个http连接,给连接我一个session ID,我吐出一个 Session 对象给你

  • remove():

这个存储是在内存中MemorySessionStorage(内存会话存储) 还是数据库中RedisSessionStorage

4.3MemorySessionStorage(内存会话存储)

sessionStorage的内存实现。

  • save():

  • load():给我一个session ID,我从内存中吐出一个 Session 对象给你

  • remove():

4.4会话使用案例

通过调用SessionManager来创建,销毁Session

4.4.1 在 HttpServer 类中添加 SessionManager

在 HttpServer 类中添加了一个 SessionManager

4.4.2 在处理器中使用会话

有一个httpRequest,这是创建一个Session,这个Session用来存储用户的信息,比如userId,usernam,

            auto session = server_->getSessionManager()->getSession(req, resp);
            
            // 在会话中存储用户信息
            session->setValue("userId", std::to_string(userId));
            session->setValue("username", username);
            session->setValue("isLoggedIn", "true");
4.4.3 登出处理器示例
    // 销毁会话
    server_->getSessionManager()->destroySession(session->getId());


5.中间件模块

在这里插入图片描述

5.1 代码实现

这里以跨域中间件为例来介绍中间件的完整实现流程。
**跨域是什么?**就是我们在点击一个网站上的内容时,如果跳转到别的网站,就是跨域。还在自己这个网站上就不是跨域。
在这里插入图片描述
在这里插入图片描述

跨域中间件的作用就是给服务器设置一个Crosconfig,这个Crocconfig中注册了哪些域名的客户端(也就是前端)可以访问服务器(一般是*,也就是所有客户端都可以访问)、哪些方法可以跨域请求。

5.1.1中间件基类接口 (Middleware.h)

这是一个抽象基类(Abstract Base Class),它定义了所有中间件必须遵守的规则。

5.1.2 2. 中间件链管理 (MiddlewareChain.h)

有一个middlewares数组,里面全是middleware。
相当于封装了 CorsMiddleware

5.1.3. CORS配置类 (CorsConfig.h)

这是一张白名单。上面写着:“只允许 http://localhost:8080 的人进来,允许带 Content-Type 的行李”。

5.1.4CORS中间件实现(CorsMiddleware.cpp)

继承了Middleware基类的子类。

项目里的代码太复杂了,我简化了他的逻辑
,我们预设与一个【环境】

项目是前后端分离的架构,前端在发 JSON 请求时,浏览器为了安全会默认拦截,所以我必须在后端处理跨域(CORS)问题。

  1. 如果,如果收到的是 OPTIONS 预检请求,【中间件】会直接拦截,返回 200 OK 和允许跨域的 Header,不让它进入后面的业务逻辑。
  2. 如果是正常的业务请求,【中间件】会给响应头统一自动加上 Access-Control-Allow-Origin 等字段。这样到了onMessage的接口时,浏览器会认为这是一个安全的跨域HttpRequest

6. 集成数据库连接池模块

【线程池】是有c100K问题,只有保证线程的【复用】,才能处理。
【连接池】是DB连接的【复用】

6.1 DbConnection数据库的单个连接

我这个业务逻辑中,解析完了httpRequest后,发现需要访问数据库。
但是每一个客户端访问服务端的数据库都需要id password登入数据库,
这太费时了。
所以直接让服务端中常驻几个DbConnection对象,会大大加快速度。

我们创建一个连接池,只要线程需要,就去拿。
【线程池】+【连接池】的联动。连接池就是线程用的一脸快车!
在这里插入图片描述

6.1.1 DbConnection::DbConnection()构造函数接口。

当你创建一个 DbConnection 对象时,顺便立刻连上 MySQL 数据库。
使用的是封装好的 driver,直接给你搞好tcp三次握手

6.1.2 sql注入

黑客写 '); DROP TABLE chat_message; --
那么拼接后的语句可能变成如下(导致一个表被删除!!!)

INSERT INTO chat_message (id, username, is_user, content, ts) VALUES (
    123, 
    'Alice', 
    1, 
    ''); DROP TABLE chat_message; --', 
    1234567890)

解决方案:
使用 ? 或 @param 占位符

std::string sql = "INSERT INTO chat_message (id, username, is_user, content, ts) VALUES (?, ?, ?, ?, ?)";

这样即使黑客写'); DROP TABLE chat_message; -- 只会被当作一个?的值。

6.2 数据库连接池

连接池和线程池,虽然都叫池,但是有很大的不同!
线程池,【生产者】是主线程生成任务,消费者是【消费者】消费任务。【任务队列】用来存任务。
连接池是不管主线程和子线程,【队列】中存储的是【已经初始化好的空闲DBConnection】,【消费者】是线程,需要一个DBConnection,所以 getConnection() 。【生产者】也是线程,用完了还回queue中,所以releaseConnection()

#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <vector>

// 1. 模拟一个数据库连接(假的,只打印日志)同上面的DBConnection
class MockConnection {
public:
    int id;
    MockConnection(int i) : id(i) {}
    
    // 模拟执行 SQL
    void query(std::string sql) {
        std::cout << "连接[" << id << "] 正在执行: " << sql << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时
    }
};

// 2. 连接池类
class ConnectionPool {
private:
    std::queue<MockConnection*> pool_; // 仓库:存放空闲连接
    std::mutex mtx_;                   // 锁:保证借车还车不冲突
    std::condition_variable cv_;       // 喇叭:没车时等待,有车时通知

public:
    // 初始化:由于创建连接很慢,我们启动时先造好放在池子里
    ConnectionPool(int size) {
        for (int i = 0; i < size; ++i) {
            pool_.push(new MockConnection(i + 1));
        }
        std::cout << "连接池初始化完成,共有 " << size << " 个连接" << std::endl;
    }

    // A. 借连接 (Get)
    MockConnection* getConnection() {
        std::unique_lock<std::mutex> lock(mtx_); // 1. 上锁

        // 2. 如果池子空了,就等待(阻塞在这里,直到有人还车)
        while (pool_.empty()) {
            std::cout << "没连接了,线程 " << std::this_thread::get_id() << " 在排队等待..." << std::endl;
            cv_.wait(lock); 
        }

        // 3. 取出一个连接
        MockConnection* conn = pool_.front();
        pool_.pop();
        
        return conn; // 返回给用户
    }

    // B. 还连接 (Release)
    void releaseConnection(MockConnection* conn) {
        std::unique_lock<std::mutex> lock(mtx_); // 1. 上锁
        
        // 2. 放回池子
        pool_.push(conn);
        
        // 3. 通知正在等待的一个线程:“有车了,快醒醒!”
        cv_.notify_one(); 
    }
};

7https模块

在这里插入图片描述

7.1 SSL四次握手公私钥

在这里插入图片描述在这里插入图片描述

7.2 集成 SSL 到 HTTP 服务器

  • 以前:lisFD听到有连接来了—》建立http连接
  • 现在:lisFD听到有连接来了–》ssl四次握手加密—》建立http连接
    就是在这个函数中,在建立连接时增加了ssl握手的操作
void onNewConnection(int client_fd) {
    // 1. 创建一个 SSL 对象 (它是这个连接专属的翻译官)
    SSL* ssl = SSL_new(ctx);

    // 2. 把这个 SSL 对象和底层的 TCP Socket 绑定在一起
    // 意思是:以后 ssl 负责往 client_fd 里读写数据
    SSL_set_fd(ssl, client_fd);

    // 3. 【关键】进行 SSL 握手 (Handshake)
    // 这就是之前说的“四次握手”发生的阶段
    int ret = SSL_accept(ssl); 
    
    if (ret <= 0) {
        // 握手失败(比如客户端不是 HTTPS 请求,或者网络断了)
        // 打印错误,关闭连接
        return; 
    }

    // 握手成功!现在连接是安全的了。
    // 把这个 ssl 指针保存到你的 Connection 对象里去。
}

8.框架应用之卡码五子棋

8.1前端的一点知识

这是一份html的代码。是由三种语言写的html,css,javascript。
html是用来组建这个网页的
css是美工用来调整网页的美术部分
javascript则是程序员利用回调函数,实现功能的部分。

<!DOCTYPE html>
<html>
<head>
    <style>
        /* 找到 id 叫 "myButton" 的东西,给它化妆 */
        #myButton {
            padding: 20px 40px;       /* 按钮内边距(让它胖一点) */
            background-color: green;  /* 背景绿色 */
            color: white;             /* 文字白色 */
            font-size: 20px;          /* 字号 */
            border: none;             /* 去掉边框 */
            border-radius: 10px;      /* 把直角磨圆 */
            cursor: pointer;          /* 鼠标放上去变成小手 */
        }
    </style>
</head>
<body>

    <button id="myButton">我是初始状态</button>

    <script>
        // 第一步:用 C++ 的思维,先获取对象的指针
        // document.getElementById 就像是 findObjectById()
        let btn = document.getElementById("myButton");

        // 第二步:给这个对象绑定一个“回调函数” (Callback)
        // onclick 就像是 Qt 里的 connect signal slot
        btn.onclick = function() {
            // 这里写逻辑代码
            
            // 1. 修改 HTML 内容 (原本是 "我是初始状态")
            btn.innerText = "我被点了一下!";
            
            // 2. 修改 CSS 样式 (原本是绿色)
            btn.style.backgroundColor = "red";
            
            // 3. 打印日志 (就像 std::cout)
            console.log("按钮被点击了,C++ 程序员你好!");
        };
    </script>
</body>
</html>

8.2 有几个重要问题

8.2.1如何实现下棋的功能

通过javascript代码实现以下功能:
首先是客户端c向s请求棋盘。
然后客户端下棋(c直接在棋盘上放一个子,前端的代码多出了一个子,告诉s现在的变动,而不是向s申请一份新的棋盘)

s收到变动的HttpRequest后,就回复一个ok的Httpresponse。
s更新棋盘数组,数组中多了一个子
s还做【游戏是否结束】的判断

8.2.2 ai对手如何选择在哪里下棋?

通过一些算法

8.2.3前后端分离仔细说说

服务器上只是【前端的代码】,不是【前端】。
真正的【前端】就是出现在客户端电脑上的东西。
我们拿baidu.com来说明。
客户在浏览器中输入baidu.com。弹出的网页就是百度的前端。当我要搜索小狗的图片时,前端将这个请求传给【后端】httpserver框架。

9.面试

1. 你在开发自定义HttpServer框架中具体负责了哪些部分?

  • http模块:http请求报文+响应报文的报文解析封装模块

2.你在实现 基于Reactor模型 的 高性能HTTP服务器 时遇到了哪些挑战?

  1. 将muduo框架与httpSever整合。线程池与连接池整合。
    “最大的挑战是如何【在 Reactor 模型下】保证 【EventLoop循环】 不阻塞。 因为 Muduo 的 IO 线程必须快速响应,不能在耗时业务上停留太久。所以我必须把耗时的业务逻辑(比如数据库查询、五子棋 AI 计算)剥离出去。 我的解决办法是:引入了线程池+连接池。

在这里插入图片描述
2. TCP 的粘包与 HTTP 的完整性解析
用httpContext状态机完美解决。

  1. 没有考虑数据库连接超时的问题。
    昨天晚上登入了,但没关掉网页,第二天发现网站打不开了。
    这是因为DBconnection因为闲置太久,过了8小时mysql会单方面切断TCP连接。但服务器端还不知道,
    调用连接池的getConnection() 高高兴兴地把这个“已经死掉”的连接对象借给了线程,才会报错。
    解决:
    可以在连接池中添加一个接口,这个接口就是每过一个小时,把pool里的DBconnection在一个后台线程中全跑一遍,这样就不会达到8小时了。

  2. 垃圾回收机制

  • 有共享的智能指针。比如httpcontext,没有一个客户端和服务器都有一个,有一个就+1
  • 连接池的回收。服务器用完DBconnection,不是销毁,而是重新放回【连接池】
  • RALL,智能锁自己销毁

3.描述一下你是如何集成OpenSSL来实现HTTPS支持的。

  • http:lisFD听到有连接来了—》建立http连接
  • https:lisFD听到有连接来了–》ssl四次握手加密—》建立http连接

4.你设计的动态路由管理系统是如何支持多种HTTP请求方法的?

为每种HTTP 请求方法创建一个独立的路由表。有静态路由+动态路由

5.会话管理功能是如何实现的,你如何处理会话超时?

我的项目中有一个会话管理模块。

6.你开发的中间件模块有哪些功能,它们如何增强了系统扩展性?

跨域中间件,连接前端服务器和后端服务器。

7.在项目中为什么使RabbitMQ消息队列?

RabbitMQ支持异步写入
AIHelper中的pushMessageToMysql放入sql语句到消息队列中(充当生产者)
RabbitMQThreadPool的worker`将消息队列的读取进行相应操作(充当消费者)

8.你是如何保证用户会话的上下文记忆的?

在服务器初始化阶段,服务器会执行一个sql语句,预先从 MySQL 中按时间戳顺序拉取指定用户与 AI 的聊天历史消息,并加载到内存中。

9.如果阿里云百炼 API 出现延迟或不可用,你会如何保证系统的可用性?

超时控制
在 HTTP 请求调用百炼 API 时,应设置合理的超时时间(如 3s–5s),以避免请求无限阻塞。若在规定时间内未获得响应,系统可立即启用降级策略,例如:

返回本地兜底提示(如“服务繁忙,请稍后再试”)。

10.项目中 AI 部分采用了外部 API 调用,你会考虑 本地模型部署 吗?为什么?

外部api调用,就是利用http服务器做了一个中转站。
本地模型部署则是把这个别人的【源码】+【数据权重】下载到自己的电脑中,但是确实别人大公司的显卡群
本地模型部署的优势是:1.可以自己在原本的 模型上魔改 2.保护隐私 3.不受别人ai网站每分钟询问量的限制

11.你觉得这个项目在 真实生产环境 中存在哪些不足?(例如性能、可靠性、安全性)

10.AI应用服务平台

和五子棋其实没有特别大的差别。
只是把前端下棋的棋盘换成了,聊天记录。每向聊天入口询问一次,就是把聊天记录打包成有JSON的HttpRequst给服务器。

服务器初始化
用户:wj --》服务器:拿个DBConnection执行sql语句–》服务器:得到了wj的聊天上下文–》服务器:发给ai网站。

有用户登入后,第一步服务器初始化,需要将该用户的聊天上下文从【数据库MySQL】加载到【服务器内存】中,加载到指定用户的AIHelper中的messages数组中。
在这里插入图片描述
因为我有一个【集成的数据库连接池】,运用这个DBConnection,执行sql语句(把聊天上下文都查询一下)。
加载到HttpServer的内存后,再通过Http字符串格式发送到ai网站中去

Http框架:
在这里插入图片描述
业务层:
开始执行自己手写的处理函数ChatHandler()
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

RabbitMQ消息队列

是httpServer需要在MySQL中写数据,会用到的一个模块,(MySQL-》server并不会用到这个)。
RabbitMQ消息队列异步写入
在这里插入图片描述

业务层是处理AI对话ChatSendHandler

这个处理器调用了Session模块,实现了一个上下文记忆的功能。
在这里插入图片描述

在这里插入图片描述

一些常识

api-key是什么?
你要调用这个ai平台的api,所需要的密码
docker是什么?
一个打包了各种环境的镜像,(ubantu的系统是多少,MySQL的系统是多少)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐