简介:这份资源围绕 libcurl 与微信公众号消息推送展开,面向需要在服务端或客户端程序中集成微信报警通知能力的 C/C++ 开发者,尤其适合已有一定网络编程基础、希望快速落地告警推送功能的技术人员。压缩包共 106 个文件,约 3.59MB,以 85 个 .h 头文件为主体,配合 4 个 .lib 静态库、4 个 .dll 动态库及 3 个 .pdb 调试符号,另有 .cpp 源码、.vcxproj 工程文件、.sln 解决方案、Makefile.am 与 run.bat 脚本等,覆盖从源码编译到运行调试的完整链路。资源核心演示了借助 libcurl 调用微信接口、将报警信息推送到粉丝微信账号的实现方式,读者可据此理解 HTTP 请求封装、SSL 依赖库配置以及工程组织思路,并直接复用其中的库文件与示例代码,减少自行摸索接口调用与编译环境的时间。目前已有 319 人学习下载,可作为微信消息推送模块的参考实现。1. 从一条推送说起:libcurl-weixin-message 到底解决什么问题很多做后台服务的开发者都遇到过这个场景:业务系统跑在纯 C/C++ 环境里,没有 Python 的 requests,没有 Node 的 axios,甚至连个像样的 HTTP 客户端库都没装,但产品经理突然要求「用户下单后给微信公众号推一条模板消息」。这时候你打开微信官方文档,发现接口是 HTTPS 的,要 POST JSON,要带 access_token,还要处理证书校验。手写 socket 加 OpenSSL 不是不行,但光是拼 HTTP 报文和解析响应就够你调一整天。libcurl-weixin-message 就是冲着这个痛点来的。它把 libcurl 这个久经考验的传输库和微信公众号的消息推送接口封装在一起,让你在 C/C++ 项目里用几行代码就能完成「获取 access_token → 组装模板消息 → POST 到微信服务器 → 解析返回结果」这一整套流程。适合的人群很明确:做嵌入式后台、游戏服务端、物联网网关,或者任何以 C/C++ 为主力语言、又需要对接微信公众号消息能力的开发者。你不需要引入庞大的 SDK,也不用被某个云厂商的封装绑死,核心依赖就是 libcurl 加一个 JSON 解析库,编译出来干干净净。2. 架构拆解:libcurl 与微信接口之间隔了哪几层2.1 整体分层设计这个项目的结构不复杂,但分层思路值得说清楚。从下往上大致是三层:最底层是 libcurl 的 easy 接口,负责实际的 TCP 连接、TLS 握手、HTTP 报文收发;中间层是微信 API 的协议适配层,处理 access_token 的获取与缓存、URL 拼接、请求体 JSON 序列化、响应体反序列化;最上层是业务调用层,暴露给使用者的就是几个函数,比如初始化配置、发送模板消息、查询发送状态。常见做法是把 access_token 的缓存放在中间层,用一个结构体保存 token 字符串和过期时间戳。因为微信的 access_token 有 7200 秒有效期,而且获取频率有限制,每次发消息都去重新获取会被限流。我一般会在中间层做一个简单的内存缓存,过期前 5 分钟自动刷新,刷新失败时保留旧 token 继续用,避免因为一次网络抖动导致整个推送链路挂掉。2.2 目录结构说明虽然没有拿到完整的源码包,但按这类项目的惯例,目录结构大致会是这样:libcurl-weixin-message/ ├── include/ │ └── weixin_message.h # 对外暴露的头文件 ├── src/ │ ├── weixin_message.c # 核心实现:token 管理、消息组装、HTTP 调用 │ ├── http_client.c # libcurl 封装:初始化、回调、超时设置 │ └── json_util.c # JSON 序列化与解析辅助 ├── examples/ │ └── send_template.c # 最小可运行示例 ├── tests/ │ └── test_token.c # token 缓存逻辑的单元测试 ├── CMakeLists.txt └── README.md这个结构的好处是职责清晰:http_client.c 只管传输,json_util.c 只管数据格式,weixin_message.c 负责业务逻辑编排。你要换掉 JSON 库或者换掉 HTTP 库,改动范围是可控的。2.3 核心数据结构先看对外暴露的关键结构体,这决定了你调用时怎么传参:/* weixin_message.h */ typedef struct { char appid[64]; /* 公众号的 AppID */ char secret[64]; /* 公众号的 AppSecret */ char access_token[512]; /* 缓存的 access_token */ long token_expire_at; /* token 过期时间戳(秒) */ int timeout_ms; /* HTTP 请求超时,单位毫秒 */ } weixin_ctx_t; typedef struct { char touser[128]; /* 接收消息的用户 openid */ char template_id[128]; /* 模板消息 ID */ char url[512]; /* 点击消息跳转链接,可为空 */ char data_json[2048]; /* 模板数据,JSON 格式字符串 */ } weixin_template_msg_t;weixin_ctx_t是整个会话的上下文,初始化时填入 appid 和 secret,后续所有调用都带着它。token_expire_at用time(NULL)的返回值来比较,简单可靠。weixin_template_msg_t里的data_json直接放 JSON 字符串,而不是用结构体嵌套,这样调用方可以灵活组装任意模板字段,不用为每个模板改代码。2.4 获取 access_token 的实现这是整个链路的第一步,也是最容易出问题的一步:/* 获取或刷新 access_token,成功返回 0,失败返回 -1 */ int weixin_get_token(weixin_ctx_t *ctx) { long now = (long)time(NULL); /* 如果 token 还在有效期内(预留 300 秒余量),直接返回 */ if (ctx->access_token[0] != '\0' && now < ctx->token_expire_at - 300) { return 0; } char url[512]; snprintf(url, sizeof(url), "https://api.weixin.qq.com/cgi-bin/token" "?grant_type=client_credential&appid=%s&secret=%s", ctx->appid, ctx->secret); char resp[4096] = {0}; if (http_get(ctx, url, resp, sizeof(resp)) != 0) { return -1; /* 网络层失败,保留旧 token */ } /* 解析 {"access_token":"xxx","expires_in":7200} */ char token[512] = {0}; int expires_in = 0; if (json_get_string(resp, "access_token", token, sizeof(token)) != 0 || json_get_int(resp, "expires_in", &expires_in) != 0) { return -1; /* 解析失败,可能是 errcode 返回 */ } strncpy(ctx->access_token, token, sizeof(ctx->access_token) - 1); ctx->token_expire_at = now + expires_in; return 0; }这段代码有几个关键点。第一,缓存判断里预留了 300 秒余量,避免 token 在请求飞行途中过期。第二,网络失败时直接返回 -1 但不清理旧 token,这样如果只是临时抖动,下一次调用还能用旧 token 撑一会儿。第三,解析失败时也返回 -1,因为微信在 appid 或 secret 错误时会返回{"errcode":40013,"errmsg":"invalid appid"},这时候重试也没用,需要调用方检查配置。2.5 发送模板消息的完整流程拿到 token 之后,发送模板消息就是组装 JSON 再 POST:int weixin_send_template(weixin_ctx_t *ctx, const weixin_template_msg_t *msg) { if (weixin_get_token(ctx) != 0) { return -1; } /* 组装请求体 */ char body[4096]; snprintf(body, sizeof(body), "{\"touser\":\"%s\",\"template_id\":\"%s\",\"url\":\"%s\",\"data\":%s}", msg->touser, msg->template_id, msg->url, msg->data_json); char url[512]; snprintf(url, sizeof(url), "https://api.weixin.qq.com/cgi-bin/message/template/send" "?access_token=%s", ctx->access_token); char resp[4096] = {0}; if (http_post_json(ctx, url, body, resp, sizeof(resp)) != 0) { return -1; } /* 检查返回的 errcode */ int errcode = 0; if (json_get_int(resp, "errcode", &errcode) == 0 && errcode != 0) { /* 42001 表示 token 过期,清掉缓存下次重试 */ if (errcode == 42001) { ctx->access_token[0] = '\0'; } return -1; } return 0; }这里有个细节值得注意:微信返回的 JSON 里,成功时errcode为 0 或者干脆没有这个字段,失败时才有。所以判断逻辑是「能解析出 errcode 且不为 0 才算失败」。另外对 42001(token 过期)做了特殊处理,清空缓存让下次调用重新获取,这是一种轻量级的自愈机制。2.6 libcurl 封装的关键参数http_client.c 里对 libcurl 的封装,有几个参数必须设置对:static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { /* 标准写法:把响应体追加到用户缓冲区 */ strncat((char *)userdata, (char *)ptr, size * nmemb); return size * nmemb; } int http_post_json(weixin_ctx_t *ctx, const char *url, const char *body, char *resp, size_t resp_len) { CURL *curl = curl_easy_init(); if (!curl) return -1; struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "Content-Type: application/json"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, ctx->timeout_ms); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); /* 生产环境必须开启 */ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); CURLcode ret = curl_easy_perform(curl); curl_slist_free_all(headers); curl_easy_cleanup(curl); return (ret == CURLE_OK) ? 0 : -1; }CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST这两个参数是血泪教训。开发阶段有人图省事设成 0 跳过证书校验,上线后一旦遇到中间人攻击,appsecret 和用户 openid 全暴露。生产环境必须开启,如果遇到证书链问题,正确做法是更新系统的 CA 证书包,而不是关校验。3. 跑起来:从编译到发出第一条模板消息3.1 依赖准备在动手编译之前,先把依赖装齐。以常见的 Linux 发行版为例:# Debian/Ubuntu 系 sudo apt-get install libcurl4-openssl-dev libjson-c-dev cmake gcc # RHEL/CentOS 系 sudo yum install libcurl-devel json-c-devel cmake gcclibcurl 负责 HTTPS 传输,json-c 负责 JSON 解析。如果你用的是别的 JSON 库(比如 cJSON),把 json_util.c 里的几个函数换掉就行,接口保持json_get_string和json_get_int两个签名不变。3.2 编译与链接项目用 CMake 管理构建,标准流程:mkdir build && cd build cmake .. make生成的产物包括静态库libweixin_message.a和示例程序send_template。链接时注意顺序,libcurl 依赖一堆系统库:gcc -o myapp myapp.c -L./build -lweixin_message -lcurl -ljson-c -lpthread如果链接时报undefined reference to SSL_CTX_new之类的错误,说明 libcurl 是动态链接 OpenSSL 的,需要补上-lssl -lcrypto。这个坑在静态编译时特别常见。3.3 最小可运行示例examples/send_template.c 里是一个完整的调用示例:#include <stdio.h> #include "weixin_message.h" int main(void) { weixin_ctx_t ctx = {0}; strncpy(ctx.appid, "你的appid", sizeof(ctx.appid) - 1); strncpy(ctx.secret, "你的secret", sizeof(ctx.secret) - 1); ctx.timeout_ms = 5000; weixin_template_msg_t msg = {0}; strncpy(msg.touser, "用户openid", sizeof(msg.touser) - 1); strncpy(msg.template_id, "模板ID", sizeof(msg.template_id) - 1); strncpy(msg.url, "https://example.com/order/123", sizeof(msg.url) - 1); /* 模板数据:first、keyword1、keyword2、remark 是常见字段 */ snprintf(msg.data_json, sizeof(msg.data_json), "{\"first\":{\"value\":\"您的订单已发货\"}," "\"keyword1\":{\"value\":\"订单号12345\"}," "\"keyword2\":{\"value\":\"顺丰速运\"}," "\"remark\":{\"value\":\"点击查看详情\"}}"); if (weixin_send_template(&ctx, &msg) == 0) { printf("发送成功\n"); } else { printf("发送失败,请检查日志\n"); } return 0; }编译运行后,如果配置正确,目标用户就能在微信里收到一条模板消息。data_json里的字段名必须和你在公众号后台选的模板字段一一对应,写错了微信会返回errcode: 47003,提示参数不匹配。3.4 参数配置速查参数位置说明常见值appidweixin_ctx_t公众号后台「基本配置」里获取wx 开头的 18 位字符串secretweixin_ctx_t同上,注意保密32 位字符串timeout_msweixin_ctx_tHTTP 超时,太短容易误判失败3000~10000touserweixin_template_msg_t接收者的 openid,不是微信号28 位左右字符串template_idweixin_template_msg_t模板消息 ID后台模板列表里复制data_jsonweixin_template_msg_t模板数据,字段名要对上JSON 字符串4. 避坑排查:推送失败时先看这几处4.1 返回 40001 或 42001:token 相关错误现象是发送接口返回{"errcode":40001,"errmsg":"invalid credential"}或 42001。原因通常是 token 缓存逻辑有问题,或者多个进程共用同一个 appid 导致 token 互相覆盖。微信的 access_token 是全局唯一的,A 进程刷新后 B 进程手里的旧 token 就失效了。解决办法是把 token 缓存放到 Redis 或数据库里做集中管理,而不是每个进程各自缓存。如果项目规模小,至少保证只有一个进程负责刷新。4.2 返回 40013:appid 无效现象是获取 token 时直接返回invalid appid。原因一般是 appid 填错了,或者把订阅号的 appid 用在了服务号接口上。检查公众号后台「基本配置」页面,确认 appid 和 secret 是同一套。另外注意不要有多余空格,从网页复制时经常带不可见字符。4.3 HTTPS 请求超时或证书错误现象是 curl 返回CURLE_SSL_CACERT或直接超时。原因是系统的 CA 证书包太旧,不认微信服务器的新证书链。解决办法是更新 ca-certificates 包,或者用CURLOPT_CAINFO指定一个较新的 cacert.pem 文件。千万不要用CURLOPT_SSL_VERIFYPEER=0来绕过,这是给自己埋雷。4.4 返回 47003:模板参数不匹配现象是发送时返回argument invalid。原因是 data_json 里的字段名和模板定义不一致。比如模板里定义的是keyword1,你写成了keyword_1,或者模板要求first和remark必填但你漏了。解决办法是去公众号后台「模板消息」页面,点开对应模板,逐字段核对名称和顺序。4.5 内存泄漏与并发问题现象是服务跑几天后内存持续上涨,或者多线程同时发送时偶发崩溃。原因是 libcurl 的curl_global_init没有调用,或者多个线程共用一个 CURL 句柄。正确做法是在程序启动时调用一次curl_global_init(CURL_GLOBAL_ALL),每个线程用自己的 CURL 句柄,用完curl_easy_cleanup释放。如果用了连接池,注意curl_easy_reset之后要重新设置所有选项。5. 二次开发:换 JSON 库、加异步、做重试拿到源码后,最常见的改造方向有三个。第一个是换 JSON 库。项目默认用 json-c,但很多嵌入式环境更习惯 cJSON,因为 cJSON 只有两个文件,移植方便。改造时只需要重写 json_util.c 里的json_get_string和json_get_int,保持函数签名不变,上层代码一行不用动。我一般会先写一个测试用例,把微信返回的几种典型响应(成功、token 错误、参数错误)都跑一遍,确认解析结果一致再替换。第二个改造方向是加异步发送。同步调用在推送量大时会把业务线程堵死,尤其是微信接口偶尔抽风响应慢的时候。做法是把weixin_send_template的调用丢到一个独立的工作线程或者线程池里,主线程只管往队列里塞任务。队列可以用简单的环形缓冲区实现,每个任务保存weixin_ctx_t的副本和weixin_template_msg_t的副本,避免多线程访问同一块内存。注意weixin_ctx_t里的 token 缓存要做成线程安全的,加个互斥锁保护读写。第三个方向是加重试机制。微信接口不是 100% 可用的,偶尔会返回 5xx 或者超时。我的习惯是封装一层weixin_send_template_with_retry,最多重试 3 次,每次间隔翻倍(1 秒、2 秒、4 秒)。但要注意,42001 这种 token 过期错误重试前必须先刷新 token,而 40013 这种配置错误重试多少次都没用,应该直接返回失败并打日志。下面是一个重试逻辑的骨架:int weixin_send_with_retry(weixin_ctx_t *ctx, const weixin_template_msg_t *msg) { int delays[] = {1000, 2000, 4000}; /* 毫秒 */ for (int i = 0; i < 3; i++) { if (weixin_send_template(ctx, msg) == 0) { return 0; } /* 如果是 token 过期,清缓存后立即重试,不等待 */ if (ctx->access_token[0] == '\0') { continue; } usleep(delays[i] * 1000); } return -1; }验证改造是否成功,我一般会写一个压力测试:起 10 个线程,每个线程发 100 条消息,观察内存曲线和成功率。如果成功率低于 99%,就要看日志里失败的原因分布,是网络超时多还是 token 错误多,针对性优化。从那以后我每次对接新的 HTTP 接口,都强制走一遍「先跑通单次调用 → 再加缓存 → 再加异步 → 最后压测」的流程,绝不跳过任何一步。希望帮到你。本文还有配套的精品资源,点击获取
简介:这份资源围绕 libcurl 与微信公众号消息推送展开,面向需要在服务端或客户端程序中集成微信报警通知能力的 C/C++ 开发者,尤其适合已有一定网络编程基础、希望快速落地告警推送功能的技术人员。压缩包共 106 个文件,约 3.59MB,以 85 个 .h 头文件为主体,配合 4 个 .lib 静态库、4 个 .dll 动态库及 3 个 .pdb 调试符号,另有 .cpp 源码、.vcxproj 工程文件、.sln 解决方案、Makefile.am 与 run.bat 脚本等,覆盖从源码编译到运行调试的完整链路。资源核心演示了借助 libcurl 调用微信接口、将报警信息推送到粉丝微信账号的实现方式,读者可据此理解 HTTP 请求封装、SSL 依赖库配置以及工程组织思路,并直接复用其中的库文件与示例代码,减少自行摸索接口调用与编译环境的时间。目前已有 319 人学习下载,可作为微信消息推送模块的参考实现。1. 从一条推送说起:libcurl-weixin-message 到底解决什么问题很多做后台服务的开发者都遇到过这个场景:业务系统跑在纯 C/C++ 环境里,没有 Python 的 requests,没有 Node 的 axios,甚至连个像样的 HTTP 客户端库都没装,但产品经理突然要求「用户下单后给微信公众号推一条模板消息」。这时候你打开微信官方文档,发现接口是 HTTPS 的,要 POST JSON,要带 access_token,还要处理证书校验。手写 socket 加 OpenSSL 不是不行,但光是拼 HTTP 报文和解析响应就够你调一整天。libcurl-weixin-message 就是冲着这个痛点来的。它把 libcurl 这个久经考验的传输库和微信公众号的消息推送接口封装在一起,让你在 C/C++ 项目里用几行代码就能完成「获取 access_token → 组装模板消息 → POST 到微信服务器 → 解析返回结果」这一整套流程。适合的人群很明确:做嵌入式后台、游戏服务端、物联网网关,或者任何以 C/C++ 为主力语言、又需要对接微信公众号消息能力的开发者。你不需要引入庞大的 SDK,也不用被某个云厂商的封装绑死,核心依赖就是 libcurl 加一个 JSON 解析库,编译出来干干净净。2. 架构拆解:libcurl 与微信接口之间隔了哪几层2.1 整体分层设计这个项目的结构不复杂,但分层思路值得说清楚。从下往上大致是三层:最底层是 libcurl 的 easy 接口,负责实际的 TCP 连接、TLS 握手、HTTP 报文收发;中间层是微信 API 的协议适配层,处理 access_token 的获取与缓存、URL 拼接、请求体 JSON 序列化、响应体反序列化;最上层是业务调用层,暴露给使用者的就是几个函数,比如初始化配置、发送模板消息、查询发送状态。常见做法是把 access_token 的缓存放在中间层,用一个结构体保存 token 字符串和过期时间戳。因为微信的 access_token 有 7200 秒有效期,而且获取频率有限制,每次发消息都去重新获取会被限流。我一般会在中间层做一个简单的内存缓存,过期前 5 分钟自动刷新,刷新失败时保留旧 token 继续用,避免因为一次网络抖动导致整个推送链路挂掉。2.2 目录结构说明虽然没有拿到完整的源码包,但按这类项目的惯例,目录结构大致会是这样:libcurl-weixin-message/ ├── include/ │ └── weixin_message.h # 对外暴露的头文件 ├── src/ │ ├── weixin_message.c # 核心实现:token 管理、消息组装、HTTP 调用 │ ├── http_client.c # libcurl 封装:初始化、回调、超时设置 │ └── json_util.c # JSON 序列化与解析辅助 ├── examples/ │ └── send_template.c # 最小可运行示例 ├── tests/ │ └── test_token.c # token 缓存逻辑的单元测试 ├── CMakeLists.txt └── README.md这个结构的好处是职责清晰:http_client.c 只管传输,json_util.c 只管数据格式,weixin_message.c 负责业务逻辑编排。你要换掉 JSON 库或者换掉 HTTP 库,改动范围是可控的。2.3 核心数据结构先看对外暴露的关键结构体,这决定了你调用时怎么传参:/* weixin_message.h */ typedef struct { char appid[64]; /* 公众号的 AppID */ char secret[64]; /* 公众号的 AppSecret */ char access_token[512]; /* 缓存的 access_token */ long token_expire_at; /* token 过期时间戳(秒) */ int timeout_ms; /* HTTP 请求超时,单位毫秒 */ } weixin_ctx_t; typedef struct { char touser[128]; /* 接收消息的用户 openid */ char template_id[128]; /* 模板消息 ID */ char url[512]; /* 点击消息跳转链接,可为空 */ char data_json[2048]; /* 模板数据,JSON 格式字符串 */ } weixin_template_msg_t;weixin_ctx_t是整个会话的上下文,初始化时填入 appid 和 secret,后续所有调用都带着它。token_expire_at用time(NULL)的返回值来比较,简单可靠。weixin_template_msg_t里的data_json直接放 JSON 字符串,而不是用结构体嵌套,这样调用方可以灵活组装任意模板字段,不用为每个模板改代码。2.4 获取 access_token 的实现这是整个链路的第一步,也是最容易出问题的一步:/* 获取或刷新 access_token,成功返回 0,失败返回 -1 */ int weixin_get_token(weixin_ctx_t *ctx) { long now = (long)time(NULL); /* 如果 token 还在有效期内(预留 300 秒余量),直接返回 */ if (ctx->access_token[0] != '\0' && now < ctx->token_expire_at - 300) { return 0; } char url[512]; snprintf(url, sizeof(url), "https://api.weixin.qq.com/cgi-bin/token" "?grant_type=client_credential&appid=%s&secret=%s", ctx->appid, ctx->secret); char resp[4096] = {0}; if (http_get(ctx, url, resp, sizeof(resp)) != 0) { return -1; /* 网络层失败,保留旧 token */ } /* 解析 {"access_token":"xxx","expires_in":7200} */ char token[512] = {0}; int expires_in = 0; if (json_get_string(resp, "access_token", token, sizeof(token)) != 0 || json_get_int(resp, "expires_in", &expires_in) != 0) { return -1; /* 解析失败,可能是 errcode 返回 */ } strncpy(ctx->access_token, token, sizeof(ctx->access_token) - 1); ctx->token_expire_at = now + expires_in; return 0; }这段代码有几个关键点。第一,缓存判断里预留了 300 秒余量,避免 token 在请求飞行途中过期。第二,网络失败时直接返回 -1 但不清理旧 token,这样如果只是临时抖动,下一次调用还能用旧 token 撑一会儿。第三,解析失败时也返回 -1,因为微信在 appid 或 secret 错误时会返回{"errcode":40013,"errmsg":"invalid appid"},这时候重试也没用,需要调用方检查配置。2.5 发送模板消息的完整流程拿到 token 之后,发送模板消息就是组装 JSON 再 POST:int weixin_send_template(weixin_ctx_t *ctx, const weixin_template_msg_t *msg) { if (weixin_get_token(ctx) != 0) { return -1; } /* 组装请求体 */ char body[4096]; snprintf(body, sizeof(body), "{\"touser\":\"%s\",\"template_id\":\"%s\",\"url\":\"%s\",\"data\":%s}", msg->touser, msg->template_id, msg->url, msg->data_json); char url[512]; snprintf(url, sizeof(url), "https://api.weixin.qq.com/cgi-bin/message/template/send" "?access_token=%s", ctx->access_token); char resp[4096] = {0}; if (http_post_json(ctx, url, body, resp, sizeof(resp)) != 0) { return -1; } /* 检查返回的 errcode */ int errcode = 0; if (json_get_int(resp, "errcode", &errcode) == 0 && errcode != 0) { /* 42001 表示 token 过期,清掉缓存下次重试 */ if (errcode == 42001) { ctx->access_token[0] = '\0'; } return -1; } return 0; }这里有个细节值得注意:微信返回的 JSON 里,成功时errcode为 0 或者干脆没有这个字段,失败时才有。所以判断逻辑是「能解析出 errcode 且不为 0 才算失败」。另外对 42001(token 过期)做了特殊处理,清空缓存让下次调用重新获取,这是一种轻量级的自愈机制。2.6 libcurl 封装的关键参数http_client.c 里对 libcurl 的封装,有几个参数必须设置对:static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) { /* 标准写法:把响应体追加到用户缓冲区 */ strncat((char *)userdata, (char *)ptr, size * nmemb); return size * nmemb; } int http_post_json(weixin_ctx_t *ctx, const char *url, const char *body, char *resp, size_t resp_len) { CURL *curl = curl_easy_init(); if (!curl) return -1; struct curl_slist *headers = NULL; headers = curl_slist_append(headers, "Content-Type: application/json"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, ctx->timeout_ms); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); /* 生产环境必须开启 */ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); CURLcode ret = curl_easy_perform(curl); curl_slist_free_all(headers); curl_easy_cleanup(curl); return (ret == CURLE_OK) ? 0 : -1; }CURLOPT_SSL_VERIFYPEER和CURLOPT_SSL_VERIFYHOST这两个参数是血泪教训。开发阶段有人图省事设成 0 跳过证书校验,上线后一旦遇到中间人攻击,appsecret 和用户 openid 全暴露。生产环境必须开启,如果遇到证书链问题,正确做法是更新系统的 CA 证书包,而不是关校验。3. 跑起来:从编译到发出第一条模板消息3.1 依赖准备在动手编译之前,先把依赖装齐。以常见的 Linux 发行版为例:# Debian/Ubuntu 系 sudo apt-get install libcurl4-openssl-dev libjson-c-dev cmake gcc # RHEL/CentOS 系 sudo yum install libcurl-devel json-c-devel cmake gcclibcurl 负责 HTTPS 传输,json-c 负责 JSON 解析。如果你用的是别的 JSON 库(比如 cJSON),把 json_util.c 里的几个函数换掉就行,接口保持json_get_string和json_get_int两个签名不变。3.2 编译与链接项目用 CMake 管理构建,标准流程:mkdir build && cd build cmake .. make生成的产物包括静态库libweixin_message.a和示例程序send_template。链接时注意顺序,libcurl 依赖一堆系统库:gcc -o myapp myapp.c -L./build -lweixin_message -lcurl -ljson-c -lpthread如果链接时报undefined reference to SSL_CTX_new之类的错误,说明 libcurl 是动态链接 OpenSSL 的,需要补上-lssl -lcrypto。这个坑在静态编译时特别常见。3.3 最小可运行示例examples/send_template.c 里是一个完整的调用示例:#include <stdio.h> #include "weixin_message.h" int main(void) { weixin_ctx_t ctx = {0}; strncpy(ctx.appid, "你的appid", sizeof(ctx.appid) - 1); strncpy(ctx.secret, "你的secret", sizeof(ctx.secret) - 1); ctx.timeout_ms = 5000; weixin_template_msg_t msg = {0}; strncpy(msg.touser, "用户openid", sizeof(msg.touser) - 1); strncpy(msg.template_id, "模板ID", sizeof(msg.template_id) - 1); strncpy(msg.url, "https://example.com/order/123", sizeof(msg.url) - 1); /* 模板数据:first、keyword1、keyword2、remark 是常见字段 */ snprintf(msg.data_json, sizeof(msg.data_json), "{\"first\":{\"value\":\"您的订单已发货\"}," "\"keyword1\":{\"value\":\"订单号12345\"}," "\"keyword2\":{\"value\":\"顺丰速运\"}," "\"remark\":{\"value\":\"点击查看详情\"}}"); if (weixin_send_template(&ctx, &msg) == 0) { printf("发送成功\n"); } else { printf("发送失败,请检查日志\n"); } return 0; }编译运行后,如果配置正确,目标用户就能在微信里收到一条模板消息。data_json里的字段名必须和你在公众号后台选的模板字段一一对应,写错了微信会返回errcode: 47003,提示参数不匹配。3.4 参数配置速查参数位置说明常见值appidweixin_ctx_t公众号后台「基本配置」里获取wx 开头的 18 位字符串secretweixin_ctx_t同上,注意保密32 位字符串timeout_msweixin_ctx_tHTTP 超时,太短容易误判失败3000~10000touserweixin_template_msg_t接收者的 openid,不是微信号28 位左右字符串template_idweixin_template_msg_t模板消息 ID后台模板列表里复制data_jsonweixin_template_msg_t模板数据,字段名要对上JSON 字符串4. 避坑排查:推送失败时先看这几处4.1 返回 40001 或 42001:token 相关错误现象是发送接口返回{"errcode":40001,"errmsg":"invalid credential"}或 42001。原因通常是 token 缓存逻辑有问题,或者多个进程共用同一个 appid 导致 token 互相覆盖。微信的 access_token 是全局唯一的,A 进程刷新后 B 进程手里的旧 token 就失效了。解决办法是把 token 缓存放到 Redis 或数据库里做集中管理,而不是每个进程各自缓存。如果项目规模小,至少保证只有一个进程负责刷新。4.2 返回 40013:appid 无效现象是获取 token 时直接返回invalid appid。原因一般是 appid 填错了,或者把订阅号的 appid 用在了服务号接口上。检查公众号后台「基本配置」页面,确认 appid 和 secret 是同一套。另外注意不要有多余空格,从网页复制时经常带不可见字符。4.3 HTTPS 请求超时或证书错误现象是 curl 返回CURLE_SSL_CACERT或直接超时。原因是系统的 CA 证书包太旧,不认微信服务器的新证书链。解决办法是更新 ca-certificates 包,或者用CURLOPT_CAINFO指定一个较新的 cacert.pem 文件。千万不要用CURLOPT_SSL_VERIFYPEER=0来绕过,这是给自己埋雷。4.4 返回 47003:模板参数不匹配现象是发送时返回argument invalid。原因是 data_json 里的字段名和模板定义不一致。比如模板里定义的是keyword1,你写成了keyword_1,或者模板要求first和remark必填但你漏了。解决办法是去公众号后台「模板消息」页面,点开对应模板,逐字段核对名称和顺序。4.5 内存泄漏与并发问题现象是服务跑几天后内存持续上涨,或者多线程同时发送时偶发崩溃。原因是 libcurl 的curl_global_init没有调用,或者多个线程共用一个 CURL 句柄。正确做法是在程序启动时调用一次curl_global_init(CURL_GLOBAL_ALL),每个线程用自己的 CURL 句柄,用完curl_easy_cleanup释放。如果用了连接池,注意curl_easy_reset之后要重新设置所有选项。5. 二次开发:换 JSON 库、加异步、做重试拿到源码后,最常见的改造方向有三个。第一个是换 JSON 库。项目默认用 json-c,但很多嵌入式环境更习惯 cJSON,因为 cJSON 只有两个文件,移植方便。改造时只需要重写 json_util.c 里的json_get_string和json_get_int,保持函数签名不变,上层代码一行不用动。我一般会先写一个测试用例,把微信返回的几种典型响应(成功、token 错误、参数错误)都跑一遍,确认解析结果一致再替换。第二个改造方向是加异步发送。同步调用在推送量大时会把业务线程堵死,尤其是微信接口偶尔抽风响应慢的时候。做法是把weixin_send_template的调用丢到一个独立的工作线程或者线程池里,主线程只管往队列里塞任务。队列可以用简单的环形缓冲区实现,每个任务保存weixin_ctx_t的副本和weixin_template_msg_t的副本,避免多线程访问同一块内存。注意weixin_ctx_t里的 token 缓存要做成线程安全的,加个互斥锁保护读写。第三个方向是加重试机制。微信接口不是 100% 可用的,偶尔会返回 5xx 或者超时。我的习惯是封装一层weixin_send_template_with_retry,最多重试 3 次,每次间隔翻倍(1 秒、2 秒、4 秒)。但要注意,42001 这种 token 过期错误重试前必须先刷新 token,而 40013 这种配置错误重试多少次都没用,应该直接返回失败并打日志。下面是一个重试逻辑的骨架:int weixin_send_with_retry(weixin_ctx_t *ctx, const weixin_template_msg_t *msg) { int delays[] = {1000, 2000, 4000}; /* 毫秒 */ for (int i = 0; i < 3; i++) { if (weixin_send_template(ctx, msg) == 0) { return 0; } /* 如果是 token 过期,清缓存后立即重试,不等待 */ if (ctx->access_token[0] == '\0') { continue; } usleep(delays[i] * 1000); } return -1; }验证改造是否成功,我一般会写一个压力测试:起 10 个线程,每个线程发 100 条消息,观察内存曲线和成功率。如果成功率低于 99%,就要看日志里失败的原因分布,是网络超时多还是 token 错误多,针对性优化。从那以后我每次对接新的 HTTP 接口,都强制走一遍「先跑通单次调用 → 再加缓存 → 再加异步 → 最后压测」的流程,绝不跳过任何一步。希望帮到你。本文还有配套的精品资源,点击获取