搜「企业微信长连接协议」「企业微信ws协议」「企业微信socket协议」的人,大多带着同一个疑问:消息到底怎么到我手里,我能不能拿到一条一直连着的通道。这个问题得分两段回答,因为链路上有两段,而只有其中一段由你决定。这篇把回调、轮询、长连接三种取数形态的真实代价摊开,说明为什么落到实际接入上只剩事件回调一个答案;示意按 wecomapi 的接入方式来讲。
链路上有两段连接,只有一段归你决定
第一段在账号实例和平台之间。它是有状态的,需要保活,断了要恢复。走 wecomapi 这类托管式接入时,这一段在服务方那侧运行,你看不到它的形态,也不需要看到 —— 它的实现方式可以在你完全不知情的情况下调整,任何写进你代码里的假设都会在某个周三下午变成技术债。
第二段在你的服务和网关之间,也是你真正要动手写的那一段。这里先把可选项说死:wecomapi 提供的入站入口只有事件回调 —— 平台把事件推到你的 HTTPS 地址,不提供面向使用方的长连接推送;平时说的「轮询」是你自己按节奏调查询接口,位置在补偿与对账,不是第二条推送通道。ws 和 socket 这两个检索词问的往往是第一段,而第二段能选、能测、能回退的,是你怎么接住回调、拿什么做补偿。
两段之间还有一层常被误解的关系:第一段稳不稳,你只能通过第二段间接观测 —— 事件有没有按预期到达、账号状态能不能查到。这也是为什么账号状态的可见性在选型时值得单独看一眼,它是你对第一段唯一的观测手段,没有它,第一段对你来说就是一个不会说话的黑盒。
这个区分的实际价值是止损。把工程精力花在第一段上,产出的是一堆无法验证也无法维护的假设;花在第二段上,每一个决定都能实测、能回退、能写进设计文档给别人看。
三种方式各自的真实代价
事件回调:平台推给你
时延在最低那一档,投递失败由平台按重试策略重投,你的服务保持无状态,扩缩容和滚动发布不受影响。代价有两条:一是必须有公网可达的 HTTPS 入口,内网环境要额外过一道;二是响应时延变成了你的责任 —— 处理慢会被判超时,超时触发重投,重投进一步压垮你。这条正反馈是回调链路上唯一的自毁路径,靠先快速 ACK 再异步处理来切断。
轮询:你定时去拉
语义最简单:拉到就是有,拉不到就是没有,没有连接状态需要维护,也不需要公网入站。代价是时延等于轮询间隔,而把间隔压小之后,绝大多数请求都是空转 —— 相当于自己给自己造流量,还要处理多实例同时拉取的重复问题。它的优势场景很窄,但在那个场景里无可替代。
长连接:你持有一条通道
先说清楚:这条路在 wecomapi 上没有对应入口,下面算的是「假如自己搭一条」要付的账。时延低,不需要公网入站,听起来两全其美。真实代价是把一段有状态连接拉回了你自己的部署里:滚动发布时连接要迁移,扩缩容时要处理连接漂移,进程重启后要重连并补齐窗口。这些恰好是托管式接入本来帮你省掉的东西,选长连接等于把它们又拿了回来,而且拿回来的是最难测的那部分。
还有一个维度在方案对比里几乎从不出现:排障可见性。回调的每一次投递在你的入口日志里都是一条记录,配合请求标识能和对方对齐;轮询的每一次拉取也是一条记录,甚至更容易对账;长连接则是一条持续存在的通道,没有天然的「一次」可记,你得自己给消息编号、自己记录连接生命周期,否则线上出问题时手上什么都没有。这部分工作量在选型时不会被算进去,但会在第一次线上问题时全额补交,而且补交那天没有历史记录可查,只能从当下开始补记。
所以答案不是三选一:事件回调做主链路,轮询查询接口留在补偿与对账的位置上,不做主链路,长连接不在可选项里。两种常被拿来支持长连接的前提,在这里各有各的去处:公网入站确实开不了的,走私有化部署把服务放进你自己的网络,接口形态与云端一致,业务代码基本不用改;至于「时延要从两秒压到八百毫秒」,在企业微信这类业务里很少成立,人在对话里的反应时间本来就是秒级,没有任何人会感知到这点差别。
丢失语义才是真正的分水岭
三种方式的时延差通常不是决定因素,真正拉开差距的是同一个问题:断的那段时间里,事件去哪了、谁负责补。
- 回调断了:投递失败的那些,平台按重试策略重投,你要做的是幂等。但这只覆盖「投出来了没送到」;账号掉线那段时间根本没产生投递的事件不在其中,别把设计建立在补推承诺上 —— 站内讲掉线与自愈那篇的口径是自己埋水位、恢复后主动回拉增量对账。
- 轮询断了:本来就没有连接概念,恢复后按游标继续拉即可,窗口自然被下一次拉取覆盖。
- 长连接断了:断连期间的事件没有人替你保管。没有断线后按水位线补拉的机制,这段窗口就是黑洞,而且是静默的黑洞 —— 没有任何报错告诉你少了东西。
结论很直接:长连接方案实际上是「长连接 + 补偿拉取」两套链路都得写,工程量比回调大而不是小。很多人选它是觉得它比回调更实时更可控,但把断连窗口的成本算进去之后,它是三种里最贵的一种,而且贵在最难测的地方 —— 断连只在生产环境、在你没盯着的时候发生。
还有一条容易被忽略:回调的丢失是响亮的,你的入口日志里有请求、有非 2xx、有重投记录,排查有抓手;长连接的丢失是安静的,连接对象好好地挂在内存里,只是不再有数据流过。安静的故障比响亮的故障贵一个数量级,因为发现它靠的不是告警,是某天有人问「客户上周发的那条消息呢」。
断连窗口的测试方法其实不难,难在没人主动做:在预发环境把连接强行掐断三十秒,期间让对端产生若干事件,恢复后核对条数。这条用例应该进回归,因为补偿逻辑属于那种写完就再没人碰、却会被每一次基建变更悄悄弄坏的代码。没有这条用例的长连接方案,等于把断连窗口的正确性寄托在运气上。
不管几个入口,都收敛到一个 ingest
现实里主链路和补偿链路通常并存,那就必须在解析之前把它们收成同一个函数。两个入口各写一套处理,是这类系统最稳定的一种 bug 来源:只要两边的幂等键算法有一点点不同,重复消息就会从缝里漏过去,而这种漏发生在补偿链路启动的那一刻,恰好是你最忙的时候。
// 示意结构。事件结构与幂等标识的取法以 wecomapi 文档为准
function ingest(raw, source) { // source: "callback" | "backfill"
const key = idempotencyKey(raw); // 同一套算法,两个入口共用
return queue.push({ key, source, raw, receivedAt: Date.now() });
}
app.post("/wecom/events", (req, res) => {
if (!verify(req)) return res.sendStatus(401);
res.sendStatus(200); // 先 ACK,再入队
ingest(req.body, "callback");
});
// 断连或异常之后,按水位线把窗口补齐,结果走同一个 ingest
for (const raw of await pullSince(watermark)) ingest(raw, "backfill");用 wecomapi 的事件回调做主链路时,handler 里只做三件事:验签、返回 2xx、把原始报文入队;补偿拉取的结果丢进同一个队列,由同一个消费者按同一套幂等消费。来源标记只用于观测,不参与业务判断 —— 一旦业务逻辑开始区分「这条是推来的还是拉来的」,两套行为就又分叉了,而分叉出来的那半边永远测得更少。
入口只负责收下来,顺序和并发由消费者决定。这个分工听起来是洁癖,实际收益很具体:入口天然知道自己是谁,消费者不知道,而「不知道」正是它能对两个来源一视同仁的原因。任何把来源信息往下传的设计,都是在给未来的自己埋一个条件分支。
三条判据,答完就有答案
- 1你的服务能不能开放公网入站的 HTTPS 端口?能,回调,这一条基本就定了。
- 2业务需要的时延是秒级还是百毫秒级?秒级用回调绰绰有余,人在对话里感知不到这点差别。
- 3团队愿不愿意为一段有状态连接改变部署方式?不愿意,就别碰长连接,它的代价全在这里。
三条答完,绝大多数团队的答案是「回调为主,轮询做补偿」。这个组合不新鲜,但它是唯一一个在扩缩容、发布、排障三件事上都不额外收费的组合,而这三件事会伴随系统的整个生命周期,比首次接入时省下的那点工作量重要得多。
什么时候该重新评估?两个信号:出口环境的网络策略变了,公网入站从可选变成不可能;或者业务形态从人对人变成了机器对机器,时延要求真的降到百毫秒级。除此之外,为了「更实时」这个理由去重构取数方式,收益通常小于它引入的新故障面。真要提速,先量一遍端到端时延的构成 —— 大多数团队量完会发现,耗时的大头在自己的消费者和下游依赖上,换通道解决不了。
本文讲的是取数方式的工程权衡,不涉及任何一侧的内部实现。具体提供哪些接入入口、事件类型与重试策略以 wecomapi 线上文档为准。
常见问题
- 企业微信有面向开发者的 WebSocket 接口吗?
- 企业微信官方开放平台以 HTTP 接口加事件回调为主,没有公开面向业务开发者的长连接推送规范。检索里的「ws协议」「socket协议」多是在说账号接入那一段的实现形态,不是使用方可以直接调用的东西。具体接入方式提供哪些入口,以对应的线上文档为准。
- 轮询可以当主链路吗?
- 能跑,但不划算:间隔大了时延不可接受,间隔小了绝大多数请求是空转,还要额外处理多实例重复拉取。它更合适的位置是补偿与对账 —— 主链路中断一段时间之后,用轮询按水位线把窗口补齐,再回到正常链路。
- 长连接是不是比回调更不容易丢消息?
- 两边都要你自己兜底,差别在兜底的面积。回调这边,投递失败的部分平台会按重试策略重投,你做好幂等即可;长连接那边连投递重试都没有,断连期间的事件得整段自己补拉。有一点两者一样:账号掉线那段时间压根没产生投递的事件,谁也不会替你补,只能靠自己埋水位、恢复后主动回拉增量对账,站内讲掉线与自愈那篇有完整口径。用 wecomapi 的事件回调时,先快速 ACK 再异步处理、按事件标识幂等这两条仍然要做。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
