「底层协议」「通讯协议」「客户端协议」这三个词经常被当作同义词混着用,但它们指的是不同的层。混着用的后果不是沟通不畅,而是技术决定做错了层 —— 最典型的一种,是把一个随时可能变化的实现细节写进了自己的业务假设里。这篇把链路分成四层,说清每层由谁负责、你在哪一层工作,以及跨层假设为什么是这类系统里最贵的错误;示意按 wecomapi 的接入方式来讲。
四层分工:这些词各自落在哪
- 传输层:TCP、TLS、HTTP 这些通用网络协议。它对所有 SaaS 对接都一样,没有企业微信特色,也不需要你做任何决定。
- 运行时层:一个账号怎么建立并维持在线状态。「底层协议」「客户端协议」这两个说法多半指这一层。它有状态、会变、由承担运行时的一方负责。
- 契约层:你能读到的那份接口与事件约定 —— 请求怎么发、事件长什么样、错误怎么表达、状态怎么解释。「通讯协议」「数据协议」通常指这里,它也是你唯一应该依赖的东西。
- 领域层:你自己的模型。客户、会话、任务、工单,这一层的命名应该完全由你决定,不受任何外部结构影响。
判断一个说法落在哪一层,有个比术语定义好用得多的办法:问一句「这件事变了,会不会通知我」。会通知、有版本、有过渡期的,是契约;不会通知、也不需要通知的,是实现。这条判据不需要任何背景知识就能用,而且几乎不会判错。
四层里只有第三层是双方共同承诺的东西,另外三层各自属于一方:传输层属于公共基础设施,运行时层属于承担运行时的那一方,领域层属于你。绝大多数结构性问题都发生在有人越界的时候 —— 要么你依赖了不属于你的层,要么你把属于自己的层交了出去,比如直接拿外部报文的形状当成自己的数据模型。
推论只有一句,但值得贴在代码评审的清单上:一个可以在不通知你的情况下改变的东西,不能出现在你的假设里 —— 无论它现在看起来多稳定,也无论你观察过它多少次。
只依赖契约层,理由是成本不对称
依赖契约层的代价是有限且可预算的:契约变更通常带版本、带通知、带过渡期,改起来是一次有计划的工作,可以排进迭代。依赖实现层的代价是无限的:它可以在任何一个下午变化,而你的系统会在毫无前兆的情况下表现出一类你从没写过分支的行为,且第一时间没人知道原因。
落到代码上,这条界线很好画:契约层的东西可以进解析器、进错误分类、进重试判断;实现层的信号只能出现在运维视角的面板和告警里,不能进业务逻辑。这一条画清楚,后面几节讲的坑基本都不会踩。有个简单的自查方式:把代码里所有 if 条件过一遍,凡是判断依据来自「我观察到它一般会怎样」而不是某份写下来的约定,都是候选债务。
用 wecomapi 这类统一 REST 形态接入时,落在契约层的是请求结构、事件结构、错误分类和实例状态的语义;落在实现层的是账号那侧怎么保持在线、连接如何恢复。前者可以写进代码并用测试锁住,后者订阅告警就够了,不要试图为它建模 —— 为一个你无法验证的东西建模,模型的正确性也无法验证。
跨层假设:三个真实会咬人的例子
把到达顺序当成发生顺序
顺序是传输层的性质,时序是业务层的结论,两者之间没有必然关系。重投、多消费者并行、网络抖动,任意一个都会打乱到达顺序。正确做法是用报文里携带的发生时刻,加上你自己的排序键来定序,而不是默认「先收到的先发生」。这个假设在测试环境里百分之百成立,在生产环境里每天都被打破几次。
把连接在线当成账号可用
在线是运行时层的一个信号,可用是业务层的一个结论,中间存在真实的差集:状态显示正常,但业务动作就是不生效。这类跨层假设的典型形态,是拿一个低层观测去替代高层验证 —— 而低层根本不知道业务想做什么。可用性只能用一次真实的业务动作来定义,而不是看一个布尔值。
把端类型当成能力清单
从「某某端能力更完整」这类说法推导自己能做什么,是把客户端形态当成了契约。这类说法既没有出处也没有承诺,随时可能与现实脱节。能力边界只有两个来源:你能读到的那份文档,加上你在预发环境的实测结果。二手描述在这件事上的可信度接近于零,因为它们连自己什么时候过期都不知道。
三个例子的共同点是方向一致:都用低层的观测去推高层的结论。这个方向天然不成立,因为低层不掌握业务上下文。反过来完全没问题 —— 业务层先定义要什么,再往下找能不能满足,这才是分层该有的用法。
识别跨层假设有个语言上的线索:句子里一旦出现「一般都是」「实际测下来都是」,后面接的多半就是一个跨层假设。观察到的规律不等于承诺,被观察到一百次也仍然只是观察。在设计评审里把这类句子挑出来,逐条问一句「这是文档写的,还是我们看到的」,成本极低,收益是砍掉一整类没人预料到的线上问题。
把层的边界固化进代码
分层写在设计文档里,三个月后就没人记得;写进代码结构里,它才会一直生效。下面四条按性价比排序,第一条几乎零成本,第四条需要一点纪律。
- 1外部结构只在一个地方解析。全项目只有一个 parser 能看见外部报文的原始形状,其余代码看到的都是你自己的类型。这一条同时也是所有兼容性问题的唯一改动点。
- 2契约用测试锁住,不用文档锁住。对每一类依赖的响应与事件,各留一条基于真实样本的用例;对方形态变了,测试先红,而不是线上先红。
- 3实现层信号只进监控,不进条件判断。账号在线状态可以画在面板上、可以触发告警,但不能出现在「这条消息要不要发」的 if 里。
- 4目录结构反映分层,import 方向单向。运行时、契约、领域各一个包,领域层不允许 import 运行时层的任何类型。听起来教条,但它是唯一能在半年后仍然生效的约束。
用 wecomapi 的事件回调收进来的报文也守同一条规矩:不管来自哪个入口,都先经过那唯一一个 parser 转成内部类型,再往下走。绕过 parser 直接读外部结构的代码,会在对方新增一个字段时成为你最难找的那一处 bug —— 它不报错,只是悄悄少处理了一类情况。
# 示意请求,端点与字段仅用于表达调用形态,精确定义以线上文档为准
curl -X POST https://manager.wecomapi.com/message/sendText \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"guid": "7db8...",
"toId": "78813...",
"content": "Hello WeCom"
}'
# 契约层(可依赖):请求结构、返回结构、错误怎么分类
# 实现层(不可依赖):这一跳之下走几段、连接怎么复用、账号那侧靠什么在线这段示意真正要表达的是底下那两行注释。上面的请求形态别照抄,下面那条界线是通用的:值得依赖的只有结构本身,它下面走几跳、连接怎么复用,都不该影响这行代码怎么写。一旦影响了,说明分层漏了 —— 而漏点几乎总是出现在「为了排查某个问题临时加的一个判断」上,加的时候都有充分理由,留下来就成了跨层依赖。
该关心到哪一层为止
使用方需要理解的最低层是契约层,需要感知但不依赖的是运行时层,完全不用管的是传输层。再往下多走一层,收益接近于零,风险却是实打实的 —— 你会为一个自己既无法验证也无法控制的东西做设计,而它变化时你连收到通知的渠道都没有。这条边界同时也是一条职责边界:越过它去猜实现,等于把对方的运维责任无偿背到自己身上,出了事你既解释不清也修不了。
两类变化的准备方式也完全不同。契约变更靠订阅公告、排进迭代,是计划内的工作;运行时层的变化只能当成故障来准备 —— 你没法预知它,只能确保它发生时你能在几分钟内知道,并且有一个不依赖它的回退动作。把这两件事混成一套流程,结果通常是对契约变更反应过度,对运行时层变化毫无准备。
唯一的例外是排障。定位问题时确实需要往下看一层,但那是读,不是写:拿着请求标识、时间窗和账号状态去和对方对齐事实,看完之后回到契约层写代码,别把排障时的临时观察固化成业务假设。会不会做这个切换,是团队在这类系统上成熟度的一个相当准的判断标准。
本文讲的是概念分层与依赖边界。精确的请求结构、事件类型与错误分类以 wecomapi 线上文档为准,示意代码只表达调用形态,不要照抄上生产。
常见问题
- 「企业微信底层协议」有公开规范吗?
- 没有公开发布的底层规范。企业微信官方公开的是开放平台的接口与事件文档,属于契约层。检索里出现的「底层协议」多指账号接入那一段的实现形态,它由承担运行时的一方负责,不属于使用方的开发内容,也不建议为它建模。
- 「通讯协议」和「数据协议」是一回事吗?
- 大体在同一层,切面不同:通讯偏「怎么发、怎么收、错误怎么表达」,数据偏「实体和事件长什么样」。两者都属于你能读到、也应该依赖的契约,具体结构与字段以 wecomapi 文档为准,代码则要按「文档会更新」来写。
- 排障时需要了解到多深?
- 能定位到段就够:请求有没有出你的机器、有没有到对端、对端返回了什么、事件有没有投递、消费有没有积压。用 wecomapi 接入时,实例的运行状态在控制台可查,它回答的是「这个账号现在能不能用」,不是「底下怎么实现的」。再往下是服务方的运行时,你该做的是拿着请求标识和时间窗去对齐事实,而不是自己推断对方的内部实现。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
