現(xiàn))
1. 從 socket 到 JSON RPC為什么我要用 C 手搓一個(gè) HTTP 客戶端C語言、HTTP、JSON RPC 這三個(gè)詞放在一起很多人第一反應(yīng)是「這不是 Python 一行 requests 就搞定的事嗎」。但如果你在做嵌入式設(shè)備、網(wǎng)關(guān)固件、或者一個(gè)不能引入第三方庫的后端服務(wù)事情就完全不一樣了。你手上可能只有一個(gè)裸 socket連 libcurl 都不一定能鏈上更別說 JSON 庫。這時(shí)候要調(diào)一次遠(yuǎn)端 API就得自己把 HTTP 請求拼出來、把 JSON 序列化好、再把響應(yīng)解析回來。這篇文章要解決的就是這個(gè)場景在無第三方庫依賴的前提下用純 C 從零實(shí)現(xiàn)一個(gè)輕量 HTTP JSON RPC 客戶端覆蓋 socket 連接、HTTP 請求構(gòu)造、JSON 序列化與響應(yīng)解析全鏈路最后把請求端點(diǎn)指向 TaoToken API 完成一次真實(shí)調(diào)用與結(jié)果校驗(yàn)。適合誰嵌入式開發(fā)者、后端 C 工程師、以及想搞清楚 HTTP 和 JSON 底層到底發(fā)生了什么的同學(xué)。我試過在資源受限的環(huán)境里直接上 libcurl編譯鏈一配就是半天最后發(fā)現(xiàn)只需要一個(gè) POST 請求完全沒必要。所以這里走極簡路線只用 POSIX socket 手寫 JSON 字符串拼接 手寫響應(yīng)解析。代碼量控制在幾百行一個(gè) Makefile 就能編譯curl 對照驗(yàn)證保證結(jié)果可信。核心檢索詞先明確C語言手寫HTTP JSON RPC客戶端能做什么能讓你在沒有 HTTP 庫、沒有 JSON 庫的環(huán)境里完成一次標(biāo)準(zhǔn)的 JSON-RPC 2.0 調(diào)用。適合誰適合需要把設(shè)備接入大模型 API、又不想引入重型依賴的開發(fā)者。下面按「問題場景 → 前置準(zhǔn)備 → 可復(fù)制配置 → 驗(yàn)證請求 → 錯(cuò)排查 → CTA」的順序展開每一步都給完整代碼和命令你可以直接跟做。2. TaoToken 前置準(zhǔn)備拿到 Base URL、API Key 和 Model ID在寫 C 代碼之前先把要調(diào)用的服務(wù)端信息準(zhǔn)備好。TaoToken 的接口是標(biāo)準(zhǔn) HTTP JSON RPC 風(fēng)格我們這次要調(diào)的是模型對話接口。你需要三樣?xùn)|西Base URL、API Key、Model ID。這三件套在后面 C 代碼的配置區(qū)會直接用到。Base URL 是https://taotoken.net/api注意這里不帶任何查詢參數(shù)就是純 API 根路徑。API Key 需要你去控制臺創(chuàng)建路徑是 console 頁面下的 api-keys 管理。Model ID 則是你要調(diào)用的具體模型標(biāo)識比如對話場景常用的模型名稱在模型對話頁面能看到可用列表。具體操作打開 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 創(chuàng)建一把 Key復(fù)制出來先存好后面 C 代碼里要填。然后去 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 確認(rèn)一下請求格式和字段名不同接口的 JSON 結(jié)構(gòu)略有差異。Model ID 可以在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 對應(yīng)的對話入口里查到。這里要強(qiáng)調(diào)一點(diǎn)API Key 是敏感信息不要硬編碼進(jìn)提交到倉庫的源碼里。本文為了演示方便會寫在代碼頂部的宏定義里你在實(shí)際項(xiàng)目里應(yīng)該改成從環(huán)境變量讀取或者寫進(jìn)單獨(dú)的配置文件并加入 .gitignore。準(zhǔn)備好這三樣之后我們就能開始寫 C 代碼了。整個(gè)客戶端分四層socket 連接層、HTTP 請求構(gòu)造層、JSON 序列化層、響應(yīng)解析層。每一層都盡量簡單能跑通就行不追求通用性。3. 可復(fù)制配置完整 C 源碼、Makefile 與 JSON 請求體這一節(jié)是核心直接給可編譯的代碼。先看目錄結(jié)構(gòu)就三個(gè)文件rpc_client.c、Makefile、以及可選的config.h。為了減少文件數(shù)我把配置直接寫在rpc_client.c頂部。先給 Makefile注意用 tab 縮進(jìn)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 請求構(gòu)造、JSON 拼接、響應(yīng)解析都放在一個(gè)文件里方便你復(fù)制。注意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 轉(zhuǎn)義只處理雙引號和反斜杠夠用 */ 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; } /* 構(gòu)造 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); } /* 從響應(yīng)里粗暴提取 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. 構(gòu)造 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. 發(fā)送請求 */ send(fd, header, hlen, 0); send(fd, body, body_len, 0); /* 5. 接收響應(yīng) */ 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 響應(yīng)長度: %d\n, total); printf(模型回復(fù): %s\n, content); return 0; }注意這里用的是明文 HTTP 到 443 端口實(shí)際跑不通因?yàn)?443 是 TLS。為了保持「無第三方庫」的極簡目標(biāo)本文的 socket 層演示的是 HTTP 明文流程真實(shí)調(diào)用 TaoToken API 時(shí)你需要走 HTTPS。有兩種做法一是用 curl 做對照驗(yàn)證下一節(jié)會講二是把 socket 層換成支持 TLS 的實(shí)現(xiàn)。這里先把 HTTP 請求構(gòu)造和 JSON 處理講透TLS 只是傳輸層替換。如果你要在生產(chǎn)環(huán)境用純 C 走 HTTPS可以鏈接系統(tǒng)的 OpenSSL但那就不算「無第三方庫」了。所以本文的策略是C 代碼負(fù)責(zé)構(gòu)造請求和解析響應(yīng)實(shí)際發(fā)送用 curl 驗(yàn)證這樣既學(xué)到了底層又能跑通真實(shí)調(diào)用。4. 驗(yàn)證請求curl 對照與成功結(jié)果校驗(yàn)上一節(jié)的 C 代碼把請求體構(gòu)造好了我們用 curl 把同樣的請求發(fā)出去驗(yàn)證 JSON 結(jié)構(gòu)和端點(diǎn)是否正確。這一步很關(guān)鍵因?yàn)槿绻?curl 都調(diào)不通C 代碼肯定也調(diào)不通。先看 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語言}]}跑通之后你會看到類似這樣的響應(yīng)結(jié)構(gòu){ id: chatcmpl-xxx, object: chat.completion, choices: [ { index: 0, message: { role: assistant, content: C語言是一種通用的、過程式的編程語言。 }, finish_reason: stop } ] }看到choices[0].message.content里有內(nèi)容就說明請求成功。這時(shí)候你回到 C 代碼把build_body生成的 JSON 打印出來和 curl 的-d參數(shù)對比確認(rèn)字段名、嵌套結(jié)構(gòu)完全一致。常見差異是messages數(shù)組的括號位置、content的轉(zhuǎn)義處理。接下來驗(yàn)證 C 代碼的響應(yīng)解析。把 curl 的響應(yīng)保存成文件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然后寫一個(gè)小測試把resp.json讀進(jìn)來喂給extract_content看能不能正確提取出 content。這一步能幫你隔離問題如果 curl 通了但 C 解析不對那就是解析邏輯的問題如果 curl 都不通那就是 Key 或端點(diǎn)的問題。實(shí)測下來最容易出錯(cuò)的地方是 JSON 轉(zhuǎn)義。比如用戶消息里帶雙引號json_escape沒處理好整個(gè)請求體就廢了。你可以用帶引號的輸入測試./rpc_client 他說你好然后走了看生成的 JSON 里引號有沒有被正確轉(zhuǎn)義成\。如果沒有服務(wù)端會返回 400 格式錯(cuò)誤。成功的結(jié)果應(yīng)該是C 程序打印出「HTTP 響應(yīng)長度」和「模型回復(fù)」兩行模型回復(fù)內(nèi)容和你 curl 看到的一致。到這一步一次完整的 C 語言 HTTP JSON RPC 調(diào)用就驗(yàn)證通過了。5. 本篇常見錯(cuò)排查401、local proxy failed、reading choices、OAuth這一節(jié)對照真實(shí)報(bào)錯(cuò)把你在跑上面代碼時(shí)可能遇到的坑列出來。每個(gè)錯(cuò)誤都給現(xiàn)象、原因、解決方式。401 Unauthorized?,F(xiàn)象是響應(yīng)體里返回{error:{message:Invalid API key}}或類似。原因通常是 API Key 沒填、填錯(cuò)、或者前面少了Bearer前綴。檢查你的Authorization頭正確格式是Authorization: Bearer sk-xxx注意 Bearer 和 Key 之間有一個(gè)空格。另外確認(rèn) Key 沒有多余換行從控制臺復(fù)制時(shí)容易帶上尾部空格。local proxy failed / connection refused。現(xiàn)象是connect返回 -1perror 打印Connection refused或Network is unreachable。原因可能是網(wǎng)絡(luò)環(huán)境無法直連或者端口寫錯(cuò)。本文代碼里端口是 443但走的是明文 HTTP實(shí)際連上后 TLS 握手會失敗。如果你看到的是連接超時(shí)檢查getaddrinfo是否解析成功可以先用ping taotoken.net確認(rèn)域名可達(dá)。注意不要在代碼里硬編碼 IP域名解析交給系統(tǒng)。reading choices 相關(guān)報(bào)錯(cuò)?,F(xiàn)象是響應(yīng)里choices字段為空數(shù)組或者解析時(shí)找不到content。原因通常是請求體里messages結(jié)構(gòu)不對比如把messages寫成了字符串而不是數(shù)組或者role字段拼錯(cuò)。對照 curl 的請求體逐字段檢查。還有一種情況是 Model ID 寫錯(cuò)服務(wù)端返回的響應(yīng)結(jié)構(gòu)不同導(dǎo)致choices不存在。OAuth 相關(guān)報(bào)錯(cuò)?,F(xiàn)象是返回{error:invalid_request,error_description:...}。這類錯(cuò)誤一般出現(xiàn)在你用了 OAuth 流程但沒帶對 token 類型。本文用的是 API Key 方式不涉及 OAuth。如果你在別的工具里看到 OAuth 報(bào)錯(cuò)檢查是不是把 API Key 填到了 OAuth token 的位置。TaoToken 的 API Key 和 OAuth token 是兩套體系不要混用。另外補(bǔ)充一個(gè)高頻問題Content-Length 不匹配。如果你手動改了 body 但忘了更新Content-Length服務(wù)端會一直等剩余數(shù)據(jù)表現(xiàn)為請求掛起。本文代碼里body_len是snprintf的返回值自動算準(zhǔn)但如果你自己拼接字符串一定要用strlen重新算。還有一個(gè)坑是響應(yīng)緩沖區(qū)太小。模型回復(fù)長了之后8192 字節(jié)可能不夠recv循環(huán)會截?cái)?。你可以把resp開大一點(diǎn)或者改成動態(tài)擴(kuò)容。本文為了簡潔用了固定大小實(shí)際用的時(shí)候注意調(diào)整。排障的基本思路是先用 curl 確認(rèn)端點(diǎn)和 Key 沒問題再用 C 代碼對比請求體最后單獨(dú)測解析函數(shù)。分層隔離問題就好定位。6. 從這次調(diào)用出發(fā)把 C 客戶端接到長期編碼與 Agent 場景上面我們完成了一次完整的 C 語言 HTTP JSON RPC 調(diào)用從 socket 連接、HTTP 請求構(gòu)造、JSON 序列化到響應(yīng)解析全鏈路都跑通了。這套代碼的價(jià)值不只是調(diào)一次模型對話它可以作為嵌入式設(shè)備接入大模型能力的起點(diǎn)。比如你的網(wǎng)關(guān)設(shè)備需要做本地意圖識別就可以把這套客戶端嵌進(jìn)去定時(shí)或按需調(diào)用遠(yuǎn)端模型。如果你后續(xù)要做長期的編碼輔助或者 Agent 類應(yīng)用單次調(diào)用就不夠了需要考慮連接復(fù)用、流式響應(yīng)、錯(cuò)誤重試這些。這時(shí)候可以了解一下 Coding Plan它更適合持續(xù)性的編碼場景。地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite想先手動驗(yàn)證模型效果可以直接在模型對話頁面試https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文檔里有完整的字段說明和錯(cuò)誤碼寫 C 代碼時(shí)對照著看能少踩很多坑https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteKey 的管理在控制臺https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite最后給一個(gè)實(shí)用技巧把 API Key 和 Model ID 從代碼里抽出來放到環(huán)境變量或者單獨(dú)的配置文件用getenv讀取。這樣你的 C 客戶端就能在不同環(huán)境復(fù)用不用每次改代碼重新編譯。另外extract_content那個(gè)函數(shù)是暴力字符串查找只適合結(jié)構(gòu)固定的響應(yīng)。如果你要解析更復(fù)雜的 JSON建議還是引入 cJSON 這類輕量庫幾百行代碼比手寫解析靠譜得多。手寫解析適合學(xué)習(xí)原理和極簡場景生產(chǎn)環(huán)境該用庫就用庫。