cpp-httplib 完全指南:核心 API、普通响应与流式响应全解析
文章目录
一、cpp-httplib 的介绍
C++ HTTP 库(cpp-httplib)是一个轻量级的 C++ HTTP 客户端/服务器库,它提供了简单的 API 来创建 HTTP 服务器和客户端,支持同步和异步操作。以下是一些关于 cpp-httplib 的主要特点:
- 轻量级: cpp-httplib 的设计目标是简单和轻量,只有一个头文件包含即可,不依赖于任何外部库。
- 跨平台: 它支持多种操作系统,包括 Windows、Linux 和 macOS。
- 同步和异步操作: 库提供了同步和异步两种操作方式,允许开发者根据需要选择。
- 支持 HTTP/1.1: 它实现了 HTTP/1.1 协议,包括持久连接和管道化。
- Multipart form-data: 支持发送和接收 multipart/form-data 类型的请求,这对于文件上传非常有用。
- SSL/TLS 支持: 通过使用 OpenSSL 或 mbedTLS 库,cpp-httplib 支持 HTTPS 和 WSS。
- 简单易用: API 设计简洁,易于学习和使用。
- 性能: 尽管是轻量级库,但性能表现良好,适合多种应用场景。
- 社区活跃: 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 解析失败等)→
res为false - 网络层成功 →
res为true,可通过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); // 匹配后执行此回调
});
// ... 其他路由
}
路由匹配规则:
- 精确匹配优先:
/api/session精确匹配 - 正则匹配其次:
/api/session/(.*)使用正则捕获路径参数 - 静态文件最后:
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 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)