1. 为什么要在 C 里同时写 HTTP 服务端和客户端如果你正在做 C 项目大概率会遇到这种需求本地起一个轻量 HTTP 服务接收外部回调或者给前端页面提供接口同时这个进程又要作为客户端去调用远端的大模型 API 做推理、翻译、摘要。用 Boost.Beast 写太重用 cpp-httplib 又觉得功能偏薄mongoose 是一个很合适的中间选择——单头文件、事件驱动、服务端和客户端一套 API 全包。这篇内容聚焦一个具体场景在同一个 C 工程里用 mongoose 搭起 HTTP 服务端监听 8000 端口再用 mongoose 的客户端能力去请求 TaoToken 的统一 API 通道把 AI 能力接进来。我会给出可复制的 CMake 依赖引入、服务端与客户端最小代码骨架以及通过 TaoToken 统一 Key 接入时的 config.toml 与 settings.json 配置骨架。目标很明确你照着敲完能一次跑通服务端监听和客户端调用两个动作。mongoose 的核心概念其实就三个mg_mgr是事件管理器持有所有活动连接mg_connection描述单个连接ev_handler是事件处理函数所有收发逻辑都写在里面。服务端用mg_bind建立监听连接客户端用mg_connect_http发起呼出连接两者共用同一个事件循环mg_mgr_poll。理解这一点后面的代码就顺了。2. TaoToken 前置准备统一 Key 与 API 通道在写代码之前先把外部依赖准备好。TaoToken 在这里扮演的角色是统一 Key 和统一 API 通道你不需要在代码里硬编码多家厂商的地址和密钥而是通过一个 Key、一个 Base URL 去访问模型能力。这对 C 这种改配置成本较高的项目尤其友好——换模型只改配置文件不动编译产物。你需要做三件事。第一注册并登录后进入控制台创建一个 API Key。第二确认你要调用的模型名称比如对话类模型。第三记下 API 的基础地址代码里会用到https://taotoken.net/api这个前缀。控制台入口在这里创建 Key 的动作在 API Keys 页面完成控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite如果你只是想先验证模型通不通不想写代码可以直接用模型对话页面发一条消息试试模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文档里有完整的请求格式说明写客户端代码前建议扫一眼接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteKey 拿到后不要写进源码提交到仓库。我习惯把它放进环境变量或者本地配置文件代码里读配置。下面第三节会给出 config.toml 和 settings.json 两套骨架你可以按项目习惯选一套。3. 可复制配置CMake 引入与配置文件骨架3.1 CMake 引入 mongoosemongoose 的引入方式很省心它就是一个mongoose.c加一个mongoose.h。你可以把这两个文件放进third_party/mongoose/目录然后用 CMake 把它编成静态库。下面是我实际在用的 CMakeLists.txt 骨架cmake_minimum_required(VERSION 3.16) project(mongoose_http_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # mongoose 作为静态库 add_library(mongoose STATIC third_party/mongoose/mongoose.c ) target_include_directories(mongoose PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/mongoose ) # 服务端可执行文件 add_executable(http_server src/http_server.cpp) target_link_libraries(http_server PRIVATE mongoose) # 客户端可执行文件 add_executable(http_client src/http_client.cpp) target_link_libraries(http_client PRIVATE mongoose)注意 mongoose.c 是 C 文件用add_library(... STATIC ...)时 CMake 会自动按 C 编译头文件里已经做了extern C处理C 侧直接 include 即可。如果你在 Windows 上用 MSVC可能需要额外链接ws2_32库if(WIN32) target_link_libraries(mongoose PUBLIC ws2_32) endif()3.2 config.toml 配置骨架把 TaoToken 的接入信息放进 config.toml代码启动时读取。这样换 Key、换模型都不用重新编译[server] listen_port 8000 [taotoken] base_url https://taotoken.net/api api_key sk-你的Key model 你的模型名称 timeout_ms 300003.3 settings.json 配置骨架如果你的项目更习惯 JSON用这套等价配置{ server: { listen_port: 8000 }, taotoken: { base_url: https://taotoken.net/api, api_key: sk-你的Key, model: 你的模型名称, timeout_ms: 30000 } }两套配置字段名保持一致方便你在代码里做一层抽象读 TOML 还是读 JSON 只影响解析函数不影响业务逻辑。实际项目里我建议把 api_key 从配置文件里抽出来用环境变量TAOTOKEN_API_KEY覆盖避免误提交。4. 服务端与客户端最小代码骨架4.1 服务端监听 8000 并回显请求服务端的核心是mg_bind建立监听连接然后mg_set_protocol_http_websocket把它标记为 HTTP 协议事件循环里处理MG_EV_HTTP_REQUEST。下面这段代码监听 8000 端口收到请求后把原始请求内容回显给客户端#include mongoose.h #include string static const char *s_http_port 8000; static void ev_handler(struct mg_connection *c, int ev, void *p) { if (ev MG_EV_HTTP_REQUEST) { struct http_message *hm (struct http_message *)p; // 取出 URL 和 body std::string url(hm-uri.p, hm-uri.len); std::string body(hm-body.p, hm-body.len); // 构造响应内容 std::string resp path url \nbody body \n; mg_send_head(c, 200, resp.size(), Content-Type: text/plain); mg_printf(c, %.*s, (int)resp.size(), resp.c_str()); } } int main(void) { struct mg_mgr mgr; struct mg_connection *c; mg_mgr_init(mgr, NULL); c mg_bind(mgr, s_http_port, ev_handler); if (c NULL) { return 1; } mg_set_protocol_http_websocket(c); for (;;) { mg_mgr_poll(mgr, 1000); } mg_mgr_free(mgr); return 0; }这里用到的几个函数值得记一下。mg_send_head发送响应头参数依次是连接、状态码、内容长度、额外头信息。mg_printf发送格式化字符串%.*s配合长度和指针可以安全输出非 null 结尾的缓冲区。如果你要做流式响应可以用mg_send_http_chunk和mg_printf_http_chunk但记得先发Transfer-Encoding: chunked头最后发一个空块表示结束。4.2 客户端调用 TaoToken 统一 API客户端用mg_connect_http发起请求事件处理函数里等MG_EV_HTTP_REPLY拿响应。下面这段代码向 TaoToken 的 API 地址发一个 POST 请求body 是标准的对话请求格式#include mongoose.h #include string static int s_done 0; static void ev_handler(struct mg_connection *c, int ev, void *p) { if (ev MG_EV_HTTP_REPLY) { struct http_message *hm (struct http_message *)p; std::string rsp(hm-body.p, hm-body.len); // 这里拿到的是模型返回的 JSON printf(response: %s\n, rsp.c_str()); c-flags | MG_F_CLOSE_IMMEDIATELY; s_done 1; } else if (ev MG_EV_CLOSE) { s_done 1; } } int main(void) { struct mg_mgr mgr; mg_mgr_init(mgr, NULL); const char *url https://taotoken.net/api/v1/chat/completions; const char *headers Content-Type: application/json\r\n Authorization: Bearer sk-你的Key\r\n; const char *post_data {\model\:\你的模型名称\, \messages\:[{\role\:\user\, \content\:\用一句话解释什么是 HTTP\}]}; mg_connect_http(mgr, ev_handler, url, headers, post_data); while (s_done 0) { mg_mgr_poll(mgr, 1000); } mg_mgr_free(mgr); return 0; }mg_connect_http的四个参数分别是事件管理器、事件处理函数、URL、额外请求头和 POST 数据。注意 URL 必须是完整地址包含协议和路径。请求头里Authorization用 Bearer 格式带上你的 KeyContent-Type声明 JSON。4.3 把配置读进来上面客户端代码里 Key 和模型名是硬编码的实际项目里应该从 config.toml 或 settings.json 读。以 TOML 为例你可以用 toml11 这类头文件库解析然后拼出 headers 和 post_dataauto cfg toml::parse(config.toml); std::string api_key cfg[taotoken][api_key].as_string(); std::string model cfg[taotoken][model].as_string(); std::string base cfg[taotoken][base_url].as_string(); std::string url base /v1/chat/completions; std::string headers Content-Type: application/json\r\n Authorization: Bearer api_key \r\n; std::string post_data {\model\:\ model \, \messages\:[{\role\:\user\, \content\:\hello\}]};这样换模型、换 Key 只改配置文件代码零改动。5. 验证请求与成功结果5.1 编译与启动服务端按第三节的 CMake 配置建好目录结构后执行mkdir build cd build cmake .. make ./http_server服务端启动后不会有输出这是正常的它在mg_mgr_poll里阻塞等待。另开一个终端用 curl 验证监听是否生效curl -X POST http://127.0.0.1:8000/hello -d nametaotoken如果服务端正常你会看到类似这样的回显path/hello bodynametaotoken这说明mg_bind监听成功MG_EV_HTTP_REQUEST事件被正确触发mg_send_head和mg_printf也工作正常。5.2 运行客户端调用 TaoToken编译出http_client后直接运行./http_client如果 Key、模型名、网络都正常你会看到模型返回的 JSON里面包含choices数组和message.content字段。到这一步服务端监听和客户端调用就都跑通了。5.3 一次完整的联调动作更贴近真实场景的做法是客户端请求打到本地服务端服务端再转发给 TaoToken。你可以在服务端的ev_handler里收到请求后调用一次mg_connect_http去请求 TaoToken把结果回写给原始客户端。这样一次动作就验证了服务端接收、客户端外呼、响应回写三条链路。联调时建议先单独跑通客户端直连再叠加服务端转发出问题好定位。6. 本篇常见错误排查6.1 服务端绑定失败返回 NULLmg_bind返回 NULL 通常是端口被占用或者权限不足。8000 端口一般不需要 root先检查是不是有别的进程占着lsof -i :8000如果有输出换一个端口或者杀掉占用进程。另外注意mg_bind的端口参数是字符串写成8000而不是8000的整数。6.2 客户端收不到响应就退出如果s_done很快变成 1 但没打印响应多半是MG_EV_CLOSE先触发了。常见原因是 URL 写错、DNS 解析失败或者 TLS 握手失败。mongoose 默认支持 HTTPS但需要确认编译时链接了 OpenSSL。检查 CMake 里是否加了find_package(OpenSSL REQUIRED) target_link_libraries(mongoose PUBLIC OpenSSL::SSL OpenSSL::Crypto)如果不想折腾 TLS可以先用 HTTP 地址做本地验证确认逻辑通了再换 HTTPS。6.3 请求返回 401 或 403这是鉴权问题。检查三处Key 是否正确、Authorization头格式是否是Bearer sk-xxx、请求头之间是否用了\r\n分隔。mongoose 对请求头格式比较敏感少一个\r\n或者用了\n都可能导致服务端解析失败。另外确认 Key 没有多余空格。6.4 响应体被截断如果返回的 JSON 不完整检查mg_send_head里的 content_length 是否和实际发送的字节数一致。用mg_printf发送时%.*s的长度参数必须是实际字节数不是字符数。中文字符一个占 3 字节算错长度会导致截断。6.5 事件循环 CPU 占用高mg_mgr_poll的第二个参数是超时毫秒数传 1000 表示最多阻塞 1 秒。如果你传了 0它会立刻返回循环变成忙等CPU 就上去了。保持 1000 或者按需调整即可。7. 继续深入的方向服务端和客户端骨架跑通后往下可以做的方向不少。服务端侧可以加路由分发根据hm-uri走不同的处理逻辑可以加静态文件服务用mg_serve_http直接托管前端页面。客户端侧可以封装一个通用的请求函数把 URL、headers、body 作为参数复用到多个 API 调用点。如果你打算把这个骨架用在长期编码或者 Agent 类项目里建议关注一下 Coding Plan它在配额和调用方式上更适合持续性的开发场景Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入过程中如果遇到鉴权或者请求格式的问题接入文档里有完整的字段说明和示例比对着改最快接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteKey 的管理和轮换在 API Keys 页面操作建议给不同项目建不同的 Key方便排查和回收API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite最后提醒一句mongoose 的版本更新比较频繁API 在不同大版本间有变化。本文代码基于较新的 mongoose 7.x 风格如果你用的是老版本mg_bind可能叫mg_bind但参数略有不同MG_EV_HTTP_REQUEST的事件名也可能有差异。遇到编译报错先对一下头文件里的宏定义比盲目搜索快。