实战Demo——基于httplib和json-rpc2.0的HttpServer

文章目录

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

  1. GET /status -- 返回服务器状态
  2. POST /echo -- 返回请求体的 JSON 解析结果(echo 字段)
  3. POST /calculate -- 接受 {"a": N, "b": M, "op": "+"},返回计算结果
  4. GET /headers -- 返回请求的所有 Header(用于调试)
  5. 支持 CORS -- 允许跨域请求
  6. 请求日志 -- 每个请求记录到控制台

步骤指引

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",...}}

提示

  1. httplib::Server::set_pre_routing_handler 在路由匹配前执行,适合做 CORS 和日志
  2. JSON 解析可能失败,使用 try-catch 包裹
  3. 计算器的 op 参数要进行校验,不支持的操作返回 400
  4. 如果请求的 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;
}