Skip to content

WebServer 项目实战 ​

基于 Boost.Asio 实现的异步 HTTP 静态服务器,C++17 + CMake 构建。

一、项目结构 ​

day0703-webserver/
├── CMakeLists.txt
├── inc/
│   ├── WebServer.hpp            # 服务器类(单例,负责监听)
│   ├── HttpSession.hpp          # 会话类(读请求→解析→处理→响应)
│   ├── HttpRequest.hpp          # 请求结构体
│   ├── HttpResponse.hpp         # 响应报文封装
│   ├── RequestHandler.hpp       # 处理器抽象类(策略模式)
│   ├── FileHandler.hpp          # 静态文件处理器
│   ├── ApiHandler.hpp           # API 处理器
│   ├── Router.hpp               # 路由分发
│   ├── RequestHandlerContext.hpp# RAII 管理处理器
│   └── FileUtils.hpp            # 文件读取 / MIME 判断
└── src/
    ├── main.cpp
    ├── WebServer.cpp
    └── HttpSession.cpp

二、HTTP 协议要点 ​

一个 HTTP 请求报文,头部以 \r\n\r\n 结束(这是读取的依据):

GET /index.html HTTP/1.1\r\n     ← 请求行:方法 URI 版本
Host: localhost:8888\r\n
\r\n                             ← 空行,标志头部结束

响应报文自己手动拼接:

HTTP/1.1 200 ok\r\n
Content-Type: text/html; charset=utf-8\r\n
Content-Length: 25\r\n
Connection: close\r\n
\r\n
<body内容>

三、核心流程 ​

main → WebServer::start()          bind + listen
     → doAccept()                  异步等连接
     → HttpSession::doRead()       异步读请求头
     → parseRequestMessage()       解析出 method / uri
     → Router → Handler            静态文件 or API
     → doWrite()                   异步写响应,关闭连接
     → doAccept()                  循环,继续等下一个连接

四、关键代码 ​

1. WebServer:单例 + 异步监听 ​

cpp
class WebServer {
private:
    io_context io;          // Asio 的事件循环上下文
    tcp::acceptor acceptor; // 接收器:bind + listen
    WebServer();
public:
    WebServer(const WebServer&) = delete;   // C++11 禁用拷贝
    static WebServer& getInstance() {
        static WebServer webServer;         // 局部静态变量 → 线程安全单例
        return webServer;
    }
};

void WebServer::start(int port, std::string webRoot) {
    acceptor.open(tcp::v4());
    acceptor.bind(tcp::endpoint(tcp::v4(), port)); // 绑定
    acceptor.listen();                             // 监听
    doAccept();
    io.run();               // 开启事件循环,阻塞在这里处理所有异步回调
}

异步接受连接(递归注册,实现持续监听):

cpp
void WebServer::doAccept() {
    auto socket = std::make_shared<tcp::socket>(io);
    acceptor.async_accept(*socket, [this, socket](const error_code& ec) {
        if (!ec) {
            auto session = std::make_shared<HttpSession>(socket, webRoot);
            session->doRead();
        }
        doAccept();   // 处理完(无论成败)再等下一个客户端
    });
}

知识点:async_accept 立即返回,回调在连接到来时触发;socket 用 shared_ptr 持有,lambda 捕获它保证对象存活到回调执行。

2. HttpSession:会话生命周期管理(最容易崩溃的地方) ​

cpp
class HttpSession : public std::enable_shared_from_this<HttpSession> { ... };

void HttpSession::doRead() {
    auto buff = std::make_shared<boost::asio::streambuf>();
    auto self = shared_from_this();   // 关键:回调持有自己,计数+1

    // 读到 "\r\n\r\n"(请求头结束标志)才触发回调
    async_read_until(*socket, *buff, "\r\n\r\n",
        [buff, self, this](const error_code& ec, size_t len) {
            if (ec) return;              // EOF = 客户端断开
            handleRequest(buff.get());
        });
}

知识点:

  • enable_shared_from_this 让对象在异步回调中安全地持有自己,防止异步操作未完成时对象被销毁。
  • streambuf 是 Asio 专为异步设计的缓冲区,自动扩容,无需手动 prepare/commit。
  • async_read_until 按分隔符读取,比固定长度 async_read_some 更适合 HTTP 报文。

3. 解析请求行 ​

cpp
void HttpSession::parseRequestMessage(streambuf* buff, HttpRequest& request) {
    std::istream is(buff);        // streambuf → 流,像 cin 一样用
    std::string line;
    std::getline(is, line);       // 取第一行 "GET /index.html HTTP/1.1"
    std::istringstream iss(line);
    iss >> request.method >> request.uri;   // 空格分割
}

4. Router + 策略模式 ​

cpp
// 抽象处理器
class RequestHandler {
public:
    virtual std::string handle(const std::string&) = 0;
};

// 路由:根据 uri 前缀选择处理器
RequestHandler* Router::route(const std::string& uri, const std::string& webRoot) {
    if (uri.substr(0, 5) == "/api/")
        return new ApiHandler();
    return new FileHandler(webRoot);
}

RequestHandlerContext 用 RAII 包住裸指针,析构时自动 delete,防止内存泄漏:

cpp
RequestHandlerContext ctx(Router().route(uri, webRoot)); // 构造接管
std::string res = ctx.handle(uri);                       // 析构自动释放

5. FileHandler:静态文件处理 ​

cpp
std::string FileHandler::handle(const std::string& uri) {
    std::string path = (uri == "/") ? webRoot + "/index.html" : webRoot + uri;
    if (FileUtils::exists(path)) {
        std::string body = FileUtils::read(path);
        return HttpResponse::ok(body, FileUtils::mime(path));  // 按后缀定 Content-Type
    }
    return HttpResponse::notFound();   // 404
}

FileUtils 两个要点:

cpp
// C++17 filesystem 判断文件存在
static bool exists(const std::string& path) { return std::filesystem::exists(path); }

// 二进制读整个文件(迭代器构造,一步到位,图片也能读)
static std::string read(const std::string& path) {
    std::ifstream ifs(path, std::ios::binary);
    return {std::istreambuf_iterator<char>(ifs), {}};
}

6. 响应报文封装 ​

cpp
static std::string ok(const std::string& body, const std::string& contentType) {
    std::ostringstream oss;
    oss << "HTTP/1.1 200 ok\r\n";
    oss << "Content-Type: " << contentType << "; charset=utf-8\r\n";
    oss << "Content-Length: " << body.size() << "\r\n";   // 必须准确,否则浏览器解析异常
    oss << "Connection: close\r\n\r\n";                   // 两个\r\n后紧跟 body
    oss << body;
    return oss.str();
}

API 返回 JSON 用 C++11 原始字符串字面量,免转义引号:

cpp
str = R"({"success":true,"code":200,"data":[{"id":1,"name":"张三"}]})";

7. 写响应并关闭连接 ​

cpp
void HttpSession::doWrite(std::string& res) {
    auto self = shared_from_this();   // 同样保命
    async_write_some(buffer(res.c_str(), res.size()),
        [this, self](const error_code& ec, size_t) {
            error_code ignore;
            socket->shutdown(tcp::socket::shutdown_both, ignore);
            socket->close(ignore);    // Connection: close,响应完就断开
        });
}

五、CMake 要点 ​

cmake
cmake_minimum_required(VERSION 3.10)
project(webServer)
set(CMAKE_CXX_STANDARD 17)                    # filesystem 需要 C++17
find_package(Boost REQUIRED)                  # 查找 Boost 库
include_directories(${PROJECT_SOURCE_DIR}/inc)
file(GLOB_RECURSE SRCFILES ${PROJECT_SOURCE_DIR}/src/*.cpp)  # 递归收集源码
add_executable(${PROJECT_NAME} ${SRCFILES})
target_link_libraries(${PROJECT_NAME} PRIVATE Boost::headers)
bash
cd build && cmake .. && make    # 可执行文件输出到 bin/

六、测试 ​

bash
./bin/webServer                 # 默认监听 8888,webRoot = ~/html

curl -i http://localhost:8888/                 # 首页 → 200 index.html
curl -i http://localhost:8888/img/logo.png     # 图片 → 200 image/png
curl -i http://localhost:8888/api/users        # API  → 200 application/json
curl -i http://localhost:8888/notexist.html    #      → 404
curl -i -X POST http://localhost:8888/         #      → 405

七、知识点小结 ​

知识点用在哪里
单例模式(局部静态变量)WebServer 全局唯一实例
Boost.Asio 异步 IO(async_accept / async_read_until / async_write_some)全程,单线程处理多客户端
enable_shared_from_this + shared_ptr 生命周期管理HttpSession 异步回调防崩溃
streambuf / istream / istringstream接收缓冲与请求行解析
策略模式 + 多态RequestHandler/FileHandler/ApiHandler
RAII 管理裸指针RequestHandlerContext
C++17 std::filesystem / 二进制文件读取FileUtils
C++11 原始字符串 R"()"拼接 JSON
HTTP 报文格式、Content-Type / Content-Length / 状态码响应封装