boost.asio网络学习:Http Server

1. 项目在做什么

这是一个基于 Boost.Asio + Boost.Beast 的最小 HTTP Server。

它的核心目标是:

  • 监听 TCP 端口
  • 读取客户端发来的 HTTP request
  • 根据 request 的 method/path 选择处理逻辑
  • 组装 HTTP response
  • 异步写回给客户端

当前工程的构建入口是 CMakeLists.txt,真正的服务端逻辑主要都在 src/server.cpp

2. HTTP 的基本概念

HTTP 可以先理解成一种"客户端和服务器约定好的文本协议"。

一次最常见的流程是:

  1. 浏览器或 curl 发一个 request
  2. 服务器解析 request
  3. 服务器返回一个 response

2.1 Request 是什么

HTTP request 一般由三部分组成:

  1. 请求行
  2. 请求头
  3. 请求体

例如:

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:协议版本
  • HostUser-AgentAccept:headers,请求头
  • 空行后面的内容:body,请求体;GET 通常没有 body

在你的项目里,请求类型是:

cpp 复制代码
http::request<http::string_body>

这表示:

  • 这是一个 HTTP request 对象
  • body 用字符串保存

2.2 Response 是什么

HTTP response 也由三部分组成:

  1. 状态行
  2. 响应头
  3. 响应体

例如:

http 复制代码
HTTP/1.1 200 OK
Server: http_server
Content-Type: text/plain; charset=utf-8
Content-Length: 2

ok

这里可以这样理解:

  • 200 OK:状态码 + 状态说明
  • ServerContent-TypeContent-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():启动 accept
  • do_accepet():发起异步 accept
  • on_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=1
  • path_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
    表示状态码 200
  • request.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()));

含义是:

  1. 把 response 放进 shared_ptr
  2. 保存到 res_holder_,防止异步写完成前对象被销毁
  3. 调用 http::async_write(...) 异步写回客户端
  4. 写完后进入 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();

主要做三件事:

  1. 检查写错误
  2. 写完后释放 res_holder_
  3. 如果不需要断开连接,就继续读下一个 request

这说明你的 server 是支持 HTTP keep-alive 思路的:

  • 一个 TCP 连接上可以连续处理多个请求
  • 不是"一次请求就强制断一次连接"

6. Request 和 Response 在你项目里的对应关系

可以把这部分背成一个简化版流程图:

  1. 客户端发来 HTTP request
  2. Listener 接收到 TCP 连接
  3. HttpSession::do_read() 异步读取 request
  4. request_ 被填充为 http::request<string_body>
  5. Router::route() 根据 method/path 构造 http::response<string_body>
  6. Send::operator() 发起 http::async_write
  7. 客户端收到 HTTP response

你以后看任何 HTTP 服务端代码,都可以先找这 7 步。

7. 这个项目里最重要的几个 HTTP 字段

7.1 method

在 request 里通过 request.method() 拿到。

常见值:

  • GET
  • POST
  • PUT
  • DELETE

你当前只处理了 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 最好保存引用而不是拷贝

HttpSessionListener 当前都把 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,就代表成功了。

相关推荐
-许平安-2 小时前
MCP项目笔记三(server)
网络·c++·笔记·mcp
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第八章指针之循环后移
c语言·c++·算法
福楠2 小时前
C++ | 哈希的应用
开发语言·c++·哈希算法
乾元2 小时前
安全官(CISO)的困惑:AI 投入产出比(ROI)的衡量
网络·人工智能·安全·网络安全·chatgpt·架构·安全架构
快乐柠檬不快乐2 小时前
C++中的代理模式实现
开发语言·c++·算法
良木生香2 小时前
【C++初阶】:C++类和对象(上):类的定义 & 类的实例化 & this指针
c语言·开发语言·c++
marsh02062 小时前
13 openclaw数据验证与过滤:确保应用安全性的第一道防线
网络·数据库·ai·编程·技术
70asunflower2 小时前
CUDA基础知识巩固检验练习题【附有参考答案】(8)
c++·人工智能·cuda
终端鹿2 小时前
深度解析 WebSocket DevTools 插件
网络·websocket·网络协议