ChatNova——beast实现http服务器
GateServer
网关服务器主要应答客户端基本的连接请求,包括根据服务器负载情况选择合适服务器给客户端登录,注册,获取验证服务等,接收http请求并应答。
1.boost的安装
Boost是一个广泛使用的C++库集合,它提供了许多增强C++功能的工具和组件。Boost库包括了用于高性能计算、并发编程、数据结构、算法、正则表达式、数学、图形、网络编程等方面的工具。
特点:
- 跨平台支持:Boost可以在多个操作系统和编译器上使用,包括Windows、Linux、macOS等。
- 广泛的标准库扩展:Boost库在很多方面补充和扩展了C++标准库,比如提供智能指针(boost::shared_ptr)和容器类型(boost::unordered_map)等。
- 高效能:Boost的许多实现专注于性能,经过精心优化,适合高性能计算任务。
- 与C++标准库兼容:许多Boost库的实现是C++标准库的一部分,因此它们的代码常常是可移植的,并且可以随着C++标准库的进化而优化。
常见的Boost库包括:
Boost.Asio:提供了异步输入输出(I/O)的支持,特别适用于网络编程。
Boost.Spirit:用于语法分析,能够帮助你解析和生成各种格式的文本数据。
Boost.Thread:提供多线程编程支持。
Boost.Filesystem:用于处理文件系统操作,如文件路径、目录遍历等。
Boost.Regex:提供正则表达式的处理功能。
Boost.Math:提供各种数学函数,特别是对于高精度数学计算非常有用。
2.beast实现http服务器
Beast 是 Boost 库中的一个组件,专门用于处理 HTTP 和 WebSocket 协议。它提供了一组高效且易于使用的工具,用于构建 HTTP 客户端和服务器应用,以及 WebSocket 客户端和服务器。Beast 结合了 Boost.Asio(用于异步 I/O)和 C++11/14/17 的特性,使得编写高效的网络应用程序变得更加简洁。
几个基本概念:
事件循环:事件循环(Event Loop)是一种编程模式,通常用于异步编程和并发程序设计。它的主要目的是在单线程环境下,通过不断地等待和处理事件来实现高效的 I/O 操作和任务调度。
事件循环的工作原理可以简要描述为:
- 初始化阶段:程序启动时,事件循环被创建并开始运行。它会持续监控和处理事件(例如用户输入、网络请求、定时任务等)。
- 等待事件:事件循环会等待某些事件的发生,常见的事件类型有:
- I/O 事件:如网络请求、文件读取、写入等。
- 定时事件:如定时器超时、定期执行某个操作等。
- 用户交互:如用户点击按钮、键盘输入等。
- 处理事件:当事件发生时,事件循环会根据不同的事件类型调用相应的回调函数或处理逻辑。比如,当接收到网络数据时,它会触发一个回调函数来处理这些数据。
- 重复循环:一旦当前事件处理完毕,事件循环会继续等待下一个事件。这个过程会一直持续,直到程序退出。
回调函数能不能运行很久?回调函数不能运行很久的根本原因
回调函数的执行时间问题与其在异步编程模型中的位置和作用有关。
异步 I/O 模型中的回调:
在 Boost.Asio 或 Node.js 等事件驱动的编程模型中,回调函数通常是由事件循环在事件发生时调度执行的。
如果回调函数做了长时间的计算或等待(例如 I/O 操作、网络请求),它会阻塞事件循环或当前线程,这会导致 后续事件无法及时处理,影响程序的响应性和吞吐量。
线程阻塞问题:
如果回调函数运行时间过长(例如涉及大量计算),它会占用当前线程(或者线程池中的一个线程),导致该线程无法处理其他事件。如果这个线程是事件循环的核心线程(例如 io_context.run() 运行的线程),则所有后续事件都无法处理,这会导致系统卡顿或响应延迟。
添加一个Cserver类
作用:
代码说明
前置:enable_shared_from_this
是 C++ 标准库中的一个模板类,用于 允许一个对象通过 shared_ptr 获取指向自身的 shared_ptr,即使它最初是通过裸指针或其他方式创建的。
解释
enable_shared_from_this 是一个基类模板,当类继承自它时,该类的对象可以通过 shared_ptr 安全地访问自己。这是通过shared_from_this()
成员函数实现的。
当对象是由 shared_ptr 管理时,它会保存一个引用计数,这样可以确保对象在不再被引用时被销毁。当你希望能够通过 shared_ptr 来管理对象并确保其生命周期时,enable_shared_from_this 提供了一个安全的方式来获取指向该对象的 shared_ptr。
CServer.h声明acceptor, 以及用于事件循环的上下文iocontext,和构造函数
class CServer:public std::enable_shared_from_this<CServer> |
CServer的构造函数
初始化了一个 TCP 服务器对象。构造函数的作用是通过 boost::asio::io_context 创建网络监听套接字(_acceptor),并准备好接收客户端连接。
//boost::asio::io_context& ioc:boost::asio::io_context& ioc: |
成员初始化列表:_ioc(ioc)
这行将构造函数参数 ioc 初始化为 CServer 类的成员变量 _ioc。通过这个成员变量,CServer 类的实例可以与 io_context 交互,注册和处理异步事件。
_acceptor(ioc, tcp::endpoint(tcp::v4(), port))
这一行代码创建了一个 tcp::acceptor 对象 _acceptor,用于监听传入的 TCP 连接。具体含义是:
tcp::acceptor
是 Boost.Asio 用于接收传入连接的类。
ioc:
将 io_context 传递给 _acceptor,以便它可以将接收到的连接事件注册到事件循环中。
tcp::endpoint(tcp::v4(), port)
:创建一个 tcp::endpoint,即一个 IP 地址和端口的组合,用于指定监听的地址和端口。
tcp::v4():
表示使用 IPv4 协议(你也可以使用 tcp::v6() 来选择 IPv6)。
port:
表示监听的端口号(例如 8080)。_socket(ioc)
这一行创建了一个 tcp::socket 对象 _socket,它表示与客户端建立连接的通信套接字。ioc 作为参数传递给 _socket,允许它将事件注册到事件循环中。
_socket 还没有连接到任何客户端,它只是一个未连接的套接字,准备与客户端建立连接。
CServer
void CServer::Start() |
HttpConnection
类文件
|
_buffer 用来接受数据
_request 用来解析请求
_response 用来回应客户端
_deadline 用来做定时器判断请求是否超时
start函数
void HttpConnection::Start() |
http::async_read(...) {...});
Boost.Beast 提供的函数,用于 异步读取 HTTP 请求,它做了几件事:
_socket:与客户端建立的 TCP 连接。
_buffer:一个 beast::flat_buffer 类型的缓冲区,用来临时存储接收到的数据。
_request:一个 http::requesthttp::string_body 类型的对象,用于解析并保存 HTTP 请求的内容。
lambda 回调函数:当请求读完后调用。
🟢 本质上:这一行代码表示 注册一个异步读操作,等客户端发来 HTTP 请求时再执行下面的回调函数。
实现HandleReq
void HttpConnection::HandleReq() { |
为了方便我们先实现Get请求的处理,根据请求类型为get调用LogicSystem的HandleGet接口处理get请求.
LogicSystem
|
|
_post_handlers和_get_handlers分别是post请求和get请求的回调函数map,key为路由,value为回调函数。
RegGet函数
void LogicSystem::RegGet(std::string url, HttpHandler handler) { |
在构造函数中实现具体的消息注册
LogicSystem::LogicSystem() { |
HandleGet函数
bool LogicSystem::HandleGet(std::string path, std::shared_ptr<HttpConnection> con) { |
主函数
int main() |
目前无法实现带参数的get请求,所以要加解析url的处理方法
解决参数解析的问题
URL 参数解析总结:
这段代码实现了 URL 编码、URL 解码 和 GET 请求的参数解析。我们可以将其分为几大部分:十六进制与十进制转换,URL 编码与解码,GET 请求的参数解析,以及如何在 HTTP 连接中实现这些操作。下面是整个流程和方法的总结:
1. 十进制字符转为十六进制字符
在 URL 编码中,字符需要转化为十六进制的 ASCII 码值,通常用于将特殊字符转换成 %
形式。例如,字符 'A'
转换成十六进制就是 %41
。
// 十进制转十六进制 |
- 对于
0-9
的数字,直接加 48,得到对应的 ASCII 码值。 - 对于大写字母
A-Z
和小写字母a-z
,分别加上 55 和 87,以转换成相应的 ASCII 码。
2. 十六进制字符转为十进制字符
unsigned char FromHex(unsigned char x) { |
这个函数将十六进制字符(如
'A'
、'1'
)转为相应的十进制数值。'A'
到'Z'
对应的是 10 到 15,使用x - 'A' + 10
来处理。'0'
到'9'
则直接转换为 0 到 9。
3. URL 编码
UrlEncode
函数用于将字符串中的特殊字符编码为 %
加十六进制值(例如空格变为 +
,特殊字符变为 %xx
格式)。
std::string UrlEncode(const std::string& str) { |
- 如果字符是字母或数字,直接拼接。
- 如果是空格,转换为
+
。 - 对于其他字符,使用
%
加上该字符的 ASCII 码的十六进制表示。
4. URL 解码
UrlDecode
函数实现了 URL 解码,即将 URL 编码的字符串(如 %20
)还原成原始字符。
std::string UrlDecode(const std::string& str) { |
- 将
+
转换为空格。 - 对于
%xx
格式的字符,使用FromHex
将十六进制还原为原字符。
5. GET 请求参数解析
在 HttpConnection
类中,解析 URL 中的查询字符串(?key1=value1&key2=value2
)并提取出参数键值对。
void HttpConnection::PreParseGetParam() { |
- 该方法从请求中提取出 URL 和查询参数,使用
UrlDecode
解码键值对并保存到_get_params
中。
6. 处理 GET 请求的逻辑
在 HandleReq
中,当收到 GET
请求时,调用 PreParseGetParam
解析查询参数,并将处理后的数据传递给 LogicSystem
:
void HttpConnection::HandleReq() { |
7. 处理 GET
请求的回调
LogicSystem::LogicSystem() { |
- 通过
RegGet
注册一个回调函数,当请求路径是/get_test
时,会返回请求的参数列表。
总结
URL 编码与解码:
- 编码:将特殊字符转为
%xx
格式,空格转为+
。 - 解码:将
%xx
转换回字符,+
转为空格。
- 编码:将特殊字符转为
GET 请求参数解析:
- 提取查询字符串(
?key1=value1&key2=value2
),并将每对键值进行解码和存储。
- 提取查询字符串(
请求处理:
- 通过
PreParseGetParam
解析 URL 查询参数,然后通过回调将参数传递给相应的处理函数。
- 通过
这些方法有效地实现了 URL 的编码、解码以及 GET 请求参数的解析,为 HTTP 请求的处理提供了便利。如果你有进一步的问题,随时可以问我!
思考:
1.端口和socket的联系是什么
端口(port)和套接字(socket)是计算机网络中密切相关的两个概念,它们在网络通信中共同起作用。
端口(port)和套接字(socket)是计算机网络中密切相关的两个概念,它们在网络通信中共同起作用。下面从定义、联系和举例三个方面来说明:
一、基本定义
概念 | 定义 |
---|---|
端口(Port) | 用于区分同一主机中多个网络应用程序的“编号”,每个网络服务通常绑定一个特定的端口号。是一个 16 位无符号整数(范围 0 ~ 65535)。 |
套接字(Socket) | 网络通信的端点,由 IP 地址 + 端口号 + 协议(TCP/UDP)三元组构成,可以看作是进程间通信的“插口”。 |
二、二者的联系
Socket依赖端口来标识进程:
- 网络中的通信本质上是“进程与进程”的通信,而端口就是用来标识不同进程的。
- Socket 是一个编程接口,用于创建连接,通过绑定(bind)端口才能监听或建立连接。
完整的Socket地址 = IP + Port:
- 客户端连接服务器,其实就是连接“某台主机的某个端口”。
- 比如:连接
192.168.1.100:80
,表示访问该主机上监听 80 端口的服务(通常是 HTTP 服务)。
一个Socket可绑定一个端口,但一个端口只能绑定一个Socket(TCP):
- TCP连接时,如果两个套接字绑定了同一个端口,系统会报错(除非设置SO_REUSEADDR)。
三、类比理解(便于初学者)
实体 | 类比 | 说明 |
---|---|---|
IP地址 | 电话区号 | 表示是哪台机器 |
端口号 | 电话分机号 | 表示该机器上的哪一个服务 |
套接字 | 电话插孔 | 把IP和端口连接在一起,成为一个“可以打电话”的接口 |
四、示例说明
// 伪代码:创建一个 socket 并绑定端口 |
此处:
- 端口:8080
- Socket:sockfd 代表的 socket 绑定了这个端口
- 服务程序就可以通过这个 socket 接收来自客户端的连接
五、总结一句话
端口是数字,用来区分不同进程;Socket 是编程接口,通过绑定端口来实现网络通信。
如需深入了解 Socket 编程模型
、端口复用
或 TCP 四元组
,我可以进一步讲解。
2.为什么使用 enable_shared_from_this
- 确保对象的生命周期:在成员函数中需要使用 shared_ptr 来保持对象的生命周期。例如,可能需要将对象传递给其他函数或线程,确保对象在没有其他 shared_ptr 引用时被销毁。
- 避免悬挂指针:如果通过裸指针返回指向对象的 shared_ptr,可能会导致悬挂指针(指向已销毁对象的指针)。enable_shared_from_this 确保了通过 shared_ptr 返回对象的正确生命周期管理。
- 异步操作是延迟执行的,如果没有保留对当前对象的引用,那么在回调触发前对象可能已经销毁,会造成访问空悬指针、崩溃等问题。
你的问题非常好,让我们一步一步地来彻底解释这句代码:
3. 什么是 io_context
?
boost::asio::io_context
是 Boost.Asio 的事件调度器,它的作用是:
管理和调度所有异步任务,比如
async_accept()
、async_read()
、async_write()
等。
本质上它就是一个事件循环机制。
它类似一个“任务大本营”或“事件处理中心”。
4. 那 ioc{ 1 }
中的 1
是什么意思?
io_context ioc{ 1 };
中的1
不是指定线程数,而是初始化 io_context 内部任务计数器(work count)的方式。并告诉它:我大概只会用 1 个线程来调用run()
|