Appearance
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,防止内存泄漏:cppRequestHandlerContext 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 / 状态码 | 响应封装 |