文章目录
4. Exercise 3: HTTP Echo Server
基本信息
| 项目 | 内容 |
|---|---|
| 难度 | ⭐⭐ |
| 预估时间 | 2-3 小时 |
| 核心技能 | cpp-httplib, HTTP路由, JSON处理, CORS |
| 产出物 | 一个功能完整的 HTTP 服务器 |
学习目标
- 掌握 cpp-httplib 库的基本使用
- 理解 HTTP 路由和处理函数
- 理解 Content-Type 和 CORS 头
- 为理解 SSE 和 HTTP Stream 传输做准备
前置准备
阅读以下文档:
- 网络编程基础 -- cpp-httplib 部分
- 项目中的
include/httplib.h-- 了解 API 接口
需求规格
实现一个 HTTP 服务器,监听 localhost:8080:
- GET /status -- 返回服务器状态
- POST /echo -- 返回请求体的 JSON 解析结果(
echo字段) - POST /calculate -- 接受
{"a": N, "b": M, "op": "+"},返回计算结果 - GET /headers -- 返回请求的所有 Header(用于调试)
- 支持 CORS -- 允许跨域请求
- 请求日志 -- 每个请求记录到控制台
步骤指引
Step 1: 基本框架 (15分钟)
cpp
#include "httplib.h"
#include "json.hpp"
#include <iostream>
using json = nlohmann::json;
int main() {
httplib::Server svr;
// CORS 中间件
svr.set_pre_routing_handler([](const httplib::Request& req, httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type");
if (req.method == "OPTIONS") {
res.status = 204;
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
});
// TODO: 注册路由
std::cout << "Server listening on http://localhost:8080" << std::endl;
svr.listen("0.0.0.0", 8080);
return 0;
}
Step 2: 实现路由 (40分钟)
依次实现每个路由的处理函数。
Step 3: 测试 (20分钟)
bash
# 启动服务器
./http_echo_server
# 另一个终端测试
curl http://localhost:8080/status
curl -X POST http://localhost:8080/echo -H "Content-Type: application/json" -d '{"message":"hello"}'
curl -X POST http://localhost:8080/calculate -H "Content-Type: application/json" -d '{"a":10,"b":3,"op":"*"}'
curl http://localhost:8080/headers
期望输出
plain
# GET /status
{"server":"http-echo-server","status":"running","uptime_seconds":120}
# POST /echo {"message":"hello"}
{"echo":"hello","received_at":"2024-01-01T12:00:00Z"}
# POST /calculate {"a":10,"b":3,"op":"*"}
{"result":30,"expression":"10 * 3"}
# GET /headers
{"headers":{"Host":"localhost:8080","User-Agent":"curl/8.0",...}}
提示
httplib::Server::set_pre_routing_handler在路由匹配前执行,适合做 CORS 和日志- JSON 解析可能失败,使用 try-catch 包裹
- 计算器的
op参数要进行校验,不支持的操作返回 400 - 如果请求的 Content-Type 不是 application/json,返回 415 Unsupported Media Type
扩展挑战
- 添加 GET /history 返回最近的10个请求记录
- 添加 POST /delay/N 端点,模拟耗时操作(sleep N秒后返回)
- 将路由从 lambda 改为命名的函数,理解可读性的提升
- 同时监听 8080 和 8081 两个端口(创建两个 Server 实例)
MyAnswer
cpp
#include <iostream>
#include <chrono>
#include <ctime>
#include <mutex>
#include <sstream>
#include <thread>
#include <deque>
#include <cstring>
// MinGW 头文件中缺少 GetAddrInfoExCancel 声明,但 vcpkg 默认启用了
// CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO。取消该宏让 httplib 使用兼容的阻塞式 getaddrinfo。
#ifdef __MINGW32__
#undef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO
#endif
#include <httplib.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// ======================== 全局状态 ========================
// 服务器启动时间(用于计算 uptime)
const auto g_serverStartTime = std::chrono::steady_clock::now();
// 请求历史记录(最近 10 条)
struct HistoryEntry {
std::string method;
std::string path;
std::string timestamp;
};
std::deque<HistoryEntry> g_requestHistory;
std::mutex g_historyMutex;
constexpr size_t kMaxHistory = 10;
// ======================== 工具函数 ========================
// 获取当前 UTC 时间的 ISO 8601 格式字符串
std::string GetCurrentTimeISO() {
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf;
#ifdef _WIN32
gmtime_s(&tm_buf, &tt);
#else
gmtime_r(&tt, &tm_buf);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm_buf);
return std::string(buf);
}
// 获取当前本地时间字符串(用于日志)
std::string GetLocalTimeStr() {
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf;
#ifdef _WIN32
localtime_s(&tm_buf, &tt);
#else
localtime_r(&tt, &tm_buf);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
return std::string(buf);
}
// 获取服务器运行秒数
int64_t GetUptimeSeconds() {
auto now = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::seconds>(now - g_serverStartTime).count();
}
// 设置 JSON 响应体
void SetJsonResponse(httplib::Response& res, const json& body, int status = 200) {
res.status = status;
res.set_content(body.dump(), "application/json");
}
// 记录请求日志 + 写入历史
void LogRequest(const httplib::Request& req) {
auto ts = GetLocalTimeStr();
std::cout << "[" << ts << "] " << req.method << " " << req.path << std::endl;
// OPTIONS 预检请求不写入历史
if (req.method == "OPTIONS") return;
std::lock_guard<std::mutex> lock(g_historyMutex);
if (g_requestHistory.size() >= kMaxHistory) {
g_requestHistory.pop_front();
}
g_requestHistory.push_back({req.method, req.path, ts});
}
// ======================== 路由处理函数 ========================
// GET /status --- 返回服务器状态
void HandleGetStatus(const httplib::Request& req, httplib::Response& res) {
json resp;
resp["server"] = "http-echo-server";
resp["status"] = "running";
resp["uptime_seconds"] = GetUptimeSeconds();
SetJsonResponse(res, resp);
}
// POST /echo --- 回显请求体中的 message 字段
void HandlePostEcho(const httplib::Request& req, httplib::Response& res) {
// 校验 Content-Type
if (req.get_header_value("Content-Type").find("application/json") == std::string::npos) {
SetJsonResponse(res, {{"error", "Unsupported Media Type. Use application/json"}}, 415);
return;
}
try {
json body = json::parse(req.body);
std::string message = body.value("message", "");
json resp;
resp["echo"] = message;
resp["received_at"] = GetCurrentTimeISO();
SetJsonResponse(res, resp);
} catch (const json::parse_error& e) {
SetJsonResponse(res, {{"error", std::string("Invalid JSON: ") + e.what()}}, 400);
}
}
// POST /calculate --- 简单计算器
void HandlePostCalculate(const httplib::Request& req, httplib::Response& res) {
// 校验 Content-Type
if (req.get_header_value("Content-Type").find("application/json") == std::string::npos) {
SetJsonResponse(res, {{"error", "Unsupported Media Type. Use application/json"}}, 415);
return;
}
try {
json body = json::parse(req.body);
double a = body.at("a").get<double>();
double b = body.at("b").get<double>();
std::string op = body.at("op").get<std::string>();
double result;
std::ostringstream expr;
if (op == "+") {
result = a + b;
expr << a << " + " << b;
} else if (op == "-") {
result = a - b;
expr << a << " - " << b;
} else if (op == "*") {
result = a * b;
expr << a << " * " << b;
} else if (op == "/") {
if (b == 0) {
SetJsonResponse(res, {{"error", "Division by zero"}}, 400);
return;
}
result = a / b;
expr << a << " / " << b;
} else {
SetJsonResponse(res, {{"error", "Unsupported operation '" + op + "'. Use +, -, *, /"}}, 400);
return;
}
json resp;
resp["result"] = result;
resp["expression"] = expr.str();
SetJsonResponse(res, resp);
} catch (const json::parse_error& e) {
SetJsonResponse(res, {{"error", std::string("Invalid JSON: ") + e.what()}}, 400);
} catch (const json::out_of_range& e) {
SetJsonResponse(res, {{"error", "Missing required fields: a, b, op"}}, 400);
}
}
// GET /headers --- 返回所有请求头
void HandleGetHeaders(const httplib::Request& req, httplib::Response& res) {
json headers = json::object();
for (const auto& h : req.headers) {
headers[h.first] = h.second;
}
json resp;
resp["headers"] = headers;
SetJsonResponse(res, resp);
}
// GET /history --- 返回最近 10 条请求记录(扩展)
void HandleGetHistory(const httplib::Request& req, httplib::Response& res) {
std::lock_guard<std::mutex> lock(g_historyMutex);
json items = json::array();
for (const auto& entry : g_requestHistory) {
items.push_back({
{"method", entry.method},
{"path", entry.path},
{"timestamp", entry.timestamp}
});
}
json resp;
resp["history"] = items;
resp["count"] = items.size();
SetJsonResponse(res, resp);
}
// POST /delay/<seconds> --- 模拟耗时操作(扩展)
void HandlePostDelay(const httplib::Request& req, httplib::Response& res, int seconds) {
if (seconds < 0 || seconds > 30) {
SetJsonResponse(res, {{"error", "Delay must be between 0 and 30 seconds"}}, 400);
return;
}
std::cout << " → Simulating delay of " << seconds << " second(s)..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(seconds));
json resp;
resp["delayed_ms"] = seconds * 1000;
resp["message"] = "Delayed response after " + std::to_string(seconds) + " second(s)";
SetJsonResponse(res, resp);
}
// ======================== 路由注册 ========================
void SetupRouters(httplib::Server& svr) {
svr.Get("/status", [](const httplib::Request& req, httplib::Response& res) {
HandleGetStatus(req, res);
});
svr.Post("/echo", [](const httplib::Request& req, httplib::Response& res) {
HandlePostEcho(req, res);
});
svr.Post("/calculate", [](const httplib::Request& req, httplib::Response& res) {
HandlePostCalculate(req, res);
});
svr.Get("/headers", [](const httplib::Request& req, httplib::Response& res) {
HandleGetHeaders(req, res);
});
// 扩展:请求历史
svr.Get("/history", [](const httplib::Request& req, httplib::Response& res) {
HandleGetHistory(req, res);
});
// 扩展:模拟延时 --- 使用正则捕获秒数
svr.Post(R"(/delay/(\d+))", [](const httplib::Request& req, httplib::Response& res) {
int seconds = std::stoi(req.matches[1]);
HandlePostDelay(req, res, seconds);
});
}
// ======================== 主函数 ========================
int main() {
httplib::Server svr;
// ── CORS 中间件 + 请求日志 ──
svr.set_pre_routing_handler([](const httplib::Request& req, httplib::Response& res) {
// CORS 头
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type");
// 请求日志
LogRequest(req);
if (req.method == "OPTIONS") {
res.status = 204;
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
});
// ── 注册路由 ──
SetupRouters(svr);
// ── 同时监听 8080 和 8081 两个端口(扩展挑战)──
std::thread port8081([&]() {
httplib::Server svr2;
svr2.set_pre_routing_handler([](const httplib::Request& req, httplib::Response& res) {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type");
std::cout << "[8081] " << req.method << " " << req.path << std::endl;
if (req.method == "OPTIONS") {
res.status = 204;
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
});
SetupRouters(svr2);
std::cout << "Server listening on http://localhost:8081" << std::endl;
svr2.listen("0.0.0.0", 8081);
});
std::cout << "Server listening on http://localhost:8080" << std::endl;
svr.listen("0.0.0.0", 8080);
port8081.join();
return 0;
}