1. 框架梳理

我们在muduo框架中主要解决的是C100K的问题,是传输层中用户空间所要做的事情。
传输层内核空间做的就是TCP,UDP哪些子深奥的东西。即使是陈硕也只是写用户空间的东西哦。
2.HTTP报文解析封装模块
2.1 了解一些基础的知识更方便学习
HTTP(超文本传输协议),客户端浏览器 与 服务端交流的文本
这个是请求报文(和HTTP请求类不是同一个东西)

访问127.0.0.1这个服务器的8080端口。这个8080端口就像muduo框架中的acceptor,监听accpetorchannel只有一个,连接conchannel有N个。
2.2 HttpRequest类(HTTP请求)
将http这个超文本写进类中,可以用成员变量一 一对应请求行呀,请求头呀,请求体呀。非常方便
2.3 HttpContext 上下文类
它是一个状态机。因为tcp报文(http报文)是流式传输的,不能精准获得一个http报文大小的数据。
只有状态机才能避免数据的半包和沾包。
该状态机有三个状态: 我正在解析请求行,我正在解析请求头,我正在解析请求体。遇见/r/n/r/n就切换状态。
在 HttpContext::parseRequest 函数中,它就像一个流水线工人:
`我正在解析请求行`的状态时,先读第一行,设置好 HttpRequest 的 method_ 和 path_。
`我正在解析请求头`的状态时,再读中间的行,一行行塞进 HttpRequest 的 headers_ 字典里。
`我正在解析请求体`最后读剩下的部分,塞进 HttpRequest 的 content_ 里。
2.4 HttpResponse类(HTTP响应)
同HttpRequest类
2.5 HttpServer类
这个类 涉及事件驱动机制。
功能一:新连接建立时回调(onConnection):回调函数(这个函数是回调函数,如果它被调用,会实现一些功能)
给新连接设置一个HttpContext对象用于解析请求报文,提取封装请求信息
功能二:接收连接数据的消息回调(onMessage) :回调函数
服务端程序在接收到跟服务端建立连接的客户端的数据时,会调用该函数。
该函数的作用是:

功能三:在业务层上进行回调函数的注册
注册静态路由处理器
server.Get("/path", cb),把这条路由信息写入Router 内部的那个哈希表(handlers_)中。
当URI为/path时,触发这个cb
cpp
void Get(const std::string& path, const HttpCallback& cb)
{
router_.registerCallback(HttpRequest::kGet, path, cb);
}
3.Router类(路由模块)
3.1路由(Router)模块
route接口 存贮路由信息,将URL与一个回调函数映射
cpp
/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.会话管理模块
会话:一次请求+一次响应不叫会话。只有多次请求+响应叫会话。http只能满足 一次请求+一次响应 时的情况。这时候就需要会话管理模块。
http本来是无状态的,当需要有状态时(比如维持一个登入信息),就需要用到会话模块。例:这个请求是"张三"发的,他已经登录了。
4.1SessionManager(会话管理器)
使得 会话数据可以存储在内存中或持久化到数据库中,以便在服务器重启后恢复。
它持有一个 SessionStorage 的指针。这个 SessionStorage就是session要存的地方
cpp
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,
cpp
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 登出处理器示例
cpp
// 销毁会话
server_->getSessionManager()->destroySession(session->getId());
5.中间件模块

5.1 代码实现
这里以跨域中间件为例来介绍中间件的完整实现流程。
跨域 就是当前主机访问的服务器,不在本地而是去访问别域名的服务器。(比如前端的http://localhost:3000跨域访问后端的http://localhost:8080)
跨域中间件的作用就是给服务器设置一个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基类的子类。
before接口:
cpp
void CorsMiddleware::before(HttpRequest& request) {
// 1. 【核心动作】把"进来的信"先扣在手里
request_ = &request;
}
after接口:
cpp
void CorsMiddleware::after(HttpResponse& response) {
// 1. 【回看】从口袋里掏出刚才存的那封"来信"
// 看看这封信是谁寄来的(查看工牌 Origin)
const std::string& origin = request_->getHeader("Origin");
// 2. 【核对】去查白名单(CorsConfig)
// "老板允许 http://localhost:3000 的人拿数据吗?"
if (isOriginAllowed(origin)) {
// 3. 【盖章】如果允许,就在"回信"上盖个章
// 章的内容是:Access-Control-Allow-Origin: http://localhost:3000
addCorsHeaders(response, origin);
}
}
5.1.5 总结流程

5.1.6 为什么需要跨域中间件
首先,我们得明白,这是一个前后端分离的项目。
前端服务器,http://localhost:3000。它只存了 .html 文件、.css 样式文件、.js 脚本文件(也就是网页的前端,一些登录按钮呀)。
后端服务器,http://localhost:8080,它存了五子棋的 AI 算法、数据库连接、登录逻辑。

6. 集成数据库连接池模块
6.1 DbConnection数据库的单个连接
我这个业务逻辑中,解析完了httpRequest后,发现需要访问数据库。
但是每一个客户端访问服务端的数据库都需要id password登入数据库,
这太费时了。
所以直接让服务端中常驻几个DbConnection对象,会大大加快速度。
我们创建一个连接池,只要线程需要,就去拿。
6.1.1 DbConnection::DbConnection()构造函数接口。
当你创建一个 DbConnection 对象时,顺便立刻连上 MySQL 数据库。
使用的是封装好的 driver,直接给你搞好tcp三次握手
6.1.2 DbConnection::executeQuery()接口,执行查询
sql注入 :我写一个sql查询语句SELECT * FROM users WHERE id = ?
使用这个接口,因为PreparedStatement()的关系,?这个地方只会填入字符串。(比如 0602)。如果没有这个PreparedStatement()约束,在?处黑客会直接写一些sql语句,导致错误
6.2 数据库连接池
连接池和线程池,虽然都叫池,但是有很大的不同!
线程池,【生产者】是主线程生成任务,消费者是【消费者】消费任务。【任务队列】用来存任务。
连接池是不管主线程和子线程,【队列】中存储的是【已经初始化好的空闲DBConnection】,【消费者】是线程,需要一个DBConnection,所以 getConnection() 。【生产者】也是线程,用完了还回queue中,所以releaseConnection()
cpp
#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握手的操作
cpp
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.框架应用之卡码五子棋
路由信息一
路由信息中
cpp
"/login EntryHandler::handle
当有一个httpRequest的URI是这个时,触发handle接口。
这个接口执行以下步骤:
这个接口在磁盘中找".../WebApps/GomokuServer/resource/entry.html"


text/html中的内容
html
<html>
<body>
<h1>欢迎来到五子棋对战平台</h1>
<button>开始游戏</button>
</body>
</html>
最后【服务端】把这个httpResponse发给【客户端】浏览器。
路由信息二
路由信息
html
/在线人数 getBackendData()
当前端有人按了查询在线人数的 按钮时,执行getBackendData()接口。
这个接口执行流程如下:
把后端的数据=》json格式=》前端的httpResponse中
9.面试
1. 你在开发自定义HttpServer框架中具体负责了哪些部分?
- http模块:http请求报文+响应报文的报文解析封装模块
2.你在实现 基于Reactor模型 的 高性能HTTP服务器 时遇到了哪些挑战?
- 将muduo框架与httpSever整合。线程池与连接池整合。
"最大的挑战是如何【在 Reactor 模型下】保证 【EventLoop循环】 不阻塞。 因为 Muduo 的 IO 线程必须快速响应,不能在耗时业务上停留太久。所以我必须把耗时的业务逻辑(比如数据库查询、五子棋 AI 计算)剥离出去。 我的解决办法是:引入了线程池+连接池。

- TCP 的粘包与 HTTP 的完整性解析
用httpContext状态机完美解决。
-
没有考虑数据库连接超时的问题。
昨天晚上登入了,但没关掉网页,第二天发现网站打不开了。
这是因为DBconnection因为闲置太久,过了8小时mysql会单方面切断TCP连接。但服务器端还不知道,
调用连接池的getConnection() 高高兴兴地把这个"已经死掉"的连接对象借给了线程,才会报错。
解决:可以在连接池中添加一个接口,这个接口就是每过一个小时,把pool里的DBconnection在一个后台线程中全跑一遍,这样就不会达到8小时了。
-
垃圾回收机制
- 有共享的智能指针。比如httpcontext,没有一个客户端和服务器都有一个,有一个就+1
- 连接池的回收。服务器用完DBconnection,不是销毁,而是重新放回【连接池】
- RALL,智能锁自己销毁
3.描述一下你是如何集成OpenSSL来实现HTTPS支持的。
- http:lisFD听到有连接来了---》建立http连接
- https:lisFD听到有连接来了--》ssl四次握手加密---》建立http连接
4.你设计的动态路由管理系统是如何支持多种HTTP请求方法的?
为每种HTTP 请求方法创建一个独立的路由表。有静态路由+动态路由
5.会话管理功能是如何实现的,你如何处理会话超时?
我的项目中有一个会话管理模块。
6.你开发的中间件模块有哪些功能,它们如何增强了系统扩展性?
跨域中间件,连接前端服务器和后端服务器。