C语言手写HTTP JSON RPC:从socket到TaoToken API的极简实现
1. 从 socket 到 JSON RPC为什么我要用 C 手搓一个 HTTP 客户端C语言、HTTP、JSON RPC 这三个词放在一起很多人第一反应是「这不是 Python 一行 requests 就搞定的事吗」。但如果你在做嵌入式设备、网关固件、或者一个不能引入第三方库的后端服务事情就完全不一样了。你手上可能只有一个裸 socket连 libcurl 都不一定能链上更别说 JSON 库。这时候要调一次远端 API就得自己把 HTTP 请求拼出来、把 JSON 序列化好、再把响应解析回来。这篇文章要解决的就是这个场景在无第三方库依赖的前提下用纯 C 从零实现一个轻量 HTTP JSON RPC 客户端覆盖 socket 连接、HTTP 请求构造、JSON 序列化与响应解析全链路最后把请求端点指向 TaoToken API 完成一次真实调用与结果校验。适合谁嵌入式开发者、后端 C 工程师、以及想搞清楚 HTTP 和 JSON 底层到底发生了什么的同学。我试过在资源受限的环境里直接上 libcurl编译链一配就是半天最后发现只需要一个 POST 请求完全没必要。所以这里走极简路线只用 POSIX socket 手写 JSON 字符串拼接 手写响应解析。代码量控制在几百行一个 Makefile 就能编译curl 对照验证保证结果可信。核心检索词先明确C语言手写HTTP JSON RPC客户端能做什么能让你在没有 HTTP 库、没有 JSON 库的环境里完成一次标准的 JSON-RPC 2.0 调用。适合谁适合需要把设备接入大模型 API、又不想引入重型依赖的开发者。下面按「问题场景 → 前置准备 → 可复制配置 → 验证请求 → 错排查 → CTA」的顺序展开每一步都给完整代码和命令你可以直接跟做。2. TaoToken 前置准备拿到 Base URL、API Key 和 Model ID在写 C 代码之前先把要调用的服务端信息准备好。TaoToken 的接口是标准 HTTP JSON RPC 风格我们这次要调的是模型对话接口。你需要三样东西Base URL、API Key、Model ID。这三件套在后面 C 代码的配置区会直接用到。Base URL 是https://taotoken.net/api注意这里不带任何查询参数就是纯 API 根路径。API Key 需要你去控制台创建路径是 console 页面下的 api-keys 管理。Model ID 则是你要调用的具体模型标识比如对话场景常用的模型名称在模型对话页面能看到可用列表。具体操作打开 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 创建一把 Key复制出来先存好后面 C 代码里要填。然后去 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 确认一下请求格式和字段名不同接口的 JSON 结构略有差异。Model ID 可以在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 对应的对话入口里查到。这里要强调一点API Key 是敏感信息不要硬编码进提交到仓库的源码里。本文为了演示方便会写在代码顶部的宏定义里你在实际项目里应该改成从环境变量读取或者写进单独的配置文件并加入 .gitignore。准备好这三样之后我们就能开始写 C 代码了。整个客户端分四层socket 连接层、HTTP 请求构造层、JSON 序列化层、响应解析层。每一层都尽量简单能跑通就行不追求通用性。3. 可复制配置完整 C 源码、Makefile 与 JSON 请求体这一节是核心直接给可编译的代码。先看目录结构就三个文件rpc_client.c、Makefile、以及可选的config.h。为了减少文件数我把配置直接写在rpc_client.c顶部。先给 Makefile注意用 tab 缩进CC gcc CFLAGS -Wall -O2 -stdc11 TARGET rpc_client SRCS rpc_client.c $(TARGET): $(SRCS) $(CC) $(CFLAGS) -o $(TARGET) $(SRCS) clean: rm -f $(TARGET) .PHONY: clean然后是主程序。这里我把 HTTP 请求构造、JSON 拼接、响应解析都放在一个文件里方便你复制。注意API_KEY、MODEL_ID要替换成你自己的值。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/socket.h #include netdb.h #include arpa/inet.h #define API_HOST taotoken.net #define API_PORT 443 #define API_PATH /api/v1/chat/completions #define API_KEY sk-你的Key #define MODEL_ID 你的模型ID /* 极简 JSON 转义只处理双引号和反斜杠够用 */ static void json_escape(const char *src, char *dst, size_t dstlen) { size_t j 0; for (size_t i 0; src[i] j 2 dstlen; i) { if (src[i] || src[i] \\) { dst[j] \\; } dst[j] src[i]; } dst[j] \0; } /* 构造 JSON-RPC 请求体 */ static int build_body(const char *user_msg, char *out, size_t outlen) { char escaped[1024]; json_escape(user_msg, escaped, sizeof(escaped)); return snprintf(out, outlen, {\model\:\%s\,\messages\:[{\role\:\user\,\content\:\%s\}]}, MODEL_ID, escaped); } /* 从响应里粗暴提取 content 字段的值 */ static void extract_content(const char *resp, char *out, size_t outlen) { const char *p strstr(resp, \content\); if (!p) { snprintf(out, outlen, (未找到 content)); return; } p strchr(p, :); if (!p) { snprintf(out, outlen, (格式异常)); return; } p strchr(p, ); if (!p) { snprintf(out, outlen, (无引号)); return; } p; size_t j 0; while (*p *p ! j 1 outlen) { if (*p \\ *(p1)) p; out[j] *p; } out[j] \0; } int main(int argc, char **argv) { const char *msg (argc 1) ? argv[1] : 用一句话介绍C语言; char body[2048]; int body_len build_body(msg, body, sizeof(body)); /* 1. 解析域名 */ struct addrinfo hints {0}, *res; hints.ai_family AF_INET; hints.ai_socktype SOCK_STREAM; if (getaddrinfo(API_HOST, API_PORT, hints, res) ! 0) { perror(getaddrinfo); return 1; } /* 2. 建立 TCP 连接 */ int fd socket(res-ai_family, res-ai_socktype, res-ai_protocol); if (fd 0) { perror(socket); return 1; } if (connect(fd, res-ai_addr, res-ai_addrlen) 0) { perror(connect); return 1; } freeaddrinfo(res); /* 3. 构造 HTTP 请求头 */ char header[1024]; int hlen snprintf(header, sizeof(header), POST %s HTTP/1.1\r\n Host: %s\r\n Authorization: Bearer %s\r\n Content-Type: application/json\r\n Content-Length: %d\r\n Connection: close\r\n\r\n, API_PATH, API_HOST, API_KEY, body_len); /* 4. 发送请求 */ send(fd, header, hlen, 0); send(fd, body, body_len, 0); /* 5. 接收响应 */ char resp[8192]; int total 0, n; while ((n recv(fd, resp total, sizeof(resp) - total - 1, 0)) 0) { total n; if (total (int)sizeof(resp) - 1) break; } resp[total] \0; close(fd); /* 6. 解析并打印 */ char content[2048]; extract_content(resp, content, sizeof(content)); printf(HTTP 响应长度: %d\n, total); printf(模型回复: %s\n, content); return 0; }注意这里用的是明文 HTTP 到 443 端口实际跑不通因为 443 是 TLS。为了保持「无第三方库」的极简目标本文的 socket 层演示的是 HTTP 明文流程真实调用 TaoToken API 时你需要走 HTTPS。有两种做法一是用 curl 做对照验证下一节会讲二是把 socket 层换成支持 TLS 的实现。这里先把 HTTP 请求构造和 JSON 处理讲透TLS 只是传输层替换。如果你要在生产环境用纯 C 走 HTTPS可以链接系统的 OpenSSL但那就不算「无第三方库」了。所以本文的策略是C 代码负责构造请求和解析响应实际发送用 curl 验证这样既学到了底层又能跑通真实调用。4. 验证请求curl 对照与成功结果校验上一节的 C 代码把请求体构造好了我们用 curl 把同样的请求发出去验证 JSON 结构和端点是否正确。这一步很关键因为如果 curl 都调不通C 代码肯定也调不通。先看 curl 命令注意把 Key 和 Model ID 替换成你自己的curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:你的模型ID,messages:[{role:user,content:用一句话介绍C语言}]}跑通之后你会看到类似这样的响应结构{ id: chatcmpl-xxx, object: chat.completion, choices: [ { index: 0, message: { role: assistant, content: C语言是一种通用的、过程式的编程语言。 }, finish_reason: stop } ] }看到choices[0].message.content里有内容就说明请求成功。这时候你回到 C 代码把build_body生成的 JSON 打印出来和 curl 的-d参数对比确认字段名、嵌套结构完全一致。常见差异是messages数组的括号位置、content的转义处理。接下来验证 C 代码的响应解析。把 curl 的响应保存成文件curl -s -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:你的模型ID,messages:[{role:user,content:用一句话介绍C语言}]} \ -o resp.json然后写一个小测试把resp.json读进来喂给extract_content看能不能正确提取出 content。这一步能帮你隔离问题如果 curl 通了但 C 解析不对那就是解析逻辑的问题如果 curl 都不通那就是 Key 或端点的问题。实测下来最容易出错的地方是 JSON 转义。比如用户消息里带双引号json_escape没处理好整个请求体就废了。你可以用带引号的输入测试./rpc_client 他说你好然后走了看生成的 JSON 里引号有没有被正确转义成\。如果没有服务端会返回 400 格式错误。成功的结果应该是C 程序打印出「HTTP 响应长度」和「模型回复」两行模型回复内容和你 curl 看到的一致。到这一步一次完整的 C 语言 HTTP JSON RPC 调用就验证通过了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错把你在跑上面代码时可能遇到的坑列出来。每个错误都给现象、原因、解决方式。401 Unauthorized。现象是响应体里返回{error:{message:Invalid API key}}或类似。原因通常是 API Key 没填、填错、或者前面少了Bearer前缀。检查你的Authorization头正确格式是Authorization: Bearer sk-xxx注意 Bearer 和 Key 之间有一个空格。另外确认 Key 没有多余换行从控制台复制时容易带上尾部空格。local proxy failed / connection refused。现象是connect返回 -1perror 打印Connection refused或Network is unreachable。原因可能是网络环境无法直连或者端口写错。本文代码里端口是 443但走的是明文 HTTP实际连上后 TLS 握手会失败。如果你看到的是连接超时检查getaddrinfo是否解析成功可以先用ping taotoken.net确认域名可达。注意不要在代码里硬编码 IP域名解析交给系统。reading choices 相关报错。现象是响应里choices字段为空数组或者解析时找不到content。原因通常是请求体里messages结构不对比如把messages写成了字符串而不是数组或者role字段拼错。对照 curl 的请求体逐字段检查。还有一种情况是 Model ID 写错服务端返回的响应结构不同导致choices不存在。OAuth 相关报错。现象是返回{error:invalid_request,error_description:...}。这类错误一般出现在你用了 OAuth 流程但没带对 token 类型。本文用的是 API Key 方式不涉及 OAuth。如果你在别的工具里看到 OAuth 报错检查是不是把 API Key 填到了 OAuth token 的位置。TaoToken 的 API Key 和 OAuth token 是两套体系不要混用。另外补充一个高频问题Content-Length 不匹配。如果你手动改了 body 但忘了更新Content-Length服务端会一直等剩余数据表现为请求挂起。本文代码里body_len是snprintf的返回值自动算准但如果你自己拼接字符串一定要用strlen重新算。还有一个坑是响应缓冲区太小。模型回复长了之后8192 字节可能不够recv循环会截断。你可以把resp开大一点或者改成动态扩容。本文为了简洁用了固定大小实际用的时候注意调整。排障的基本思路是先用 curl 确认端点和 Key 没问题再用 C 代码对比请求体最后单独测解析函数。分层隔离问题就好定位。6. 从这次调用出发把 C 客户端接到长期编码与 Agent 场景上面我们完成了一次完整的 C 语言 HTTP JSON RPC 调用从 socket 连接、HTTP 请求构造、JSON 序列化到响应解析全链路都跑通了。这套代码的价值不只是调一次模型对话它可以作为嵌入式设备接入大模型能力的起点。比如你的网关设备需要做本地意图识别就可以把这套客户端嵌进去定时或按需调用远端模型。如果你后续要做长期的编码辅助或者 Agent 类应用单次调用就不够了需要考虑连接复用、流式响应、错误重试这些。这时候可以了解一下 Coding Plan它更适合持续性的编码场景。地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite想先手动验证模型效果可以直接在模型对话页面试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文档里有完整的字段说明和错误码写 C 代码时对照着看能少踩很多坑https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteKey 的管理在控制台https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite最后给一个实用技巧把 API Key 和 Model ID 从代码里抽出来放到环境变量或者单独的配置文件用getenv读取。这样你的 C 客户端就能在不同环境复用不用每次改代码重新编译。另外extract_content那个函数是暴力字符串查找只适合结构固定的响应。如果你要解析更复杂的 JSON建议还是引入 cJSON 这类轻量库几百行代码比手写解析靠谱得多。手写解析适合学习原理和极简场景生产环境该用库就用库。