项目5 |HTTP服务框架

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服务器 时遇到了哪些挑战?

  1. 将muduo框架与httpSever整合。线程池与连接池整合。
    "最大的挑战是如何【在 Reactor 模型下】保证 【EventLoop循环】 不阻塞。 因为 Muduo 的 IO 线程必须快速响应,不能在耗时业务上停留太久。所以我必须把耗时的业务逻辑(比如数据库查询、五子棋 AI 计算)剥离出去。 我的解决办法是:引入了线程池+连接池。
  1. 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.你开发的中间件模块有哪些功能,它们如何增强了系统扩展性?

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

不想继续了,感觉没有很大的意义,东西太多了,面试的时候也不太好复习,东西太多了!

相关推荐
fy zs18 小时前
网络编程套接字
linux·服务器·网络·c++
yuanmenghao18 小时前
CAN系列 — (8) 为什么 Radar Object List 不适合“直接走 CAN 信号”
网络·数据结构·单片机·嵌入式硬件·自动驾驶·信息与通信
CCPC不拿奖不改名18 小时前
网络与API:HTTP基础+面试习题
网络·python·网络协议·学习·http·面试·职场和发展
乾元18 小时前
无线定位与链路质量预测——从“知道你在哪”,到“提前知道你会不会掉线”的网络服务化实践
运维·开发语言·人工智能·网络协议·重构·信息与通信
切糕师学AI18 小时前
SSL是什么?
网络协议
ghostwritten18 小时前
Kubernetes 网络模式深入解析?
网络·容器·kubernetes
Tao____18 小时前
企业级物联网平台
java·网络·物联网·mqtt·网络协议
OpsEye18 小时前
Redis 内存碎片的隐形消耗——如何用 memory purge 命令释放空间?
运维·网络·数据库·redis·缓存·内存·监控
say_fall19 小时前
微机原理:微型计算机基础
服务器·网络·单片机·微机原理