一、cpp-httplib 的介绍

C++ HTTP 库(cpp-httplib)是一个轻量级的 C++ HTTP 客户端/服务器库,它提供了简单的 API 来创建 HTTP 服务器和客户端,支持同步和异步操作。以下是一些关于 cpp-httplib 的主要特点:

  1. 轻量级: cpp-httplib 的设计目标是简单和轻量,只有一个头文件包含即可,不依赖于任何外部库。
  2. 跨平台: 它支持多种操作系统,包括 Windows、Linux 和 macOS。
  3. 同步和异步操作: 库提供了同步和异步两种操作方式,允许开发者根据需要选择。
  4. 支持 HTTP/1.1: 它实现了 HTTP/1.1 协议,包括持久连接和管道化。
  5. Multipart form-data: 支持发送和接收 multipart/form-data 类型的请求,这对于文件上传非常有用。
  6. SSL/TLS 支持: 通过使用 OpenSSL 或 mbedTLS 库,cpp-httplib 支持 HTTPS 和 WSS。
  7. 简单易用: API 设计简洁,易于学习和使用。
  8. 性能: 尽管是轻量级库,但性能表现良好,适合多种应用场景。
  9. 社区活跃: cpp-httplib 有一个活跃的社区,不断有新的功能和改进被加入。

二、cpp-httplib库 的安装

cpp-httplib 是一个轻量级的 C++ HTTP 库,采用 header-only 设计,只需包含一个头文件即可使用。以下是cpp-httplib 的安装:

# httplib下载
dev@dev-host:~/workspace$ git clone https://github.com/yhirose/cpp-httplib.git
# 注意:cpp-httplib是 header-only库,只需要⼀个头⽂件,在项目中只需时只需要包含该头⽂件即可
# 我的 cpp-httplib库下载在根目录下
bit@bit08:~$ ls
AIProj clash cpp-httplib test will
bit@bit08:~$ cd cpp-httplib/
bit@bit08:~/cpp-httplib/cpp-httplib$ ls
benchmark cmake CMakeLists.txt docker docker-compose.yml Dockerfile
example httplib.h LICENSE meson.build meson_options.txt README.md
split.py test

# 将httplib.h拷⻉到系统⽬录下,在程序中#include <httplib.h>时能直接找到
bit@bit08:~/cpp-httplib/cpp-httplib$ sudo cp httplib.h /usr/include/

三、cpp-httplib库 的重要组成结构

1. httplib::Request

namespace httplib {
	struct Request {
	 	 std::string method;
		 std::string path;
	 	 Headers headers;
	 	 std::string body;
	 	 Params params;
	 	 
		 // 响应处理回调函数
		 // using ResponseHandler = std::function<bool(const Response&)> ;
		 ResponseHandler response_handler;
		 // 内容接收回调函数
		 // using ContentReceiverWithProgress= function<bool(const char* data, size_t len, 
		 //                                                  uint64_t offset, uint64_t total)>
		 ContentReceiverWithProgress content_receiver;
	}
}
  • method
    表示 HTTP 请求方法(如 GET、POST、PUT 等),类型为 std::string。

  • path
    存储请求的目标路径(例如 /api/resource),类型为 std::string。

  • headers
    类型为 Headers(通常为 std::unordered_map<std::string, std::string>),存储 HTTP 请求头键值对。

  • body
    存储请求体数据(如 POST 请求的 JSON字符串 或表单数据),类型为 std::string。

  • params
    类型为 Params(可能为 std::unordered_map<std::string, std::string>),用于存储 URL 查询参数(如 ?key=value 解析后的键值对)。


在这里插入图片描述

普通HTTP响应体中,一个完整响应 包含一个响应头 和 一个响应体(响应数据),
在HTTP流式返回响应体中,一个完整响应 包含一个响应头 和 多个响应块(chunk)。在流式返回时,会先返回响应头,然后在逐个返回各个响应体。因此在发送流式响应时,需要在请求参数中告知HTTP服务器,响应头 和 chunk该如何处理。

流式响应专用:

response_handler 为响应处理回调函数(对响应头进行处理,只调用一次),实际类型为 std::function<bool(const Response&)> ,如果发起请求时设置该函数,当客户端收到完整的HTTP响应头后,会调用该函数,并传入构造好的Response对象。

content_recevier 为内容接收回调函数(对 chunk 进行处理,每个 chunk 调用一次),是处理流式处理响应的关键,类型为:function<bool(const char* data, size_t len, uint64_t offset, uint64_t total)>

  • data: 指向当前接收到的数据块的指针
  • len: 当前数据块的长度
  • offset: 当前数据块在请求体中的偏移量
  • total: 请求体的总长度
  • 返回值: true表示继续接收数据,false表示停止接收数据

设置该回调函数后,客户端不会等待整个响应体传输完再存到 response.body中,而是每收到一小块数据(chunk)就立刻调用该回调函数,处理实时数据


(1)使用场景

该结构通常用于 HTTP 服务器端接收请求客户端构造请求,通过填充成员变量实现请求数据的传递和处理。

(2)注意事项

  • 确保 method 和 path 符合 HTTP 协议规范。
  • headers 和 params 需注意键的大小写敏感性(根据 RFC 标准建议用小写)

2. httplib::Response

2.1 基本结构

namespace httplib {
	struct Response {
		 std::string version;
		 int status = -1;
		 std::string reason;
		 Headers headers;
		 std::string body;
		 
		 // 设置响应体内容和类型
		 void set_content(const std::string &s, 
		                  const std::string &content_type);
		 // 添加或修改响应头
		 void set_header(const std::string &key, 
		                 const std::string &val);
		                 
         // set_chunked_content_provider 是 httplib::Response 提供的 流式响应发送接口 ,
         // 用于 分块传输(Chunked Transfer Encoding) 场景。
         void set_chunked_content_provider(const std::string& content_type,  
                                           std::function<bool(size_t offset, DataSink& sink)> provide);
	}
}
  • version
    HTTP协议版本(如HTTP/1.1)

  • status
    HTTP状态码(如200表示成功)

  • reason
    状态码的文本描述(如OK)

  • headers
    HTTP响应头集合(类型为Headers,通常是键值对容器)

  • body
    响应体内容(如HTML 或 JSON数据)

2.2 httplib::Response::set_content 详解(普通响应)

(1) 接口签名

void set_content(const std::string& content, const std::string& content_type);
参数 类型 说明
content std::string 响应体内容(一次性全部提供)
content_type std::string MIME 类型,如 "text/plain""application/json"

  • 示例1:简单文本响应

场景:一个简单的问候接口,返回纯文本。

#include <httplib.h>

int main() {
    httplib::Server server;

    server.Get("/hello", [](const httplib::Request& req, httplib::Response& res) {
        res.set_content("你好,世界!", "text/plain");
    });

    server.listen("0.0.0.0", 8080);
    return 0;
}

客户端请求

curl http://localhost:8080/hello

HTTP 响应

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 18

你好,世界!

关键点set_content 会自动计算并设置 Content-Length 头,接收方知道何时读完响应体。


  • 示例2:JSON 响应

场景:一个用户信息查询接口,返回 JSON 格式数据。

#include <httplib.h>
#include <jsoncpp/json/json.h>

// 构建 JSON 字符串的工具函数
std::string buildUserJson(const std::string& name, int age, const std::string& email) {
    Json::Value root;
    root["name"] = name;
    root["age"] = age;
    root["email"] = email;

    Json::StreamWriterBuilder builder;
    return Json::writeString(builder, root);
}

int main() {
    httplib::Server server;

    server.Get("/user/zhangsan", [](const httplib::Request& req, httplib::Response& res) {
        std::string json = buildUserJson("张三", 25, "zhangsan@example.com");
        res.set_content(json, "application/json");
    });

    server.Get("/user/lisi", [](const httplib::Request& req, httplib::Response& res) {
        std::string json = buildUserJson("李四", 30, "lisi@example.com");
        res.set_content(json, "application/json");
    });

    server.listen("0.0.0.0", 8080);
    return 0;
}

HTTP 响应

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 52

{"name":"张三","age":25,"email":"zhangsan@example.com"}

  • 示例3:带状态码和自定义头的响应

场景:模拟一个登录接口,成功返回 200,失败返回 401。

#include <httplib.h>
#include <jsoncpp/json/json.h>

std::string makeJson(const std::string& key, const std::string& value) {
    Json::Value root;
    root[key] = value;
    return Json::writeString(Json::StreamWriterBuilder(), root);
}

int main() {
    httplib::Server server;

    server.Post("/login", [](const httplib::Request& req, httplib::Response& res) {
        // 解析请求体
        std::string body = req.body;
        // 假设 body = {"username": "admin", "password": "123456"}

        // 模拟验证
        bool loginSuccess = (body.find("\"admin\"") != std::string::npos);

        if (loginSuccess) {
            // 设置自定义头
            res.set_header("X-Request-Id", "abc-123-def");
            res.set_header("X-Server-Version", "1.0.0");

            // 设置状态码和响应体
            res.status = 200;
            res.set_content(makeJson("token", "eyJhbGciOiJIUzI1NiJ9..."), "application/json");
        } else {
            res.status = 401;
            res.set_content(makeJson("error", "用户名或密码错误"), "application/json");
        }
    });

    server.listen("0.0.0.0", 8080);
    return 0;
}

成功响应

HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: abc-123-def
X-Server-Version: 1.0.0
Content-Length: 42

{"token":"eyJhbGciOiJIUzI1NiJ9..."}

失败响应

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 36

{"error":"用户名或密码错误"}

(2) set_content 底层机制

┌─────────────────────────────────────────────────────────────────┐
│                set_content 内部执行流程                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  res.set_content("Hello", "text/plain")                        │
│       │                                                         │
│       ├── ① 设置 Content-Type → "text/plain"                    │
│       │                                                         │
│       ├── ② 设置响应体字符串 → body = "Hello"                    │
│       │                                                         │
│       ├── ③ 自动计算 Content-Length → 5                         │
│       │                                                         │
│       └── ④ 标记响应体完整,一次性发送                            │
│                                                                 │
│  处理函数返回后:                                                │
│       │                                                         │
│       └── httplib 框架发送完整 HTTP 响应                         │
│            HTTP/1.1 200 OK                                      │
│            Content-Type: text/plain                             │
│            Content-Length: 5                                    │
│                                                                 │
│            Hello                                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

(3) set_content 与 set_chunked_content_provider 的对比

对比维度 set_content() set_chunked_content_provider()
响应方式 一次性发送完整响应体 分块逐步发送
Content-Length 自动计算并设置 不设置(使用 Transfer-Encoding: chunked)
内存占用 需完整构建响应体 只需当前块在内存中
响应延迟 需等待全部内容生成 可立即发送首块
调用复杂度 简单,一行代码 复杂,需注册回调函数
适用场景 小数据、静态内容、普通 API 流式数据、大数据、实时推送

代码对比

// 方式1:set_content(一次性)
server.Get("/api/data", [](auto& req, auto& res) {
    std::string data = getAllData();  // 一次获取全部数据
    res.set_content(data, "application/json");  // 一次发送
});

// 方式2:set_chunked_content_provider(逐步)
server.Get("/api/stream", [](auto& req, auto& res) {
    res.set_chunked_content_provider("application/json",
        [](size_t offset, auto& sink) -> bool {
            // 逐步生成并发送数据
            for (int i = 0; i < 100; i++) {
                std::string chunk = getNextChunk();
                sink.write(chunk.c_str(), chunk.size());
            }
            sink.done();
            return false;
        });
});

(4) 常用 MIME 类型速查

content_type 用途
"text/plain" 纯文本
"text/html" HTML 页面
"application/json" JSON 数据
"application/xml" XML 数据
"text/css" CSS 样式
"application/javascript" JavaScript 脚本
"image/png" PNG 图片
"image/jpeg" JPEG 图片
"application/octet-stream" 二进制文件下载
"text/event-stream" SSE 流(通常用 chunked 方式)

(5) 总结

要点 说明
本质 一次性设置完整的响应体,框架自动处理 Content-Length
调用时机 在处理函数内,调用 set_content 后函数返回
状态码 默认 200,可通过 res.status 覆盖
自定义头 通过 res.set_header()set_content 前后均可设置
适用场景 所有普通 API 响应、静态页面、JSON 数据返回
限制 不能用于流式场景,响应体必须一次全部生成

2.3 httplib::Response::set_chunked_content_provider 详解(流式响应函数)

(1) 接口签名

void set_chunked_content_provider(
    const std::string& content_type,
    std::function<bool(size_t offset, DataSink& sink)> provider
);

  • 示例1:实时进度条推送

场景:模拟一个文件处理任务,服务器每处理 20% 就向客户端推送一次进度,最终推送完成通知。

#include <httplib.h>
#include <thread>
#include <chrono>

int main() {
    httplib::Server server;

    server.Get("/progress", [](const httplib::Request& req, httplib::Response& res) {
        // 设置响应头
        res.set_header("Cache-Control", "no-cache");
        res.set_header("Connection", "keep-alive");
        res.set_header("X-Accel-Buffering", "no");  // 禁用 nginx 缓冲

        // 注册分块内容提供者
        res.set_chunked_content_provider("text/plain",
            [](size_t offset, httplib::DataSink& sink) -> bool {
                
                // 模拟 5 个步骤,每个步骤耗时 1 秒
                for (int i = 1; i <= 5; i++) {
                    // 模拟耗时操作
                    std::this_thread::sleep_for(std::chrono::seconds(1));

                    // 构建进度消息
                    int percent = i * 20;
                    std::string msg = "进度: " + std::to_string(percent) + "%\n";

                    // 发送给客户端
                    sink.write(msg.c_str(), msg.size());
                }

                // 发送完成消息
                std::string done = "处理完成!\n";
                sink.write(done.c_str(), done.size());
                sink.done();  // 标记流结束

                return false;  // 不再提供更多数据
            });
    });

    server.listen("0.0.0.0", 8080);
    return 0;
}

客户端收到的内容(实时逐行显示):

进度: 20%
进度: 40%
进度: 60%
进度: 80%
进度: 100%
处理完成!

时间线

第1秒: 客户端收到 "进度: 20%"
第2秒: 客户端收到 "进度: 40%"
第3秒: 客户端收到 "进度: 60%"
第4秒: 客户端收到 "进度: 80%"
第5秒: 客户端收到 "进度: 100%"
第5秒: 客户端收到 "处理完成!" + 连接关闭

  • 示例2:大文件分块下载

场景:一个 100MB 的大文件,按每块 1MB 分块发送,避免服务端内存爆炸。

#include <httplib.h>
#include <fstream>
#include <vector>

const size_t CHUNK_SIZE = 1024 * 1024;  // 每块 1MB

int main() {
    httplib::Server server;

    server.Get("/download/large_file", [](const httplib::Request& req, httplib::Response& res) {
        // 打开文件
        std::ifstream file("/path/to/large_file.bin", std::ios::binary);
        if (!file.is_open()) {
            res.status = 404;
            res.set_content("文件不存在", "text/plain");
            return;
        }

        // 获取文件大小
        file.seekg(0, std::ios::end);
        size_t fileSize = file.tellg();
        file.seekg(0, std::ios::beg);

        res.set_header("Content-Type", "application/octet-stream");

        // 分块发送
        res.set_chunked_content_provider("application/octet-stream",
            [file = std::move(file), fileSize](
                size_t offset, httplib::DataSink& sink) mutable -> bool {
                
                std::vector<char> buffer(CHUNK_SIZE);

                // 读取文件块
                file.read(buffer.data(), CHUNK_SIZE);
                size_t bytesRead = file.gcount();

                if (bytesRead > 0) {
                    // 发送当前块
                    sink.write(buffer.data(), bytesRead);
                }

                // 如果读取完毕,关闭流
                if (bytesRead < CHUNK_SIZE) {
                    sink.done();
                    return false;
                }

                return false;  // 注意:框架可能只调用一次回调
            });
    });

    server.listen("0.0.0.0", 8080);
    return 0;
}

流程对比

传统方式(内存爆炸):
  读取 100MB 文件 → 全部放入内存 → 一次性发送 → 内存占用 100MB+

分块方式(内存友好):
  读取 1MB → 发送 1MB → 释放内存 → 读取下一个 1MB → ...
  内存占用始终 ≤ 1MB

(2) 回调函数 offset 参数的用法

offset 表示当前已发送的总字节数,可用于实现断点续传或进度计算。

server.Get("/stream", [](const httplib::Request& req, httplib::Response& res) {
    res.set_chunked_content_provider("text/plain",
        [](size_t offset, httplib::DataSink& sink) -> bool {
            
            // 第一次调用 offset = 0
            // 发送 10 字节
            std::string chunk1 = "0123456789";  // 10 字节
            sink.write(chunk1.c_str(), chunk1.size());
            // 此时 offset 变为 10
            
            return false;  // 仅调用一次
        });
});

多次调用回调的场景(返回 true 继续调用):

server.Get("/multi", [](const httplib::Request& req, httplib::Response& res) {
    res.set_chunked_content_provider("text/plain",
        [counter = 0](size_t offset, httplib::DataSink& sink) mutable -> bool {
            
            counter++;
            std::string msg = "第" + std::to_string(counter) 
                            + "次调用, offset=" + std::to_string(offset) + "\n";
            sink.write(msg.c_str(), msg.size());

            if (counter >= 3) {
                sink.done();
                return false;  // 停止调用
            }
            return true;  // 继续调用
        });
});

客户端收到

第1次调用, offset=0
第2次调用, offset=25
第3次调用, offset=50

(3) DataSink 完整接口

方法 签名 说明
write bool write(const char* data, size_t n) 写入 n 字节数据
done void done() 标记流结束
is_writable bool is_writable() 检查客户端是否还连接着

is_writable() 的使用场景:检测客户端是否断开连接。

res.set_chunked_content_provider("text/plain",
    [](size_t offset, httplib::DataSink& sink) -> bool {
        
        for (int i = 0; i < 100; i++) {
            // 检查客户端是否还在
            if (!sink.is_writable()) {
                // 客户端已断开,停止发送
                return false;
            }

            // 发送数据
            std::string msg = "数据块 #" + std::to_string(i) + "\n";
            sink.write(msg.c_str(), msg.size());
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }

        sink.done();
        return false;
    });

(4) 核心机制图解

┌─────────────────────────────────────────────────────────────────┐
│                    set_chunked_content_provider 机制             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ① 注册回调                                                      │
│     res.set_chunked_content_provider(type, callback)            │
│     └── 仅注册,不执行                                           │
│                                                                 │
│  ② 处理函数返回                                                  │
│     └── httplib 框架接管                                         │
│                                                                 │
│  ③ 框架发送 HTTP 响应头                                          │
│     HTTP/1.1 200 OK                                             │
│     Content-Type: text/plain                                    │
│     Transfer-Encoding: chunked          ← 关键:分块传输编码     │
│                                                                 │
│  ④ 框架调用回调函数                                              │
│     callback(offset, dataSink)                                  │
│                                                                 │
│  ⑤ 回调内部通过 dataSink 发送数据                                │
│     dataSink.write(data, len)                                   │
│     └── 框架自动封装为 HTTP chunk 格式发送                       │
│                                                                 │
│  ⑥ 回调返回 false                                               │
│     └── 框架调用 dataSink.done() 标志 → 发送结束块,关闭连接     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

HTTP Chunked 编码格式

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

5\r\n              ← 块大小(16进制,5=5字节)
Hello\r\n          ← 块数据
7\r\n              ← 块大小(7字节)
 World!\r\n        ← 块数据
0\r\n              ← 大小为0,表示结束
\r\n

(5) 与普通响应的对比

对比维度 set_content() set_chunked_content_provider()
响应体 一次性构建完整内容 逐步生成、逐步发送
Content-Length 必须设置 不需要(自动使用 chunked 编码)
内存占用 全部响应内容在内存中 只有当前块在内存中
延迟 高(等全部生成完) 低(首块即刻发送)
适用场景 小数据、静态内容 流式数据、大数据、实时推送

(6) 总结

要点 说明
本质 将 HTTP 响应头和数据发送分离,响应头先发送,数据体后续分块推送
核心优势 无需预知 Content-Length,适合流式/实时/大数据场景
回调返回值 true = 还有数据(框架会再次调用),false = 结束
必须调用 dataSink.done() 通知客户端流结束
典型场景 SSE 推送、进度条、日志流、大文件下载、实时数据

3. httplib::Server

3.1 基本结构

namespace httplib {
	class Server {
		 using Handler = std::function<void(const Request &, Response &)>;
		  
		 Server &Get(const std::string &pattern, Handler handler);
		 
		 Server &Post(const std::string &pattern, Handler handler);
		 
		 Server &Put(const std::string &pattern, Handler handler); 
		 
		 Server &Delete(const std::string &pattern, Handler handler);
		 
		 bool listen(const std::string &host, int port);

		 void set_mount_point(const std::string& mount_point, const std::string& path);
	}
}

3.2 httplib::Server::set_mount_point 方法解析(设置 静态文件服务挂载点)

ChatServer/
├── ChatServer.cpp          # 服务器实现
├── ChatServer.h            # 服务器头文件
├── main.cpp                # 程序入口
├── CMakeLists.txt          # CMake 构建配置
└── build/                  # 构建输出目录
    ├── AIChatServer        # 可执行文件
    ├── www/                # 静态资源目录
    │   ├── index.html
    │   ├── style.css
    │   └── app.js
    └── CMakeFiles/...      # CMake 构建中间文件

set_mount_point 是 httplib 库中用于设置 静态文件服务挂载点 的方法,将指定的 URL 路径映射到本地文件系统目录,实现静态资源(HTML、CSS、JS、图片等)的自动服务。

AIChatServer 是由 ChatServer.cpp、ChatServer.h 和 main.cpp构建形成的 服务端可执行文件

  • 假设在 ChatServer.cpp 文件下:
// 创建http服务器实例
std::unique_ptr<httplib::Server> _chatServer = std::make_unique<httplib::Server>();
// 将根路径 /(构建http通信时的根路径)挂载到当前目录(可执行文件所在目录;也就是build目录)下的 www 文件夹。
_chatServer->set_mount_point("/", "./www");
  • 工作机制
    (1) 请求匹配 :当收到 HTTP 请求时,httplib 会先检查请求路径是否匹配已注册的挂载点
    (2) 文件查找 :若匹配成功,会在对应的本地目录中查找请求的文件
    (3) 默认索引 :若请求路径为 / ,httplib 会自动查找 index.html 文件
    (4) 路由优先级 :手动注册的路由(如 /api/session )优先级高于静态文件挂载点

  • 访问示例
    假设服务器运行在 http://localhost:8080 , 目录结构如下:

build/                  # 构建输出目录
    ├── AIChatServer        # 可执行文件
    └── www/                # 静态资源目录
        ├── index.html
        ├── style.css
        └── app.js

则可通过以下 URL 访问:

根路径 /(构建http通信时的根路径)挂载到当前目录(可执行文件所在目录;也就是build目录)下的 www 文件夹

URL 映射文件
http://localhost:8080/ (构建http通信时的根路径;触发默认索引) www/index.html
http://localhost:8080/index.html www/index.html
http://localhost:8080/style.css www/style.css
http://localhost:8080/app.js www/app.js

3.3 手动注册路由规则(示例演示)

  • ChatServer.h
#pragma once
#include <httplib.h>
#include <memory>
#include <ai_chat_sdk/ChatSDK.h>

namespace ai_chat_server{

// 服务器配置信息
struct ServerConfig{
    std::string host = "0.0.0.0";    // 服务器绑定ip
    int port = 8080;                 // 服务器绑定端口
    std::string logLevel = "INFO";   // 日志级别

    // 模型需要的配置信息
    double temperature = 0.7;        // 温度参数
    int maxTokens = 1024;            // 最大token数

    // API Key
    std::string deepseekAPIKey;     // deepseek API Key
    std::string geminiAPIKey;       // gemini API Key
    std::string chatGPTAPIKey;      // chatGPT API Key

    // Ollama
    std::string ollamaModelName;    // Ollama模型名称
    std::string ollamaModelDesc;    // Ollama模型描述
    std::string ollamaEndpoint;     // Ollama的url地址
};


class ChatServer{
public:
    ChatServer(const ServerConfig& config);

    bool start();   // 启动服务器
    void stop();    // 停止服务器
    bool isRunning()const;   // 是否正在运行

private:
    // 构造响应体 (默认是错误响应体,JSON格式)
    std::string buildResponse(const std::string& message, bool success = false);
    // 处理创建会话请求
    void handleCreateSessionRequest(const httplib::Request& request, httplib::Response& response);
    // 处理删除会话请求
    void handleDeleteSessionRequest(const httplib::Request& request, httplib::Response& response);
    // 处理获取会话列表请求
    void handleGetSessionListsRequest(const httplib::Request& request, httplib::Response& response);
    // 处理发送消息请求-全量返回
    void handleSendMessageRequest(const httplib::Request& request, httplib::Response& response);

    // 设置HTTP路由规则
    void setHttpRoutes();

private:
    ServerConfig _config;   // 服务器配置信息
    std::unique_ptr<httplib::Server> _chatServer = nullptr;     // HTTP服务器
    std::shared_ptr<ai_chat_sdk::ChatSDK> _chatSDK = nullptr;   // 聊天SDK
    std::atomic<bool> _isRunning = {false};   // 是否正在运行
};

} // end ai_chat_server
  • ChatServer.cpp
#include "ChatServer.h"
#include <ai_chat_sdk/util/mylog.h>
#include <jsoncpp/json/json.h>
#include <thread>

namespace ai_chat_server{

    ChatServer::ChatServer(const ServerConfig& config)
        : _config(config)
    {
        // 初始化ChatSDK实例 (参数是 数据库名称, 可以不填, 默认是 "ai_chat.db")
        _chatSDK = std::make_shared<ai_chat_sdk::ChatSDK>();

        // 提供所有模型提供者的配置信息
        std::vector<std::shared_ptr<ai_chat_sdk::Config>> config_list;

        config_list.push_back(std::make_shared<ai_chat_sdk::ApiConfig>("deepseek-v4-flash", config.temperature, config.maxTokens, config.deepseekAPIKey, 
                                                                       "https://api.deepseek.com"));
        config_list.push_back(std::make_shared<ai_chat_sdk::ApiConfig>("gemini-2.5-flash", config.temperature, config.maxTokens, config.geminiAPIKey, 
                                                                       "https://generativelanguage.googleapis.com"));
        config_list.push_back(std::make_shared<ai_chat_sdk::ApiConfig>("gpt-5.4-mini", config.temperature, config.maxTokens, config.chatGPTAPIKey, 
                                                                       "https://api.openai.com"));
        config_list.push_back(std::make_shared<ai_chat_sdk::OllamaConfig>(config.ollamaModelName, config.temperature, config.maxTokens, config.ollamaEndpoint, 
                                                                          config.ollamaModelDesc));

        // ai_chat_sdk::ChatSDK对象 注册并初始化所有模型提供者
        _chatSDK->registerAndInit_AllProviders(config_list);

        // 创建http服务器实例
        _chatServer = std::make_unique<httplib::Server>();
    }

    // 启动服务器
    bool ChatServer::start()
    {
        if(_isRunning.load())
        {
            ERR("ChatServer is running!!!");
            return false;
        }

        // 设置路由规则
        setHttpRoutes();

        // 设置静态资源的路径
        // 前端页面相关的所有文件都放在www目录下  注意:将来前端页面名称命名为index.html
        // 当用户在浏览器中输入:http://ip:port/index.html    http://ip:port也能访问index.html页面
        // 在httplib中,默认情况下,如果请求路径中只有ip和端口,httplib默认会使用index.html文件
        _chatServer->set_mount_point("/", "./www");

        // 为了不卡服务器,不卡主线程,服务器在单独的线程中运行
        std::thread serverThread([this](){
            INFO("ChatServer start on {} :{}", _config.host, _config.port);
            // 启动服务器 (开始监听 HTTP 请求)
            _chatServer->listen(_config.host, _config.port);
            // httplib::Server::listen() 是 阻塞的,会一直运行直到调用 httplib::Server::stop() 方法
            INFO("ChatServer end on {} :{}", _config.host, _config.port);
        });

        serverThread.detach();  // 分离线程,将线程资源从主线程中分离出来,避免主线程阻塞
        _isRunning.store(true);
        INFO("ChatServer start success!!!");
        return true;
    }

    // 停止服务器
    void ChatServer::stop()
    {
        if(!_isRunning.load()){
            ERR("ChatServer is not running!!!");
            return;
        }

        // 当你调用 httplib::Server::stop() 方法:
        // 1. 正在运行的 HTTP 服务器会停止接受新连接 - 服务器不再接受新的 HTTP 请求
        // 2. 等待现有连接完成 - 允许当前正在处理的请求完成(优雅关闭)
        // 3. 释放服务器资源 - 清理服务器占用的资源
        if(_chatServer){
            _chatServer->stop();
            // httplib::Server::stop() 方法用于 停止正在运行的 HTTP 服务器 
        }

        _isRunning.store(false);
        INFO("ChatServer stop success!!!");
    }

    // 是否正在运行
    bool ChatServer::isRunning()const
    {
        return _isRunning.load();
    }

    // 构造响应体 (默认是错误响应体,JSON格式)
    std::string ChatServer::buildResponse(const std::string& message, bool success)
    {
        Json::Value root;
        root["success"] = success;
        root["message"] = message;

        // 先定义 Json::StreamWriterBuilder工厂类
        Json::StreamWriterBuilder swber;
        // 通过工厂类 Json::StreamWriterBuilder 构建Json::StreamWriter类对象
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());

        // 通过Json::StreamWriter类中的 writer接口进行序列化
        std::stringstream ss;
        swb->write(root, &ss);

        std::string response = ss.str();

        return response;
    }

    // 处理创建会话请求
    void ChatServer::handleCreateSessionRequest(const httplib::Request& request, httplib::Response& response)
    {
        Json::Value val;
        // 从请求体中获取JSON字符串
        std::string json_str = request.body;

        // 先定义 Json::CharReaderBuilder工厂类
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());

        // 通过Json::CharReader类中的 parse接口进行反序列化
        std::string error;
        bool ret = srd->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &val, &error);
        if(!ret)
        {
            // 解析JSON字符串失败
            std::string errorJsonStr = buildResponse("Failed to parse JSON string", false);    
            response.set_content(errorJsonStr, "application/json");
            response.status = 400;  // 客户端发送的请求有语法错误,服务器无法理解或处理该请求
            return;
        }

        std::string model_name = val["model"].asString();
        if(model_name.empty())
        {
            std::string errorJsonStr = buildResponse("Model name is empty.", false);    
            response.set_content(errorJsonStr, "application/json");
            response.status = 400;  // 客户端发送的请求不符合规范,服务器无法处理该请求
            return;
        }

        // 创建会话 
        std::string session_id = _chatSDK->createSession(model_name);
        if(session_id.empty())
        {
            std::string errorJsonStr = buildResponse("Failed to create session.", false);    
            response.set_content(errorJsonStr, "application/json");
            // 可能是模型名字有误,请用户检查
            response.status = 500;  // 服务器内部错误,无法创建会话
            return;
        }

        // 构造响应体 (JSON格式)
        Json::Value resp;
        resp["success"] = true;
        resp["message"] = "Session created successfully.";
        
        Json::Value data;
        data["session_id"] = session_id;
        data["model"] = model_name;
        resp["data"] = data;

        // 先定义 Json::StreamWriterBuilder工厂类
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());

        // 通过Json::StreamWriter类中的 writer接口进行序列化
        std::stringstream ss;
        swb->write(resp, &ss);

        response.set_content(ss.str(), "application/json");
        response.status = 200;  // 成功
    }

    // 处理删除会话请求
    void ChatServer::handleDeleteSessionRequest(const httplib::Request& request, httplib::Response& response)
    {
        // 获取会话id,注意:会话id是一个路径参数
        std::string sessionId = request.matches[1];

        // 删除会话
        bool success = _chatSDK->deleteSession(sessionId);
        if(!success)
        {
            std::string errorJsonStr = buildResponse("delete session failed, session not found", false);    
            response.set_content(errorJsonStr, "application/json");
            response.status = 404;   // 会话不存在
            return;
        }

        std::string successJsonStr = buildResponse("delete session successfully", true);  

        response.set_content(successJsonStr, "application/json");
        response.status = 200;  // 成功
    }

    // 处理获取会话列表请求
    void ChatServer::handleGetSessionListsRequest(const httplib::Request& request, httplib::Response& response)
    {
        std::vector<std::string> session_list = _chatSDK->getSessionList();
        Json::Value resp;
        resp["success"] = true;
        resp["message"] = "Session list retrieved successfully.";
        
        for(const auto& session_id : session_list)
        {
            std::shared_ptr<ai_chat_sdk::Session> session = _chatSDK->getSession(session_id);
            session->_messages = _chatSDK->getSessionHistory(session_id);
            Json::Value val;
            val["id"] = session_id;
            val["model"] = session->_model_name;
            val["created_at"] = static_cast<int64_t>(session->_created_at);
            val["updated_at"] = static_cast<int64_t>(session->_updated_at);
            val["message_count"] = session->_messages.size();
            val["first_user_message"] = "";
            if(!session->_messages.empty())
            {
                val["first_user_message"] = session->_messages[0]._content; 
            }

            resp["data"].append(val);
        }
        
        // 先定义 Json::StreamWriterBuilder工厂类
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());

        // 通过Json::StreamWriter类中的 writer接口进行序列化
        std::stringstream ss;
        swb->write(resp, &ss);

        response.set_content(ss.str(), "application/json");
        response.status = 200;  // 成功
    }

    // 处理发送消息请求-全量返回
    void ChatServer::handleSendMessageRequest(const httplib::Request& request, httplib::Response& response)
    {
        Json::Value val;
        // 从请求体中获取JSON字符串
        std::string json_str = request.body;

        // 先定义 Json::CharReaderBuilder工厂类
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());

        // 通过Json::CharReader类中的 parse接口进行反序列化
        std::string error;
        bool ret = srd->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &val, &error);
        if(!ret)
        {
            // 解析JSON字符串失败
            std::string errorJsonStr = buildResponse("Failed to parse JSON string", false);    
            response.set_content(errorJsonStr, "application/json");
            response.status = 400;  // 客户端发送的请求有语法错误,服务器无法理解或处理该请求
            return;
        }

        // 从JSON字符串中提取会话id和消息内容
        std::string sessionId = val["session_id"].asString();
        std::string message = val["message"].asString();

        if(sessionId.empty() || message.empty()){
            std::string errorJsonStr = buildResponse("session_id or message is empty");
            response.status = 400;  // 解析请求参数失败
            response.set_content(errorJsonStr, "application/json");
            return;
        }

        // 发送消息
        std::string responseMessage = _chatSDK->sendMessage(sessionId, message);
        if(responseMessage.empty()){
            std::string errorJsonStr = buildResponse("send message failed", false);    
            response.set_content(errorJsonStr, "application/json");
            response.status = 500;  //  发送消息失败, 服务器内部错误
            return;
        }

        // 解析响应消息
        Json::Value resp;
        resp["success"] = true;
        resp["message"] = "Message sent successfully.";
        
        Json::Value data;
        data["session_id"] = sessionId;
        resp["response"] = responseMessage;

        resp["data"] = data;

        // 先定义 Json::StreamWriterBuilder工厂类
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());

        // 通过Json::StreamWriter类中的 writer接口进行序列化
        std::stringstream ss;
        swb->write(resp, &ss);

        response.set_content(ss.str(), "application/json");
        response.status = 200;  // 成功
    }

    // 设置HTTP路由规则
    void ChatServer::setHttpRoutes()
    {
        // 处理创建会话请求
        _chatServer->Post("/api/session", [this](const httplib::Request& request, httplib::Response& response){
            handleCreateSessionRequest(request, response);
        });

        // 处理删除会话请求
        _chatServer->Delete("/api/session/(.*)", [this](const httplib::Request& request, httplib::Response& response){
            handleDeleteSessionRequest(request, response);
        });
        
        // 处理获取会话列表请求
        _chatServer->Get("/api/sessions", [this](const httplib::Request& request, httplib::Response& response){
            handleGetSessionListsRequest(request, response);
        }); 

        // 处理发送消息请求-全量返回
        _chatServer->Post("/api/message", [this](const httplib::Request& request, httplib::Response& response){
            handleSendMessageRequest(request, response);
        });
    }
} // namespace ai_chat_sdk

  • 路由映射表
HTTP 方法 URL 路径 处理函数 功能描述
POST /api/session handleCreateSessionRequest 创建会话
DELETE /api/session/(.*) handleDeleteSessionRequest 删除指定会话
GET /api/sessions handleGetSessionListsRequest 获取会话列表
POST /api/message handleSendMessageRequest 发送消息(全量返回)
  • 路径参数说明

正则捕获组 (.*) 用于匹配动态路径参数:

// 删除会话路由
_chatServer->Delete("/api/session/(.*)", [...]);
// 请求示例: DELETE /api/session/abc123
// request.matches[1] = "abc123"  (会话ID)

  • httplib 中路由的匹配优先级:

(1) 手动注册的路由 (如 /api/session )→ 最高优先级
(2) 静态文件挂载点 (如 / 映射到 www )→ 次优先级

4. httplib::Client

4.1 基本结构

namespace httplib {
	class Client {
	public:
	     // 允许直接传入 C 风格字符串作为主机地址,避免不必要的 std::string 构造
		 explicit Client(const std::string &host, int port);
     	 // 省略端口参数,默认使用 HTTP(80) 或 HTTPS(443) 标准端口,根据后续是否启用 SSL 自动选择
         explicit Client(const std::string& host);
         // 通过完整 URL 初始化(如 http://example.com:8080),内部解析协议、主机和端口。
         // 需处理以下情况:
		 //  (1) 自动识别 http/https 协议
		 //  (2) 提取域名和端口(若未指定则使用协议默认端口)
         explicit Client(const std::string& url);
         
         // 设置连接超时时间
         // (1) sec 参数:以秒为单位的超时值(整数类型)
         // (2) usec 参数:以微秒为单位的附加超时值(整数类型)
         void set_connection_timeout(time_t sec, time_t usec);
         // 设置读取超时时间
         void set_read_timeout(time_t sec, time_t usec);

		 
		 Result Get(const std::string &path, const Headers &headers);
		 
		 // 原始 Post接口(保留)
		 Result Post(const std::string &path, const std::string &body, 
		             const std::string &content_type);
		 // 带 Headers类型 的 Post接口   
		 Result Post(const char* path, const Headers& headers, 
		             const std::string& body, const std::string &content_type);

		             
		 Result Put(const std::string &path, const std::string &body,
		            const std::string &content_type);
		            
		 Result Delete(const std::string &path, const std::string &body,
		               const std::string &content_type);
		               
		 // send() 是 httplib::Client 提供的 底层请求发送接口 ,相比 Get() 、 Post() 等便捷方法,
		 // 它提供了更精细的控制能力, 特别适合流式响应场景 。      
		 Result send(const Request& req);  
		         
	private:
		 std::string host_;  // 存储目标服务器的地址(如域名 或 IP)。
		 int port_;          // 服务器端口号,通常为整数类型(如 80 或 443)
		 size_t timeout_sec_;    // 请求超时时间(秒),通常为无符号整型。
		 bool keep_alive_;       // 布尔值,指示是否保持长连接(Keep-Alive)
	}
}

4.2 通过 httplib::Client::Post接口发送普通请求(httplib::Client::Post 接口详解)

    // 发送消息(非流式响应)
    std::string DeepSeekProvider::sendMessage(const std::vector<Message>& messages, 
                                              const std::map<std::string,std::string>& request_param) 
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = false;
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体 (将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);
        
        /////////////////////////////// http相关内容 ///////////////////////////////
        // 创建HTTP客户端
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(60, 0); // 60秒读取超时

        // 设置请求头
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _api_key},
            {"Content-Type", "application/json"}
        };
        
        // 发送POST请求
        httplib::Result res = client.Post("/chat/completions", headers, 
                                            request_body_str, "application/json");
        // 网络层失败(连接失败、超时等)
        if(!res) {
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }
        // 请求成功但应用层失败(HTTP 状态码非 200)
        if(res->status != 200) {
            ERR("POST请求失败, status code : {}", res->status);
            return "";
        }
        DBG("POST请求成功, response body : {}", res->body);
        ///////////////////////////////////////////////////////////////////////////

        // 解析响应体(字符串格式的JSON字符串)为 Json::Value对象
        Json::CharReaderBuilder srder;
        std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
        Json::Value resp;
        std::string error;
        bool parse_ret = srd->parse(res->body.c_str(),res->body.c_str() + res->body.size(), &resp, &error);
        if(!parse_ret) {
            ERR("Json解析失败, error : {}", error);
            return "";
        }

        // 从解析后的Json::Value对象中提取响应内容
        if(resp.isMember("choices") && resp["choices"].isArray() && !resp["choices"].empty()) 
        {
            Json::Value choice = resp["choices"][0];
            if(choice.isMember("message") && choice["message"].isObject()) {
                Json::Value message = choice["message"];
                if(message.isMember("content") && message["content"].isString()) 
                {
                    DBG("message content : {}", message["content"].asString());
                    return message["content"].asString();
                }
            }
        }
        
        // 解析失败,返回错误信息
        ERR("Invalid response format from DeepSeek API");
        return "";
    }

(1) 接口概述

Post() 是 httplib::Client 提供的便捷 HTTP POST 请求方法,用于向服务器发送 POST 请求并获取响应。相比底层的 send() 接口,它封装了请求构建过程,使用更简洁。


(2) 常用重载形式

httplib::Client::Post 提供了多种重载,适配不同场景:

重载形式 返回值 适用场景
Post(path) Result 无请求体的 POST
Post(path, body, content_type) Result 带原始请求体
Post(path, params) Result URL 编码表单
Post(path, headers, body, content_type) Result 自定义头 + 请求体

(3) 核心签名(项目中使用的形式)

Result Post(const std::string& path, const Headers& headers, 
            const std::string& body, const std::string& content_type);
参数 类型 说明
path std::string 请求路径(如 /chat/completions
headers Headers HTTP 请求头集合
body std::string 请求体内容
content_type std::string Content-Type 标识
返回值 Result 响应结果封装

(4) 在项目中的实际使用

// 设置请求头
httplib::Headers headers = {
    {"Authorization", "Bearer " + _api_key},
    {"Content-Type", "application/json"}
};

// 发送 POST 请求
httplib::Result res = client.Post("/chat/completions", headers, 
                                  request_body_str, "application/json");

调用流程

┌─────────────────────────────────────────────────────────────┐
│ 1. 构建请求头 (Authorization + Content-Type)                │
├─────────────────────────────────────────────────────────────┤
│ 2. 序列化请求体为 JSON 字符串                                │
├─────────────────────────────────────────────────────────────┤
│ 3. 调用 Post() 发送请求                                     │
│    ├── 建立 TCP 连接                                        │
│    ├── 发送 HTTP 请求                                       │
│    ├── 接收完整响应                                         │
│    └── 返回 Result 对象                                     │
├─────────────────────────────────────────────────────────────┤
│ 4. 检查响应结果                                             │
│    ├── !res → 网络层失败(超时、连接失败等)                 │
│    └── res->status != 200 → 应用层失败                      │
└─────────────────────────────────────────────────────────────┘

(5) 返回值 Result 详解

auto res = client.Post(...);

// 1. 布尔判断(网络层状态)
if(!res) {
    // 网络错误:连接失败、超时、DNS解析失败等
    httplib::Error err = res.error();
    ERR("HTTP error: {}", httplib::to_string(err));
    return "";
}

// 2. 访问响应对象(应用层状态)
if(res->status == 200) {
    // 请求成功
    std::string body = res->body;      // 响应体
    std::string header = res->get_header_value("Content-Type");  // 获取响应头
} else {
    // HTTP 错误
    ERR("status: {}, body: {}", res->status, res->body);
}

Result 支持的操作

操作 说明
operator bool() 判断网络层是否成功
error() 获取网络错误码
->status HTTP 状态码
->body 响应体内容
->headers 响应头集合
->get_header_value(name) 获取指定响应头

(6) 请求超时配置

在调用 Post() 前可配置客户端超时:

httplib::Client client(_base_url);
client.set_connection_timeout(30, 0);  // 连接超时 30 秒
client.set_read_timeout(60, 0);        // 读取超时 60 秒
// 注意:流式响应需要更长的 read_timeout(如 300 秒)

auto res = client.Post("/chat/completions", headers, body, "application/json");

(7) 完整请求示例

// 创建客户端
httplib::Client cli("https://api.deepseek.com");

// 设置超时
cli.set_connection_timeout(30, 0);
cli.set_read_timeout(60, 0);

// 设置请求头
httplib::Headers headers = {
    {"Authorization", "Bearer sk-xxx"},
    {"Content-Type", "application/json"}
};

// 构建请求体
std::string body = R"({"model":"deepseek-v4-flash","messages":[{"role":"user","content":"你好"}]})";

// 发送请求
auto res = cli.Post("/chat/completions", headers, body, "application/json");

// 处理响应
if(res && res->status == 200) {
    std::cout << "Response: " << res->body << std::endl;
} else {
    std::cerr << "Error: " << (res ? std::to_string(res->status) : httplib::to_string(res.error())) << std::endl;
}

4.3 通过 httplib::Client::send接口发送流式响应请求(httplib::Client::send 接口详解)

  • 完整示例:
    // 实现流式响应
    std::string DeepSeekProvider::sendMessageStream(const std::vector<Message>& messages, 
                                 const std::map<std::string,std::string>& request_param,
                                 std::function<void(const std::string&, bool)> callback)
    {
        // 检查模型是否初始化成功
        if(!_isAvailable) {
            ERR("model is not available");
            return "";
        }

        // 获取采样温度 和 max_tokens
        double temperature = 0.7;
        int max_tokens = 2048;
        if(request_param.find("temperature") != request_param.end()) {
            temperature = std::stod(request_param.at("temperature"));
        }
        if(request_param.find("max_tokens") != request_param.end()) {
            max_tokens = std::stoi(request_param.at("max_tokens"));
        }

        // 构建历史消息(含最新请求消息)
        Json::Value messages_array(Json::arrayValue);
        for(const auto& msg : messages) {
            Json::Value msg_obj;
            msg_obj["role"] = msg._role;
            msg_obj["content"] = msg._content;
            messages_array.append(msg_obj);
        }

        // 构建请求体
        Json::Value request_body;
        request_body["model"] = getModelName();
        request_body["messages"] = messages_array;
        request_body["stream"] = true;            // 开启流式响应
        request_body["temperature"] = temperature;
        request_body["max_tokens"] = max_tokens;

        // 序列化请求体(将Json::Value对象 转换为 字符串格式的JSON字符串)
        Json::StreamWriterBuilder swber;
        std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
        std::stringstream ss;
        int write_ret = swb->write(request_body, &ss);
        if(write_ret != 0)
        {
            ERR("Json序列化失败!");
            return "";
        }
        std::string request_body_str = ss.str();
        DBG("request_body_str : {}", request_body_str);

        //////////////////////////////////////////// 与非流式响应有区别的内容 ////////////////////////////////////////////
        // 创建HTTP Client
        httplib::Client client(_base_url);
        client.set_connection_timeout(30, 0); // 30秒超时
        client.set_read_timeout(300, 0);      // 流式响应需要更⻓的时间

        // 设置请求头
        httplib::Headers headers = {
            {"Authorization", "Bearer " + _api_key},
            {"Content-Type", "application/json"}
        };

        // 流式处理变量
        std::string buffer; // 接收流式响应的数据块
        bool gotError = false; // 响应是否成功(成功false,失败true)
        std::string errorMsg; // 错误描述符
        int statusCode = 0; // 状态码
        bool streamFinish = false; // 标记流式返回数据是否结束
        std::string fullResponse; // 累积完整的响应

        ///////////////////// 构建HTTP请求(设置处理函数) ////////////////////////
        // 创建请求对象
        httplib::Request req;
        req.method = "POST";
        req.path = "/chat/completions";
        req.headers = headers;
        req.body = request_body_str;
        // HTTP协议响应的格式: 
        // ⼀个状态行:HTTP/1.1 200 OK
        // ⼀组响应头:Content-Type,Content-Length
        // 空行:\r\n\r\n
        // 响应体
        // 普通的HTTP响应是 ⼀头⼀体
        // 流式响应:只有⼀个响应头,但响应体被拆分成多个块(chunks)陆续发送,即⼀头多块
        
        // 响应头处理(检查状态码): 状态码不是200时,记录错误信息
        req.response_handler = [&](const httplib::Response& res) {
            int statusCode = res.status;
            if(200 != statusCode){
                gotError = true;
                errorMsg = "HTTP Error:" + std::to_string(statusCode);
                return false; // 终止请求(不再调用 content_receiver)
            }
            return true; // 继续接收数据(继续调用 content_receiver 接收响应体chunk)
        };

        // 响应体处理(流式回调)
        // data:指向当前接收到的数据块的指针
        // len:当前数据块的长度
        // offset:当前数据块在响应体中的偏移量
        // totalLength:响应体的总长度
        // 流式响应 主要关注前两个参数 data 和 len 就足够了。因为在流式响应的场景下,总长度通常未知,需要持续接收直到 [DONE] 标记出现
        req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t totalLength)
        {
            // 如果http请求头出错,就不需要再继续接收了
            if(gotError)
            {
                return false;
            }
            // 追加新数据到缓冲区
            buffer.append(data, len);
            DBG("buffer: {}", buffer);
            // 处理所有完整的事件,事件和事件之间以\n\n分隔
            size_t pos = 0;
            while((pos = buffer.find("\n\n")) != std::string::npos)
            {
                std::string event = buffer.substr(0, pos);
                buffer.erase(0, pos+2); // 移除已经处理的事件
                // 处理空⾏和注释, 以:开头的是注释⾏
                if(event.empty() || event[0] == ':')
                {
                    continue;
                }
                // 检查事件类型: data: JSON字符串
                if(event.compare(0, 6, "data: ") == 0)
                {
                    std::string jsonStr = event.substr(6);
                    ////// 关键代码:处理结束标记(判断流式响应是否结束) ///////
                    if(jsonStr == "[DONE]")
                    {
                        callback("", true);    // 返回true, 代表流式响应结束
                        streamFinish = true;   // 标记流式响应结束
                        return true;
                    }

                    // 反序列化:将JSON字符串 转换为 Json::Value对象
                    Json::Value chunk;  
                    Json::CharReaderBuilder srder;
                    std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
                    std::string errs;
                    bool read_ret = srd->parse(jsonStr.c_str(), jsonStr.c_str() + jsonStr.size(), &chunk, &errs);
                    if(!read_ret)
                    {
                        ERR("Json反序列化失败: {}", errs);
                        return false;
                    }

                    // 提取Json::Value对象中的 content字段
                    if(chunk.isMember("choices") && chunk["choices"].isArray() &&
                        !chunk["choices"].empty() && chunk["choices"][0].isMember("delta") &&
                        chunk["choices"][0]["delta"].isMember("content"))
                    {
                        std::string content = chunk["choices"][0]["delta"]["content"].asString();
                        // 累积到完整响应
                        fullResponse += content;
                        callback(content, false); // 调用回调函数处理增量内容, false代表流式响应未结束
                    }
                }           
            }
            return true; // 继续接收数据
        };
        //////////////////////// HTTP请求构建结束 /////////////////////////

        // 发送HTTP请求并处理结果
        auto res = client.send(req);
        // send的返回值类型是Result类型,Result类型内部实现了operator bool(), 允许将Result类型的实例隐式转换为bool类型
        if(!res)
        {
            // 请求连接失败,⽹络问题、DNS解析失败等
            ERR("HTTP Post error: {}", httplib::to_string(res.error()));
            return "";
        }

        // 确保流正常结束
        if(!streamFinish){
            WARN("Stream ended without [DONE] marker");
            callback("", true);
        }
        return fullResponse;
    }

(1) 接口概述

send() 是 httplib::Client 提供的底层请求发送接口,相比 Get()Post() 等便捷方法,它提供了更精细的控制能力,特别适合流式响应场景


(2) 签名与参数

Result send(const Request& req);
参数 类型 说明
req httplib::Request 完整的请求对象,包含方法、路径、头、体及回调函数
返回值 httplib::Result 请求结果封装,支持布尔判断和错误访问

(3) 核心优势:回调机制

send() 接口的关键在于通过 Request 对象设置响应处理回调函数,实现流式数据处理:

// 创建HTTP Client
httplib::Client client(_base_url);
client.set_connection_timeout(30, 0); // 30秒超时
client.set_read_timeout(300, 0);      // 流式响应需要更⻓的时间

// 设置请求头
httplib::Headers headers = {
    {"Authorization", "Bearer " + _api_key},
    {"Content-Type", "application/json"}
};

httplib::Request req;
req.method = "POST";
req.path = "/chat/completions";
req.headers = headers;
req.body = request_body_str;

// 1. 响应头回调 - 检查状态码
req.response_handler = [&](const httplib::Response& res) {
    if(res.status != 200) {
        gotError = true;
        return false;  // 终止接收
    }
    return true;  // 继续接收响应体
};

// 2. 响应体回调 - 流式接收数据块
req.content_receiver = [&](const char* data, size_t len, uint64_t offset, uint64_t totalLength) {
    buffer.append(data, len);  // 累积数据
    // 处理 SSE 格式...
    return true;  // 继续接收
};

auto res = client.send(req);

回调函数签名对比

回调 签名 作用
response_handler bool(const Response&) 处理响应头,决定是否继续接收
content_receiver bool(const char*, size_t, uint64_t, uint64_t) 处理响应体数据块

(4) 返回值处理

auto res = client.send(req);

// 方式1:布尔判断(网络层成功/失败)
if(!res) {
    ERR("HTTP error: {}", httplib::to_string(res.error()));
    return "";
}

// 方式2:访问响应对象(应用层状态)
if(res->status != 200) {
    ERR("status code: {}", res->status);
}

Result 类型的特性

  • 内部实现了 operator bool(),支持隐式转换为布尔值
  • 网络层失败(连接超时、DNS 解析失败等)→ resfalse
  • 网络层成功 → restrue,可通过 res->status 访问 HTTP 状态码

4.4 httplib::Client::Post接口 和 httplib::Client::send接口的对比

  • 核心区别速览
维度 Post() send()
定位 便捷高层 API 底层通用接口
使用复杂度 简单,一行调用 复杂,需手动构建 Request
响应接收方式 一次性完整接收 支持流式增量接收
回调机制 不支持 支持 response_handler + content_receiver
HTTP 方法 固定 POST 可自定义(GET/POST/PUT/DELETE 等)
适用场景 普通 API 请求、表单提交 SSE 流式响应、大文件下载、自定义协议
代码量

(1) 使用方式对比

Post() - 简洁调用

// 项目中的使用 (DeepSeekProvider.cpp:109-110)
httplib::Headers headers = {
    {"Authorization", "Bearer " + _api_key},
    {"Content-Type", "application/json"}
};
auto res = client.Post("/chat/completions", headers, 
                      request_body_str, "application/json");

send() - 底层控制

// 项目中的使用 (DeepSeekProvider.cpp:228-320)
httplib::Request req;
req.method = "POST";
req.path = "/chat/completions";
req.headers = headers;
req.body = request_body_str;

// 设置响应头回调
req.response_handler = [&](const httplib::Response& res) {
    return (res.status == 200);
};

// 设置响应体流式回调
req.content_receiver = [&](const char* data, size_t len, uint64_t, uint64_t) {
    buffer.append(data, len);
    // 实时处理数据块...
    return true;
};

auto res = client.send(req);

(2) 响应处理机制对比

Post() - 同步阻塞,完整接收

请求发送 ──→ 等待完整响应 ──→ 返回完整响应体
              (阻塞等待)
auto res = client.Post(...);
// 此时 res->body 已经包含完整的响应体
if(res && res->status == 200) {
    std::string full_response = res->body;  // 一次性获取全部数据
}

send() - 流式回调,增量处理

请求发送 ──→ 响应头到达 ──→ response_handler 回调
              │
              ├──→ 数据块1到达 ──→ content_receiver 回调
              ├──→ 数据块2到达 ──→ content_receiver 回调
              └──→ ...更多数据块...
req.content_receiver = [&](const char* data, size_t len, ...) {
    // 实时处理每个数据块
    process_chunk(data, len);
    return true;  // 返回 false 可中断接收
};

(3) 回调函数详解

send() 独有的回调机制

回调函数 触发时机 作用 返回值意义
response_handler 响应头完全接收后 检查状态码、响应头 true=继续接收;false=终止
content_receiver 每个响应体数据块到达时 处理流式数据 true=继续接收;false=终止

回调参数详解

// response_handler 参数
req.response_handler = [&](const httplib::Response& res) {
    int status = res.status;           // HTTP 状态码
    auto headers = res.headers;        // 响应头集合
    return (status == 200);            // 决定是否继续
};

// content_receiver 参数
req.content_receiver = [&](const char* data,      // 数据块指针
                           size_t len,            // 数据块长度
                           uint64_t offset,       // 当前偏移量
                           uint64_t totalLength)  // 总长度(流式可能为 0)
{
    buffer.append(data, len);
    return true;
};

(4) 适用场景对比

选择 Post() 的场景

  • 普通 REST API 调用
  • 表单提交
  • 响应体较小,可一次性接收
  • 代码简洁性优先

选择 send() 的场景

  • SSE(Server-Sent Events)流式响应
  • 大文件下载(边接收边写入)
  • 需要实时处理响应数据
  • 自定义协议实现
  • 需要精确控制请求过程

(5) 在项目中的实际应用

项目使用 Post()(非流式请求):

// DeepSeekProvider.cpp:109-121
auto res = client.Post("/chat/completions", headers, 
                      request_body_str, "application/json");
if(!res) {
    ERR("HTTP Post error: {}", httplib::to_string(res.error()));
    return "";
}
if(res->status != 200) {
    ERR("POST请求失败, status code : {}", res->status);
    return "";
}
// 直接访问完整响应体
std::string full_response = res->body;

项目使用 send()(流式请求):

// DeepSeekProvider.cpp:228-334
req.content_receiver = [&](const char* data, size_t len, ...) {
    buffer.append(data, len);
    // 解析 SSE 格式数据
    while((pos = buffer.find("\n\n")) != std::string::npos) {
        std::string event = buffer.substr(0, pos);
        // 处理每个 SSE 事件...
        if(event.compare(0, 6, "data: ") == 0) {
            std::string jsonStr = event.substr(6);
            if(jsonStr == "[DONE]") {
                callback("", true);  // 通知流结束
                return true;
            }
            // 解析增量内容并回调
            std::string content = parse_content(jsonStr);
            callback(content, false);  // 实时推送
        }
    }
    return true;
};

auto res = client.send(req);

(6) 性能与资源对比

维度 Post() send()
内存占用 高(需完整缓冲响应体) 低(可边接收边处理)
响应延迟 高(需等待完整响应) 低(可实时处理首块数据)
适用数据量 小到中等 任意大小(支持流式)

(7) 错误处理对比

Post() 错误处理

auto res = client.Post(...);
if(!res) {
    // 网络层错误:超时、连接失败、DNS 失败等
    httplib::Error err = res.error();
    switch(err) {
        case httplib::Error::Connection: ...
        case httplib::Error::Timeout: ...
    }
} else if(res->status != 200) {
    // 应用层错误:HTTP 状态码非 200
}

send() 错误处理

// 方式1:通过 response_handler 提前检测
req.response_handler = [&](const httplib::Response& res) {
    if(res.status != 200) {
        gotError = true;
        return false;  // 终止接收
    }
    return true;
};

// 方式2:通过 content_receiver 返回值中断
req.content_receiver = [&](const char*, size_t, ...) {
    if(gotError) return false;  // 提前终止
    // 处理数据...
    return true;
};

auto res = client.send(req);
if(!res) {
    // 网络层错误
}

(8) 何时选择哪个接口?

                    ┌─────────────────────────────────┐
                    │         HTTP 请求需求分析         │
                    └──────────────────┬──────────────┘
                                       │
                    ┌──────────────────┴──────────────────┐
                    │                                     │
              是否需要流式响应?                    普通请求?
                    │                                     │
            ┌───────┴───────┐                   ┌─────────┴─────────┐
            │               │                   │                   │
          需要            不需要              简单请求            复杂需求
            │               │                   │                   │
            ▼               ▼                   ▼                   ▼
       send()           Post()               Post()              send()
            │               │                   │                   │
    SSE/大文件下载      一次性接收           表单/JSON           自定义协议

  • 总结
特性 Post() send()
易用性 ★★★★★ ★★★
灵活性 ★★★ ★★★★★
流式支持 不支持 支持
代码量
推荐场景 普通 API 调用 SSE/流式/自定义场景

核心建议:优先使用 Post() 处理常规请求;当需要流式响应或精细控制时,使用 send()

5. httplib::Result 与 httplib::Response的关系

httplib::Result 是 httplib::Response的包装类

class Result {
private:
    // HasResponse: 连接成功
    // HasError:网络层失败,未连接上应用层
    enum { HasResponse, HasError } state;
    // 记录错误信息
    Error error;
    
    // httplib::Result中包含 httplib::Response对象
    Response response;
    
public:
    operator bool() const { return state == HasResponse; }
    
    Response* operator->() { return &response; }
    Response& operator*() { return response; }
    
    Error error() const { return error; }
};
  • 使用场景(判断 HTTP 请求可能失败的两种情况):
httplib::Result res = client.Post(...);
// 场景 1:网络层失败(连接失败、超时等)
if(!res) {  // Result 本身是无效的
    ERR("HTTP Post error: {}", httplib::to_string(res.error()));
    return "";
}

// 场景 2:请求成功但应用层失败(HTTP 状态码非 200)
if(res->status != 200) {  // Result 有效,但 Response 的状态码不对
    ERR("POST请求失败, status code : {}", res->status);
    return "";
}

DBG("POST请求成功, response body : {}", res->body);

6. Client → Server 完整请求流程(普通响应)

以项目中的创建会话请求为例,完整展示从 httplib::Client 发送请求到 httplib::Server 处理响应的全过程。


(1) 流程总览

┌─────────────────────────────────────────────────────────────────────────────┐
│                        Client 请求发起阶段                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  [1] 构建请求体 ──→ [2] 设置请求头 ──→ [3] 调用 Post() ──→ [4] TCP 连接          │
│                                                                             │
└──────────────────────────────┬──────────────────────────────────────────────┘
                               │ TCP/IP 网络传输
                               ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                        Server 请求处理阶段                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  [5] 接收连接 ──→ [6] 解析 HTTP ──→ [7] 路由匹配 ──→ [8] 执行处理函数            │
│       │                                            │                        │
│       │                                            ▼                        │
│       │                                    [9] 构建响应体                     │
│       │                                            │                        │
│       └────────────────────────────────────────────┼────────────────────────┘
│                                                    ▼                        │
│                                          [10] 发送响应                       │
└──────────────────────────────┬──────────────────────────────────────────────┘
                               │ TCP/IP 网络传输
                               ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                        Client 响应接收阶段                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  [11] 接收响应 ──→ [12] 解析状态码 ──→ [13] 处理响应体                           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

(2) 详细流程分解

  • 阶段一:Client 端准备与发送

步骤 1-4:Client 发起请求

// 假设有一个客户端调用(例如前端页面或其他服务)
httplib::Client client("http://localhost:8080");
client.set_connection_timeout(30, 0);
client.set_read_timeout(60, 0);

// [1] 构建请求体(JSON格式)
std::string request_body = R"({"model": "deepseek-v4-flash"})";

// [2] 设置请求头
httplib::Headers headers = {
    {"Content-Type", "application/json"}
};

// [3] 调用 Post() 发送请求
// [4] 底层建立 TCP 连接,发送 HTTP 请求
auto res = client.Post("/api/session", headers, request_body, "application/json");

发送的 HTTP 请求内容

POST /api/session HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 39

{"model": "deepseek-v4-flash"}

  • 阶段二:Server 端接收与处理

步骤 5:Server 接收连接

// ChatServer.cpp:57-63 - 服务器在独立线程中运行
std::thread serverThread([this](){
    // [5] listen() 阻塞等待并接收客户端连接
    _chatServer->listen(_config.host, _config.port);
});

步骤 6:解析 HTTP 请求

httplib::Server 内部自动完成:

  • 解析 HTTP 请求行(POST /api/session HTTP/1.1
  • 解析请求头(Content-Type, Content-Length 等)
  • 读取请求体({"model": "deepseek-v4-flash"}
  • 构建 httplib::Request 对象

步骤 7:路由匹配

// ChatServer.cpp:457-493 - 路由注册
void ChatServer::setHttpRoutes() {
    // 注册路由表
    _chatServer->Post("/api/session", [this](const httplib::Request& request, httplib::Response& response){
        handleCreateSessionRequest(request, response);  // 匹配后执行此回调
    });
    // ... 其他路由
}

路由匹配规则

  1. 精确匹配优先/api/session 精确匹配
  2. 正则匹配其次/api/session/(.*) 使用正则捕获路径参数
  3. 静态文件最后set_mount_point("/", "./www") 处理其他请求

步骤 8:执行处理函数

void ChatServer::handleCreateSessionRequest(const httplib::Request& request, httplib::Response& response)
{
    // 解析请求体
    Json::Value val;
    std::string json_str = request.body;  // 获取请求体内容
    
    // 反序列化 JSON
    Json::CharReaderBuilder srder;
    std::unique_ptr<Json::CharReader> srd(srder.newCharReader());
    bool ret = srd->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &val, &error);
    
    // 提取参数
    std::string model_name = val["model"].asString();
    
    // 调用 SDK 创建会话
    std::string session_id = _chatSDK->createSession(model_name);
    
    // ... 错误处理
}

步骤 9:构建响应体(处理函数中的内容)

// 构造成功响应
Json::Value resp;
resp["success"] = true;
resp["message"] = "Session created successfully.";

Json::Value data;
data["session_id"] = session_id;
data["model"] = model_name;
resp["data"] = data;

// 序列化为 JSON 字符串
Json::StreamWriterBuilder swber;
std::unique_ptr<Json::StreamWriter> swb(swber.newStreamWriter());
std::stringstream ss;
swb->write(resp, &ss);

// 设置响应内容
response.set_content(ss.str(), "application/json");
response.status = 200;

步骤 10:发送响应

httplib::Server 自动完成:

  • 设置响应头(Content-Type, Content-Length
  • 发送状态行(HTTP/1.1 200 OK
  • 发送响应体

发送的 HTTP 响应内容

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 89

{"success":true,"message":"Session created successfully.","data":{"session_id":"abc123","model":"deepseek-v4-flash"}}

  • 阶段三:Client 端接收与处理

步骤 11-13:Client 处理响应

// 客户端调用代码
auto res = client.Post("/api/session", headers, request_body, "application/json");

// [11] 接收响应
// [12] 解析状态码
if(!res) {
    // 网络层失败:连接超时、DNS 失败等
    std::cerr << "HTTP error: " << httplib::to_string(res.error()) << std::endl;
} else if(res->status == 200) {
    // [13] 处理响应体
    std::string response_body = res->body;
    // 解析 JSON 获取 session_id
    // {"success":true,"message":"Session created successfully.","data":{"session_id":"abc123","model":"deepseek-v4-flash"}}
} else {
    // 应用层失败:HTTP 状态码非 200
    std::cerr << "Error: " << res->status << std::endl;
}

(3) 关键数据结构传递

Client 端                          Server 端
─────────                          ─────────
httplib::Request                   httplib::Request
├── method = "POST"              ←┼→ ├── method = "POST"
├── path = "/api/session"        ←┼→ ├── path = "/api/session"
├── headers = {...}              ←┼→ ├── headers = {...}
└── body = "{...}"               ←┼→ └── body = "{...}"
                                  │
httplib::Response                 httplib::Response
├── status = 200                 ←─┼← ├── status = 200
├── headers = {...}              ←─┼← ├── headers = {...}
└── body = "{...}"               ←─┼← └── body = "{...}"

(4) 总结

阶段 组件 核心操作
请求准备 Client 构建请求体、设置请求头
请求发送 Client 建立 TCP 连接、发送 HTTP 请求
请求接收 Server listen() 阻塞等待、accept() 接收连接
请求解析 Server 解析 HTTP 协议、构建 Request 对象
路由匹配 Server 根据方法+路径查找处理函数
业务处理 Server 执行处理函数、调用 SDK
响应构建 Server 序列化响应体、设置状态码
响应发送 Server 发送 HTTP 响应、关闭连接
响应处理 Client 检查状态码、解析响应体

核心机制:httplib 通过回调函数实现路由分发,将 HTTP 请求转换为业务逻辑调用,实现了网络层与业务层的解耦。

7. Client → Server 完整请求流程(流式响应)

示例场景

服务器:模拟一个天气预测服务,逐城市推送未来 5 天的天气数据。

客户端:实时接收并显示每个城市的天气信息。


(1) 完整可运行代码

  • Server 端
// weather_server.cpp
#include <httplib.h>
#include <jsoncpp/json/json.h>
#include <thread>
#include <chrono>
#include <vector>
#include <sstream>

// 模拟天气数据
struct WeatherData {
    std::string city;
    int temperature;
    std::string condition;
};

// 模拟从数据库获取天气数据
std::vector<WeatherData> getWeatherData() {
    return {
        {"北京", 28, "晴天"},
        {"上海", 32, "多云"},
        {"广州", 35, "雷阵雨"},
        {"成都", 25, "阴天"},
        {"哈尔滨", 18, "小雨"},
    };
}

// 将一条天气数据转为 JSON 字符串
std::string weatherToJson(const WeatherData& w) {
    Json::Value root;
    root["city"] = w.city;
    root["temperature"] = w.temperature;
    root["condition"] = w.condition;
    return Json::writeString(Json::StreamWriterBuilder(), root);
}

int main() {
    httplib::Server server;

    // 流式天气推送接口
    server.Get("/weather/stream", [](const httplib::Request& req, httplib::Response& res) {
        // ============ ① 设置响应头 ============
        res.status = 200;
        res.set_header("Content-Type", "text/event-stream");
        res.set_header("Cache-Control", "no-cache");
        res.set_header("Connection", "keep-alive");
        res.set_header("Access-Control-Allow-Origin", "*");

        // ============ ② 注册流式数据提供者 ============
        res.set_chunked_content_provider("text/event-stream",
            [](size_t offset, httplib::DataSink& sink) -> bool {

                auto weatherList = getWeatherData();

                for (size_t i = 0; i < weatherList.size(); i++) {
                    // 模拟网络延迟(从气象局获取数据需要时间)
                    std::this_thread::sleep_for(std::chrono::seconds(1));

                    // 构建 SSE 格式数据
                    std::string json = weatherToJson(weatherList[i]);
                    std::string sseData = "data: " + json + "\n\n";

                    // 发送给客户端
                    sink.write(sseData.c_str(), sseData.size());
                }

                // 发送结束标记
                std::string doneData = "data: [DONE]\n\n";
                sink.write(doneData.c_str(), doneData.size());
                sink.done();

                return false;  // 不再提供数据
            });
    });

    std::cout << "天气服务器启动: http://0.0.0.0:8080" << std::endl;
    server.listen("0.0.0.0", 8080);
    return 0;
}
  • Client 端
// weather_client.cpp
#include <httplib.h>
#include <jsoncpp/json/json.h>
#include <iostream>
#include <string>

int main() {
    httplib::Client client("http://localhost:8080");
    client.set_connection_timeout(10, 0);
    client.set_read_timeout(30, 0);   // 流式响应需要较长超时

    // ============ ① 构建 Request 对象 ============
    httplib::Request req;
    req.method = "GET";
    req.path = "/weather/stream";

    std::string buffer;       // 接收缓冲区
    bool streamFinished = false;  // 流结束标记
    bool gotError = false;    // 错误标记

    // ============ ② 注册响应头回调 ============
    req.response_handler = [&](const httplib::Response& res) {
        int statusCode = res.status;
        std::cout << "[回调] response_handler 被调用" << std::endl;
        std::cout << "  HTTP 状态码: " << statusCode << std::endl;

        if (statusCode != 200) {
            gotError = true;
            std::cerr << "  错误: 非200状态码" << std::endl;
            return false;  // 终止接收
        }
        std::cout << "  状态码正常,准备接收数据..." << std::endl;
        return true;  // 继续接收响应体
    };

    // ============ ③ 注册响应体回调 ============
    req.content_receiver = [&](const char* data, size_t len,
                               uint64_t offset, uint64_t totalLength) {
        if (gotError) {
            return false;  // 之前出错,不再接收
        }

        // 追加新数据到缓冲区
        buffer.append(data, len);

        // 处理 SSE 事件(以 \n\n 分隔)
        size_t pos = 0;
        while ((pos = buffer.find("\n\n")) != std::string::npos) {
            std::string event = buffer.substr(0, pos);
            buffer.erase(0, pos + 2);  // 移除已处理的事件

            if (event.empty()) continue;

            // 解析 "data: ..." 格式
            if (event.compare(0, 6, "data: ") == 0) {
                std::string payload = event.substr(6);

                if (payload == "[DONE]") {
                    std::cout << "\n[回调] content_receiver: 收到结束标记 [DONE]" << std::endl;
                    streamFinished = true;
                    return true;
                }

                // 解析 JSON
                Json::Value root;
                Json::CharReaderBuilder builder;
                std::string errs;
                std::istringstream iss(payload);
                if (Json::parseFromStream(builder, iss, &root, &errs)) {
                    std::cout << "  📍 " << root["city"].asString()
                              << " | 🌡️  " << root["temperature"].asInt() << "°C"
                              << " | ☁️  " << root["condition"].asString() << std::endl;
                }
            }
        }
        return true;  // 继续接收
    };

    // ============ ④ 发送请求 ============
    std::cout << "正在请求天气数据..." << std::endl;
    auto res = client.send(req);

    // ============ ⑤ 检查结果 ============
    if (!res) {
        std::cerr << "网络错误: " << httplib::to_string(res.error()) << std::endl;
        return 1;
    }

    std::cout << "\n✅ 天气数据接收完毕!" << std::endl;
    return 0;
}

(2) 运行效果

  • Client 端输出
正在请求天气数据...
[回调] response_handler 被调用
  HTTP 状态码: 200
  状态码正常,准备接收数据...
  📍 北京 | 🌡️  28°C | ☁️  晴天
  📍 上海 | 🌡️  32°C | ☁️  多云
  📍 广州 | 🌡️  35°C | ☁️  雷阵雨
  📍 成都 | 🌡️  25°C | ☁️  阴天
  📍 哈尔滨 | 🌡️  18°C | ☁️  小雨

[回调] content_receiver: 收到结束标记 [DONE]

✅ 天气数据接收完毕!

时间线

第1秒: 📍 北京 | 🌡️  28°C | ☁️  晴天
第2秒: 📍 上海 | 🌡️  32°C | ☁️  多云
第3秒: 📍 广州 | 🌡️  35°C | ☁️  雷阵雨
第4秒: 📍 成都 | 🌡️  25°C | ☁️  阴天
第5秒: 📍 哈尔滨 | 🌡️  18°C | ☁️  小雨
第5秒: [DONE]

(3) 流程步骤详解

┌─────────────────────────────────────────────────────────────────────┐
│  步骤   Client 端                          Server 端                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ①     构建 Request 对象                                            │
│        req.method = "GET"                                           │
│        req.path = "/weather/stream"                                 │
│                                                                     │
│  ②     注册 response_handler                                        │
│        检查状态码                                                    │
│                                                                     │
│  ③     注册 content_receiver                                        │
│        解析 SSE 事件                                                 │
│                                                                     │
│  ④     client.send(req)                                              │
│        └── TCP 连接 ────────────────────→                           │
│        └── 发送 HTTP 请求 ──────────────→   接收请求                  │
│                                            路由匹配 GET /weather/stream
│                                            执行处理函数                │
│                                                                     │
│                                            ⑤ 设置响应头              │
│                                            Content-Type: text/event-stream
│                                                                     │
│                                            ⑥ 注册 provider          │
│                                            set_chunked_content_provider()
│                                            处理函数返回                │
│                                                                     │
│                                            ⑦ 框架发送响应头          │
│                                            HTTP/1.1 200 OK           │
│        ⑧ response_handler 回调 ←───────────  Transfer-Encoding: chunked
│        状态码=200,返回 true                                         │
│                                                                     │
│                                            ⑨ 框架调用 provider       │
│                                            sleep(1s) 模拟延迟         │
│                                            sink.write("data: {...}\n\n")
│        ⑩ content_receiver 回调 ←─────────── "北京"天气数据            │
│        解析 JSON,打印天气信息                                        │
│                                                                     │
│        ⑪-⑭ 重复⑨-⑩ 4次 ←─────────────── 上海、广州、成都、哈尔滨    │
│                                                                     │
│                                            ⑮ 发送结束标记            │
│                                            sink.write("data: [DONE]\n\n")
│                                            sink.done()               │
│        ⑯ content_receiver 回调 ←─────────── "[DONE]"                 │
│        检测到结束标记,streamFinished=true                            │
│                                                                     │
│        ⑰ 连接关闭 ←─────────────────────── ⑱ 连接关闭               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

(4) 关键对比:Client 端回调 vs Server 端回调

┌─────────────────────────────────────────────────────────────────┐
│                    Client 端 (接收方)                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  response_handler:  "响应头到了,我要不要继续?"                 │
│     参数: (const Response& res)                                 │
│     返回: true=继续接收 / false=终止                            │
│                                                                 │
│  content_receiver:  "新数据块到了,我处理一下"                   │
│     参数: (data, len, offset, totalLength)                      │
│     返回: true=继续接收 / false=终止                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    Server 端 (发送方)                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  provider:  "该我发送数据了,我要发什么?"                       │
│     参数: (offset, DataSink& sink)                              │
│     通过 sink.write() 发送数据                                  │
│     通过 sink.done() 结束流                                     │
│     返回: true=还有数据(框架再调) / false=结束                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

(5) HTTP 层面数据流

Server 发送的原始 HTTP 响应

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked

1d\r\n
data: {"city":"北京","temperature":28,"condition":"晴天"}\n\n
1d\r\n
data: {"city":"上海","temperature":32,"condition":"多云"}\n\n
1f\r\n
data: {"city":"广州","temperature":35,"condition":"雷阵雨"}\n\n
1d\r\n
data: {"city":"成都","temperature":25,"condition":"阴天"}\n\n
1f\r\n
data: {"city":"哈尔滨","temperature":18,"condition":"小雨"}\n\n
11\r\n
data: [DONE]\n\n
0\r\n
\r\n

Transfer-Encoding: chunked 格式说明

1d              ← 块大小(十六进制,1d=29字节)
data: {...}\n\n ← 块数据
0               ← 大小为0,表示结束

(6) 为什么用流式?

  • 普通响应方式
// Server 端:必须等所有数据准备好
server.Get("/weather/all", [](auto& req, auto& res) {
    auto data = getAllWeatherData();  // 等5秒,全部收集完
    res.set_content(data, "application/json");  // 一次性发送
});

客户端体验:等待 5 秒 → 一次性显示全部。

  • 流式响应方式
// Server 端:边生成边发送
server.Get("/weather/stream", [](auto& req, auto& res) {
    res.set_chunked_content_provider("text/event-stream",
        [](auto& sink) {
            for (auto& city : cities) {
                sleep(1);
                sink.write(cityJson(city));  // 逐条发送
            }
            sink.done();
        });
});

客户端体验:每隔 1 秒显示一条,不等待。

普通响应:  [──────── 等待5秒 ────────] → 一次性显示全部
流式响应:  [显示北京] → [显示上海] → [显示广州] → ...

(7) 总结

要素 Client 端 Server 端
核心接口 client.send(req) res.set_chunked_content_provider()
数据方向 接收数据 发送数据
回调函数 response_handler + content_receiver provider
数据操作 解析 data 参数 调用 sink.write()
结束信号 收到 [DONE] 调用 sink.done()
适用场景 消费流式 API 提供流式 API

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐