TT Lab
开始
学习 学习路径 课程

构建 EAI 中间层

中间层只读报文头

在 TT Lab 中继续学习

一句话总结

像 EAI 这样的中间层,只看报文的公共头来决定路径。所以报文头被固定为几个与业务无关的字段——长度、交易码、全局 ID、收发机构、请求响应标志、响应码——只要有一个系统把这些字段多切或少切一个字节,后面的所有环节就都会出错。

为什么需要它

一家银行里,渠道(网上银行、手机银行、柜台)、账户系统(账本)、信息系统、银行卡、外部系统各有几十个。如果系统之间都直接相连,连线会增加到 n×(n-1)/2 条,而且每当一个系统的格式变化,与它相连的所有一方都得修改。中间层(EAI;渠道一侧通常称为 MCI,对外一侧称为 FEP)把这些连线汇集到一个枢纽上。各个系统只与枢纽约定,由枢纽替它们判断收到的报文该发往哪里、该转换成什么格式、失败时该返回什么。

然而,枢纽要做判断,就必须有一个不了解业务内容也能读取的位置。转账报文和保险理赔报文,业务部分(正文)的样子完全不同。如果枢纽必须了解每种交易的正文布局才能决定路径,那么每出现一种新交易,都得修改枢纽。所以要在所有报文前面加上形态相同的公共头,枢纽只读报文头。正文由目的地自己解释。这种分离,是整个课程的出发点。

工作原理

本课程使用虚构的 LabHub 银行标准(LH-STD)。报文头为 80 字节,全部是 ASCII。实际机构的规格大多不对外公开,字段顺序和长度也各不相同,但其中所含字段的作用几乎一样。

字段 长度 作用
报文长度(MSG_LEN) 4 除本字段之外的其余字节数。是在 TCP 上找到报文边界的唯一依据
交易码(TX_CODE) 8 是什么交易。路由(第 2 模块)的键
全局 ID(GUID) 32 一路跟踪这一笔交易到底的编号。是跟踪(第 7 模块)和防重复(第 8 模块)的键
发送、接收机构 3+3 谁发给谁。如果是外部机构,就发往外部系统
请求响应标志 1 Q 请求 / R 响应
响应码 4 请求为空格,响应为 0000 或错误码
发送时间 14 在这一环节发出的时刻

长度是字节数。正文是 EUC-KR。EUC-KR 是承载 KS X 1001 字符集的编码,所以一个韩文字是 2 字节,而同一个字在 UTF-8 中是 3 字节。如果有人用编辑器打开报文文件再以 UTF-8 保存,字看上去一模一样,字节数却增加了,而报文长度字段仍保留着旧值。接收的一方按长度读取,并把剩下的部分误认为下一个报文的开头。另外,KS X 1001 里只收录了 2,350 个预组合形韩文字,所以名字中含有像“ddom”这样以双辅音开头的音节的客户,没有 2 字节的预组合形编码。这时各个实现的行为不同——实验镜像中的 glibc iconv 会拒绝转换,而 Python 的 euc_kr 编解码器不会报错,而是把它换成 8 字节的组合序列(在这个过程中实测)。悄悄变成 8 字节,固定长度栏位的计算就会整个错位。如何处理这样的客户,是选定编码的那一刻就必须定好的业务规则(在第 3 模块中亲自处理)。

TCP 不认识报文。TCP 是按顺序、不遗漏地传送字节的流(RFC 9293)。发送方调用了两次 send,并不意味着接收方的 recv 也会分成两次到来。一次 recv 可能来半个报文,也可能来两个半报文。所以接收的一方始终要先准确地读取 4 字节的长度,再按这个长度准确地读取。这就是报文长度字段位于报文头最前面的原因。违反这条规则的代码,在开发环境(同一台机器、较短的报文)中几乎总是能正常工作,只在生产环境中较长的报文和较慢的网络上才会出问题。

GUID 创建一次,并一路携带到底。由最初创建交易的系统(渠道)签发,枢纽、账户系统、外部系统都把收到的值原样传下去。如果每个环节都重新生成,一笔交易就会散落成四个编号,发生故障时无从串联。LH-STD 把 GUID 定为与 W3C Trace Context 的 trace-id 相同的形态——32 位小写十六进制,全 0 无效。这样,在转为 HTTP 的环节,就可以原样放进 traceparent 头。签发要用不可预测的随机数(secrets、uuid4)。如果用时刻加序号来生成,两台服务器会在同一毫秒产生相同的值。

响应是把请求翻转过来生成的。交易码和 GUID 保持不变,发送、接收机构互换,标志设为 R,填入响应码,发送时间改为现在,长度重新计算。手工填写长度的代码,总有一天一定会出错——要在生成时计算。

在现场相遇的样子

最常见的事故是长度基准的误解。有的机构的长度字段包含自身,有的不包含。这个在定义书里只占一行的差异,会让与某一个外部机构的对接在开通当天整个停摆(在第 6 模块中亲自处理各机构的差异)。第二种是悄悄放过格式错误的解析器。如果把请求响应标志为 X 的报文猜测为“请求”来处理,那么这份报文是在哪里、为什么坏掉的,没有人知道,账本就已经变了。格式错误的报文要拒绝,并用代码留下拒绝的原因(E102)。第三种是每个环节都重新生成 GUID 的系统。故障会议上,如果有人说“那个交易编号在我们这边看不到”,大多就是这种情况。

下一项实验要做什么

阅读定义书(SPEC.md),把报文头布局写到偏移量,然后按字节核对收件箱中的八个报文。接着依次制作解析器(hdr.py)、响应生成器(reply.py)、GUID 签发器(guid.py)和流拆分器(split.py),并输出对整个收件箱的判定报告。从第 2 模块开始,使用做这件事的公司内部公共库(lhstd.py)——报文头只应该有一套。