C++后端服务器实现笔记
C++后端服务器实现笔记
仓库
项目结构
1 | ├── .github/ # GitHub配置目录 |
原理解析
1. JWT认证原理
JWT(JSON Web Token)是一种用于在网络应用间安全传输信息的开放标准(RFC 7519)。它通过紧凑且自包含的方式在各方之间以JSON对象形式安全地传输信息。这些信息可以被验证和信任,因为它们是数字签名的。
JWT的核心组成部分包括:
- Header(头部):包含令牌类型和使用的签名算法
- Payload(负载):包含声明(claims),如用户信息和过期时间
- Signature(签名):用于验证消息是否被篡改
在我们的实现中,JWT主要用于身份认证和会话管理。服务器生成JWT后,客户端在后续请求中携带此令牌,服务器通过验证令牌的有效性来确认用户身份。
}
1 |
|
同时,需要有 Base64 解码函数
1 | /** |
2,sha256 签名
SHA-256 是一种安全哈希算法,它可以生成一个256位的哈希值。哈希值是一个固定长度的字符串,用于唯一标识输入数据。SHA-256 算法具有以下特点:
- 输入数据的微小变化会导致哈希值的巨大变化,这使得哈希值具有抗冲突性。也就是说,两个不同的输入数据几乎不可能产生相同的哈希值。
- SHA-256 算法是不可逆的,即无法从哈希值反推出原始数据。
- SHA-256 算法是公开的,任何人都可以使用它来生成哈希值。
- 我们可以将哈希值转成一个固定的长度(例如32个字符)的十六进制字符串,以便于存储和传输。
实现原理:
- 将需要签名的数据转换为字节数组。
- 使用 SHA-256 算法对字节数组进行哈希运算,生成一个256位的哈希值。
- 将哈希值转换为十六进制字符串。
具体算法超级复杂,直接看源码,一大堆异或(其实只要不可逆,怎么算都行),翻转等等
1 | /** |
利用哈希函数实现签名:
1 | /** |
需要给JWT设置标准头表明其类型和哈希算法:
1 | /** |
3,payload部分
payload部分是JWT的主体,用于存放用户信息,一般包括以下字段:
- iss (issuer):签发人
- sub (subject):主题
- aud (audience):受众
- exp (expiration time):过期时间
- nbf (not before):生效时间
- iat (issued at):签发时间
- jti (JWT ID):编号
通俗来讲,就是把字典(json)改成字符串。
1 | /** |
注:JsonValue是自定义的json类,用于序列化和反序列化json字符串,后面会讲到。
4,总体流程
这样,生成token的整体流程如下
获取头部
获取过期时间
获取payload
获取签名
拼接token
1 | /** |
检查token是否过期
先按照"."分割,获取payload部分,然后解析payload部分,获取过期时间,然后和当前时间比较,如果当前时间大于过期时间,则过期。
1 | /** |
解析 Claim
1 | /** |
同时我在其中顺便实现密码哈希
密码哈希流程
- 生成盐值(salt)
- 将盐值与密码进行哈希运算
- 转成16进制字符串
1 | /** |
验证密码
1 | /** |
2,JsonValue 类
在网络得到信息,大部分是json格式,需要解析json,使用JsonValue类
1 | // |
这个类的递归含量比较高,关键方法是 toJson和fromJson
1 | std::string JsonValue::toJson() const { |
3,高并发线程池
对于可能出现的高并发场景,需要使用线程池来管理线程,避免频繁创建和销毁线程带来的性能开销。线程池的实现需要考虑线程的创建、销毁、任务分配、任务执行、任务队列等细节。
在 C++17 以上,可以更加灵活地处理线程池,这也为实现基础服务器实现可能
1 | // |
实际上,就是在任务来的时候,将任务加入队列,并唤醒一个线程来处理任务,线程处理完任务后,继续等待新的任务。这个时候需要加锁,防止多个线程同时操作任务队列。全局用一个锁,保护任务队列同时防止死锁。
当然构造和析构也是要写的
1 |
|
4,服务器实现
服务器实现,就是将客户端的请求,交给线程池处理,然后返回结果。通过上面的几个组件,服务器实现起来就很简单了。
服务器结构
- Request : 接受请求结构体
- Response : 返回结果结构体
- Server : 服务器类
1 | // 请求结构 |
较多代码的原因是添加了body解析,实际上就是按照相应的格式进行解析,部分未实现。
1 | // 响应结构 |
Respond 结构体,用于封装响应信息,包括状态码、头部信息和响应体。其中,json、text、status、success 和 error 方法用于设置不同的响应类型和内容。预设不同响应,对比 Request 结构体比较简单。
通过这两个结构体,定义出路由处理器的格式
1 | // 路由处理器类型 |
然后就可以设计 Server 了。
1 | // 简单的HTTP服务器类 |
这样设计,整个服务器的框架就出来了。
通过 get, post, put, del 方法注册路由,通过 run 方法启动服务器,通过 stop 方法停止服务器。
我们主要看这几个方法
1 | void Server::get(const std::string& path, Handler handler) { |
通过锁保护,将路由注册到路由表中。
1 | // start 和 stop |
这个是服务器的启动和停止,通过信号处理函数来处理程序终止信号,从而安全地关闭服务器。
1 | /** |
先通过 socket 函数创建 IPv4 和 IPv6 的 socket,然后分别调用 bind 和 listen 函数进行绑定和监听。如果 IPv6 的 socket 创建和绑定成功,则启动一个独立的线程来监听 IPv6 连接。在主循环中,使用 accept 函数接受 IPv4 连接,并添加到线程池中处理。
后面每次接到连接,先拿到 clientSocket,是请求在网络上的标识,然后通过 clientSocket 获取到 clientAddress,这个是客户端的地址信息,包括 IP 地址和端口号。然后通过 clientSocket 和 clientAddress 创建一个 ClientSession 对象,这个对象负责处理这个连接的请求。
然后就是 handleClient,用于查找并处理请求。
1 | /** |
流程:
- 1,解析请求,获取请求行、头部、body
- 2,根据请求行中的方法和路径,查找对应的处理程序
- 3,调用处理程序,获取响应
- 4,构建响应字符串
- 5,发送响应
至此,一个简单的C++后端服务器实现完成。其余部分欢迎小伙伴们继续完善。
5. Redis缓存(RDConnector)实现原理
5.1 单例模式设计
RDConnector类采用了单例模式设计,确保整个应用中只有一个Redis连接实例。核心实现包括:
1 | // 静态单例实例指针 |
单例模式的设计确保了应用程序中只维护一个Redis连接,避免了连接资源的浪费和潜在的连接管理问题。
5.2 Redis连接管理
Redis连接的建立和管理是通过hiredis客户端库实现的。连接过程包括以下几个关键步骤:
1 | bool RdConnector::connect() { |
连接管理的特点:
- 连接前检查单例唯一性
- 断开已存在的连接,避免资源泄露
- 支持密码验证,增强安全性
- 支持数据库切换,实现数据隔离
- 详细的错误处理和日志输出
5.3 基本数据操作功能
RDConnector提供了一系列基本的数据操作方法,封装了Redis的常用命令:
5.3.1 获取数据(GET)
1 | std::string RdConnector::get(const std::string& key) { |
2.3.2 设置数据(SET)
1 | bool RdConnector::set(const std::string& key, const std::string& value) { |
2.3.3 设置数据并指定过期时间(SETEX)
1 | bool RdConnector::set(const std::string& key, const std::string& value, int expireSeconds) { |
5.3.4 检查键是否存在(EXISTS)
1 | bool RdConnector::exists(const std::string& key) { |
5.3.5 删除数据(DEL)
1 | bool RdConnector::del(const std::string& key) { |
5.4 错误处理机制
RDConnector实现了完善的错误处理机制:
- 连接错误处理:检查连接是否成功建立
- 命令执行错误处理:捕获并记录命令执行失败的情况
- 类型检查:验证Redis返回数据的类型是否符合预期
- 内存管理:确保每次操作后正确释放Redis回复对象
- 错误信息获取:通过getError()方法获取最近的错误信息
1 | std::string RdConnector::getError() { |
5.5 使用示例
1 | // 初始化Redis连接 |
6. 数据库连接(DBConnector)实现原理
6.1 单例模式设计
DBConnector类采用了单例模式设计,确保整个应用中只有一个数据库连接实例。这种设计模式能够有效管理数据库连接资源,避免频繁创建和销毁连接带来的性能开销。
1 | // 静态单例实例指针 |
单例模式的实现确保了应用程序在任何时候都只能有一个数据库连接对象,从而避免了多线程环境下的连接资源争用问题。
6.2 连接管理
DBConnector通过MySQL C API管理数据库连接,主要包括连接的建立、维护和关闭:
1 | // 构造函数初始化连接参数 |
连接管理的关键特点:
- 构造函数初始化MySQL句柄
- 析构函数自动关闭连接,避免资源泄漏
- connect方法处理连接建立和字符集设置
- 完善的错误处理和日志输出
6.3 查询与执行功能
DBConnector提供了两个核心方法来操作数据库:
6.3.1 query方法(用于SELECT查询)
1 | MYSQL_RES* DBConnector::query(const std::string& sql) { |
6.3.2 execute方法(用于INSERT、UPDATE、DELETE)
1 | bool DBConnector::execute(const std::string& sql) { |
6.4 SQL注入防护
DBConnector实现了SQL注入防护机制,通过escapeSqlLiteral方法对SQL字符串进行转义:
1 | std::string DBConnector::escapeSqlLiteral(const std::string& value) { |
6.5 使用示例
1 | // 初始化数据库连接 |
通过数据库连接和缓存功能的添加,服务器的功能更加完善,能够有效管理数据持久化和缓存,提升应用的性能和响应速度。




