1. 项目在做什么
这是一个基于 Boost.Asio + Boost.Beast 的最小 HTTP Server。
它的核心目标是:
- 监听 TCP 端口
- 读取客户端发来的 HTTP request
- 根据 request 的 method/path 选择处理逻辑
- 组装 HTTP response
- 异步写回给客户端
当前工程的构建入口是 CMakeLists.txt,真正的服务端逻辑主要都在 src/server.cpp。
2. HTTP 的基本概念
HTTP 可以先理解成一种"客户端和服务器约定好的文本协议"。
一次最常见的流程是:
- 浏览器或
curl发一个 request - 服务器解析 request
- 服务器返回一个 response
2.1 Request 是什么
HTTP request 一般由三部分组成:
- 请求行
- 请求头
- 请求体
例如:
http
GET /health HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: curl/8.9.1
Accept: */*
这里可以这样理解:
GET:method,请求方法/health:target,请求目标,通常就是路径HTTP/1.1:协议版本Host、User-Agent、Accept:headers,请求头- 空行后面的内容:body,请求体;GET 通常没有 body
在你的项目里,请求类型是:
cpp
http::request<http::string_body>
这表示:
- 这是一个 HTTP request 对象
- body 用字符串保存
2.2 Response 是什么
HTTP response 也由三部分组成:
- 状态行
- 响应头
- 响应体
例如:
http
HTTP/1.1 200 OK
Server: http_server
Content-Type: text/plain; charset=utf-8
Content-Length: 2
ok
这里可以这样理解:
200 OK:状态码 + 状态说明Server、Content-Type、Content-Length:响应头ok:响应体
在你的项目里,响应类型是:
cpp
http::response<http::string_body>
也就是"响应头 + 字符串形式的 body"。
3. 这个项目里的 HTTP 处理链路
你可以把项目理解成下面这条链:
main -> Listener -> HttpSession -> Router -> Response -> async_write
3.1 main:准备事件循环和监听器
main() 的骨架大致是这样:
cpp
int main(int argc, char **argv) {
unsigned short port = 8080;
int threads = 2;
net::io_context ioc;
auto endpoint = tcp::endpoint(tcp::v4(), port);
Router router;
std::make_shared<Listener>(ioc, endpoint, router)->run();
ioc.run();
}
它主要做了几件事:
- 创建
net::io_context ioc; - 创建监听地址和端口
- 创建
Router - 创建
Listener - 调用
ioc.run()启动事件循环
这里的 io_context 可以理解成:
- 整个异步程序的"调度中心"
- 所有异步 accept/read/write 都挂在它上面
3.2 Listener:负责 accept 新连接
Listener 这一层的骨架可以先记成:
cpp
class Listener : public std::enable_shared_from_this<Listener> {
public:
Listener(net::io_context &ioc, tcp::endpoint endpoint, const Router &router);
void run() { do_accepet(); }
private:
void do_accepet();
void on_accept(beast::error_code ec, tcp::socket socket);
};
它的职责是:
- 打开
acceptor - 绑定端口
- 开始监听
- 反复接收新连接
关键函数:
run():启动 acceptdo_accepet():发起异步 accepton_accept():拿到一个新的tcp::socket
当接收到新连接时,会创建一个 HttpSession:
cpp
std::make_shared<HttpSession>(std::move(socket), router_)->run();
意思就是:
- 每个客户端连接对应一个 session
- session 负责这个连接后续的收发
3.3 HttpSession:负责读请求、写响应
HttpSession 这一层的骨架可以先看成:
cpp
class HttpSession : public std::enable_shared_from_this<HttpSession> {
public:
void run();
private:
void do_read();
void on_read(beast::error_code ec, std::size_t bytes_transferred);
void on_write(bool need_eof, beast::error_code ec, std::size_t bytes_transferred);
};
它是项目里最重要的一层。
成员里几个关键对象:
beast::tcp_stream stream_
负责底层 socket 收发,同时比裸 socket 更方便做超时控制beast::flat_buffer buffer_
保存读到但还没完全解析的数据http::request<http::string_body> request_
保存解析出来的 HTTP 请求std::shared_ptr<void> res_holder_
保证异步写的时候 response 还活着
3.4 do_read:异步读取一个 HTTP request
读取请求的核心代码是:
cpp
http::async_read(stream_, buffer_, request_,
beast::bind_front_handler(&HttpSession::on_read, shared_from_this()));
这句的含义是:
- 从
stream_里异步读取数据 - 用
buffer_当解析缓冲区 - 解析结果放进
request_ - 读完整个 HTTP request 后,回调
on_read(...)
这里的"读完整个请求"不是只读一行,而是:
- 请求行
- 所有 header
- 如果有 body,也会一起读到完整
3.5 on_read:request 到手后怎么处理
on_read() 的核心结构可以概括成这样:
cpp
if (ec == http::error::end_of_stream) {
return do_close();
}
if (ec == http::error::body_limit) {
send_(413 response);
}
if (ec) {
send_(400 response);
}
save_req_for_log(request_);
router_.route(request_, send_);
这里是 request 处理的核心分叉点:
- 如果是
end_of_stream,说明对方准备关闭连接,调用do_close() - 如果是
body_limit,返回413 Payload Too Large - 如果是其他解析错误,理论上应该返回
400 Bad Request - 如果读取成功,就应该进入业务路由
也就是说,on_read() 这一层做的是:
- 先处理协议级错误
- 再把合法请求交给路由层
4. Router:根据 request 生成 response
Router 的骨架大致是这样:
cpp
class Router {
public:
template <class Send>
void route(const http::request<http::string_body> &request, Send &&send);
};
它的职责非常明确:
- 看 request 是什么
- 构造对应 response
- 通过
send(...)交给 session 发回去
4.1 读取 request 的关键信息
Router::route() 一开始的关键代码就是:
cpp
auto target = std::string_view(request.target().data(), request.target().size());
auto path = path_only(target);
它并不是一上来就"判断是不是 /health",而是先把请求目标整理成更适合路由判断的形式。
这里做了两件事:
request.target()取出请求目标,例如/health?x=1path_only(...)去掉 query string,只保留路径部分,例如/health
为什么要先做这一步?
- 因为路由通常关心的是"访问哪个资源",也就是路径本身
- query string 更像是"这个资源附带了哪些参数",它通常不参与最粗粒度的路由匹配
- 如果不先把
/health?x=1变成/health,那么同一个接口可能会因为带不带参数而匹配失败
整理完 path 之后,Router 再同时看两个维度:
- 请求方法是不是
GET - 请求路径是不是
/health
也就是说,这里的路由规则本质上是:
- 只接受"读取健康检查接口"的请求
- 对应到 HTTP 语义上,就是
GET /health
你可以把它理解成一个很小的分发表:
- method 决定"你想对资源做什么"
- path 决定"你想访问哪个资源"
- 两者一起才能唯一决定该走哪个处理逻辑
4.2 构造 response
当请求命中 GET /health 时,Router 会现场组装一个 HTTP 响应对象。
这个响应对象里有几类关键信息:
http::status::ok
表示状态码 200request.version()
响应一般沿用请求的 HTTP 版本response.keep_alive(request.keep_alive())
表示是否保持长连接,通常跟着客户端请求走response.set(...)
设置响应头response.body() = "ok"
设置响应体content_length(...)
设置 body 长度
如果把这段逻辑翻译成人话,其实就是:
- "这个请求我认识,而且处理成功了"
- "我返回的是一段纯文本"
- "正文内容是
ok" - "连接要不要保持,沿用客户端这次请求的约定"
未命中的路径则返回 404。它表达的不是"程序出错了",而是:
- 状态码:
http::status::not_found - body:
"not found"
也就是"服务器正常工作,但你请求的资源不存在"。
5. Response 是怎么异步发出去的
Router 自己不直接写 socket,它只调用 send(...)。
这个 send 的实现本质上是 HttpSession 里的一个内部结构体 Send。
关键步骤:
cpp
using MsgType = std::decay_t<Msg>;
auto sp = std::make_shared<MsgType>(std::forward<Msg>(msg));
self.res_holder_ = sp;
http::async_write(
self.stream_,
*sp,
beast::bind_front_handler(&HttpSession::on_write, self.shared_from_this(), sp->need_eof()));
含义是:
- 把 response 放进
shared_ptr - 保存到
res_holder_,防止异步写完成前对象被销毁 - 调用
http::async_write(...)异步写回客户端 - 写完后进入
on_write(...)
5.1 为什么要 res_holder_
因为 async_write 是异步的。
如果 response 是局部变量,函数返回后它就没了;但写操作可能还没完成。所以这里一定要把 response 延长生命周期,不然会出现悬空引用问题。
这也是异步网络编程里很常见的一点:
- 回调还没发生
- 但你引用的对象已经析构了
5.2 on_write 做什么
on_write() 的核心结构大致是:
cpp
if (ec) {
return;
}
res_holder_.reset();
if (need_eof) {
return do_close();
}
do_read();
主要做三件事:
- 检查写错误
- 写完后释放
res_holder_ - 如果不需要断开连接,就继续读下一个 request
这说明你的 server 是支持 HTTP keep-alive 思路的:
- 一个 TCP 连接上可以连续处理多个请求
- 不是"一次请求就强制断一次连接"
6. Request 和 Response 在你项目里的对应关系
可以把这部分背成一个简化版流程图:
- 客户端发来 HTTP request
Listener接收到 TCP 连接HttpSession::do_read()异步读取 requestrequest_被填充为http::request<string_body>Router::route()根据 method/path 构造http::response<string_body>Send::operator()发起http::async_write- 客户端收到 HTTP response
你以后看任何 HTTP 服务端代码,都可以先找这 7 步。
7. 这个项目里最重要的几个 HTTP 字段
7.1 method
在 request 里通过 request.method() 拿到。
常见值:
GETPOSTPUTDELETE
你当前只处理了 GET。
7.2 target
在 request 里通过 request.target() 拿到。
它包含:
- 路径
- 可能还带 query string
例如:
/health/health?full=1
7.3 version
通过 request.version() 拿到。
一般会是:
11表示 HTTP/1.1
响应里常常直接复用这个版本。
7.4 keep_alive
通过 request.keep_alive() 拿到客户端是否希望保持连接。
然后在响应里把这个约定延续下去。这样做意味着:
- 客户端如果想复用连接,服务端也配合
- 客户端如果不想保留连接,响应后就关闭
8. Beast 和 Asio 在这个项目里的分工
你可以这样理解:
- Boost.Asio:负责异步 IO、socket、事件循环、线程模型
- Boost.Beast:负责 HTTP 报文的解析与序列化
也就是:
- Asio 管"怎么收发"
- Beast 管"HTTP 长什么样、怎么解析和生成"
9. 当前代码里要特别注意的点
这部分很重要,因为它直接影响你对 request/response 的理解。
9.1 成功读到 request 后,要记得真正路由
成功读取 request 后,不能停在"读到了"这一步,而是要继续做两件事。对应代码就是:
cpp
save_req_for_log(request_);
router_.route(request_, send_);
它的含义是:
- 先把请求信息记下来,方便日志和排错
- 再把请求交给
router_,真正生成响应
如果少了这一步,就会出现:
- request 收到了
- 但没有生成 response
- 客户端看到
Empty reply from server
这正是你前面 curl 测试现象的根因。
9.2 错误分支发送响应后要及时 return
在 body_limit 或其他 ec 分支里,发送错误响应后最好立刻 return;,否则函数会继续往下走,可能出现重复处理。
9.3 400 和 413 不要混掉
body_limit 对应:
413 Payload Too Large
普通解析错误更适合:
400 Bad Request
如果状态码和 body 文案不一致,后面排错会很难受。
9.4 Router 最好保存引用而不是拷贝
HttpSession 和 Listener 当前都把 Router 存成值成员。对这个小项目没问题,但如果以后 Router 里放了共享状态、数据库连接、配置或者缓存,通常会更倾向于:
- 保存引用
- 或保存
shared_ptr<Router>
这样对象语义会更清晰。
10 完整代码
cpp
#include <boost/asio.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast.hpp>
#include <boost/beast/core/bind_handler.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/core/tcp_stream.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/fields.hpp>
#include <boost/beast/http/string_body.hpp>
#include <cstddef>
#include <memory>
#include <iostream>
namespace net = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
using tcp = net::ip::tcp;
static std::string_view path_only(std::string_view target) {
// 只获取到路径部分
auto qpos = target.find('?');
if (qpos == std::string_view::npos) {
return target;
}
return target.substr(0, qpos);
}
// 处理不同的请求
class Router {
public:
template <class Send>
void route(const http::request<http::string_body> &request, Send &&send) {
auto target = std::string_view(request.target().data(), request.target().size());
auto path = path_only(target);
// 处理 GET /health
if (request.method() == http::verb::get && path == "/health") {
http::response<http::string_body> response{http::status::ok, request.version()};
response.keep_alive(request.keep_alive());
response.set(http::field::server, "http_server");
response.set(http::field::content_type, "text/plain; charset=utf-8");
response.body() = "ok";
response.content_length(response.body().size());
return send(std::move(response));
}
// 其他路径:404
http::response<http::string_body> response{http::status::not_found, request.version()};
response.keep_alive(request.keep_alive());
response.set(http::field::server, "http_server");
response.set(http::field::content_type, "text/plain; charset=utf-8");
response.body() = "not found";
response.content_length(response.body().size());
return send(std::move(response));
}
};
class HttpSession : public std::enable_shared_from_this<HttpSession> {
public:
HttpSession(tcp::socket &&sock, const Router &router)
: stream_(std::move(sock)), router_(router), send_{*this} {}
void run() {
stream_.expires_after(std::chrono::seconds(10));
do_read();
}
private:
void do_read() {
request_ = {};
// 每轮请求都刷新一下超时
stream_.expires_after(std::chrono::seconds(10));
http::async_read(stream_, buffer_, request_,
// 绑定回调函数
beast::bind_front_handler(&HttpSession::on_read, shared_from_this()));
}
void on_read(beast::error_code ec, std::size_t bytes_transferred) {
(void)bytes_transferred;
if (ec == http::error::end_of_stream) {
return do_close();
}
// body太大:报413
if (ec == http::error::body_limit) {
save_req_for_log(request_);
// 准备 response
http::response<http::string_body> response{http::status::payload_too_large, request_.version()};
response.keep_alive(request_.keep_alive());
// 设置 header
response.set(http::field::server, "http_server");
response.set(http::field::content_type, "text/plain; charset=utf-8");
// 设置 response 的 body
response.body() = "payload too large";
// 设置 body 的长度
response.content_length(response.body().size());
send_(std::move(response));
}
// 其他解析/读取错误:400
if (ec) {
req_method_for_log_ = "-";
req_target_for_log_ = "-";
// 准备 response
http::response<http::string_body> response{http::status::bad_request, request_.version()};
response.keep_alive(request_.keep_alive());
// 设置 header
response.set(http::field::server, "http_server");
response.set(http::field::content_type, "text/plain; charset=utf-8");
// 设置 response 的 body
response.body() = "bad request";
// 设置 body 的长度
response.content_length(response.body().size());
send_(std::move(response));
}
// 成功读取到一个完整请求
save_req_for_log(request_);
// 这里开始处理请求
router_.route(request_, send_);
}
void on_write(bool need_eof, beast::error_code ec, std::size_t bytes_transferred) {
(void)bytes_transferred;
if (ec) {
std::cerr << "[session] write error: " << ec.message() << "\n";
return;
}
// 写完了释放对response的拥有
res_holder_.reset();
if (need_eof) {
return do_close();
}
do_read();
}
void do_close() {
beast::error_code ec;
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
}
void save_req_for_log(const http::request<http::string_body> &req) {
req_method_for_log_ = std::string(req.method_string());
req_target_for_log_ = std::string(req.target());
}
private:
struct Send {
HttpSession &self;
template <class Msg> void operator()(Msg &&msg) {
// 得到纯净的对象类型
using MsgType = std::decay_t<Msg>;
// 让msg活到async_write完成
auto sp = std::make_shared<MsgType>(std::forward<Msg>(msg));
self.res_holder_ = sp;
http::async_write(self.stream_, *sp, beast::bind_front_handler(&HttpSession::on_write, self.shared_from_this(), sp->need_eof()));
}
};
private:
beast::tcp_stream stream_; // 对 tcp::socket 的轻量封装,额外提供超时控制等便利
beast::flat_buffer buffer_; // 保存未解析完的数据(以及读取缓存)
http::request<http::string_body> request_;
// 异步 write 时response保持存活
std::shared_ptr<void> res_holder_;
// logging
std::string req_method_for_log_ = "-";
std::string req_target_for_log_ = "-";
Send send_{*this};
Router router_;
};
class Listener : public std::enable_shared_from_this<Listener> {
public:
Listener(net::io_context &ioc, tcp::endpoint endpoint, const Router &router) : ioc_(ioc) ,
// acceptor 绑定到一个 strand 上,保证 accept 回调串行(对于多线程 run ioc 很重要)
acceptor_(net::make_strand(ioc)), router_(router) {
beast::error_code ec;
// 1) open
acceptor_.open(endpoint.protocol(), ec);
if (ec) {
throw beast::system_error(ec);
}
// 2) 允许端口复用(服务重启更方便)
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
if (ec) {
throw beast::system_error(ec);
}
// 3) bind
acceptor_.bind(endpoint, ec);
if (ec) {
throw beast::system_error(ec);
}
// 4) listen
acceptor_.listen(net::socket_base::max_listen_connections, ec);
if (ec) {
throw beast::system_error(ec);
}
}
void run() {
do_accepet();
}
private:
void do_accepet() {
acceptor_.async_accept(net::make_strand(ioc_), beast::bind_front_handler(&Listener::on_accept, shared_from_this()));
}
void on_accept(beast::error_code ec, tcp::socket socket) {
if (ec) {
std::cerr << "[listener] accept error: " << ec.message() << "\n";
return;
} else {
std::make_shared<HttpSession>(std::move(socket), router_)->run();
}
do_accepet();
}
private:
net::io_context &ioc_;
tcp::acceptor acceptor_;
Router router_;
};
int main(int argc, char **argv) {
try {
// 命令行参数:
// argv[1] = port(默认 8080)
// argv[2] = threads(默认 1)
unsigned short port = 8080;
int threads = 2;
if (argc >= 2) port = static_cast<unsigned short>(std::atoi(argv[1]));
if (argc >= 3) threads = std::max(1, std::atoi(argv[2]));
// 事件循环 / 调度中心
net::io_context ioc;
// 启动监听器
auto enpoint = tcp::endpoint(tcp::v4(), port);
Router router;
std::make_shared<Listener>(ioc, enpoint, router)->run();
std::cout << "HTTP server listening on 0.0.0.0:" << port
<< " with " << threads << " thread(s)\n";
std::vector<std::thread> workers;
workers.reserve(std::max(0, threads - 1));
for (int i = 0; i < threads - 1; ++i) {
workers.emplace_back([&ioc] { ioc.run(); });
}
// 主线程也跑
ioc.run();
// 等待工作线程退出
for (auto &t: workers) {
t.join();
}
} catch (const std::exception &e) {
std::cerr << "fatal: " << e.what() << "\n";
return 1;
}
return 0;
}
运行该代码,然后在控制台执行命令:
shell
curl -v http://127.0.0.1:8080/health
下面结果:

返回了ok,就代表成功了。