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

构建 EAI 中间层

构建同步中继服务器 — 不知道就说不知道

在 TT Lab 中继续学习

目标

制作一个同步中继服务器:接收 LH-STD 报文,调用账户系统(HTTP JSON),并把结果转换成标准响应码返回。区分读取超时(结果未知)和连接失败(确定未发送)。

为什么重要

渠道不必了解目标系统的约定,而是要凭一个标准响应码来判断。这种翻译由中继层承担。而且中继会多出一个失败点,所以“能不能重发”取决于停在了哪里。如果弄错这个区分,就会发生重复转账。

步骤

  1. 启动账户系统夹具:nohup python3 /opt/lab/fixtures/eaimw/partner.py core --log /root/eaimw/sync/core.log > /root/eaimw/sync/core.out 2>&1 &(端口 9201)。用 curl 发送一笔正常转账(wdBank LHB、wdAcct 11002003004005、amount 整数等——参见 partner.py 开头的 API 说明),并把响应正文原样保存到 /root/eaimw/sync/core-ok.json。
  2. 根据定义书 /opt/lab/fixtures/eaimw/header/SPEC.md 第 4 节和账户系统 API,创建 /root/eaimw/sync/rspmap.csv。表头为 source,rsp_code,source 共九个:HTTP200·INSUFFICIENT_FUNDS·NO_ACCOUNT·LIMIT_EXCEEDED·HTTP500·READ_TIMEOUT·CONNECT_FAIL·UNKNOWN_TX·BAD_FRAME。
  3. /root/eaimw/sync/relay.py 骨架:用 python3 relay.py --port <P> --core <계정계URL> --timeout <초>(占位符依次为账户系统 URL、秒数)通过 TCP 接收报文。准确读取 4 字节长度,再按该长度读取(即使是分片到达的)。格式错误以 E102 答复,交易码不是 BKTR0001 则以 E101 答复(相同的 GUID、标志 R、互换机构)。BKTR0001 还没有接到账户系统上,所以以 E902 答复。
  4. 把 BKTR0001 中继到账户系统。用 lhconv 把正文转成 JSON,加入 guid,执行 POST /v1/transfers,GUID 也放在 X-GUID 请求头里。如果是 200,就返回 0000 和响应正文(BKTR0001.rsp.layout,45 字节)。账户系统恰好只调用一次。
  5. 转换业务错误:按 422 的 result 转为 B201、B202、B203,500 转为 E500。错误响应没有正文。
  6. 等待响应超过 --timeout 时,以 E901 答复。不要一直等待账户系统。
  7. 如果无法连接账户系统(连接被拒绝、连接超时),以 E902 答复。要与第 6 步的 E901 区分开。
  8. 每个连接单独处理。在处理一个缓慢的请求期间,其他请求不应该等待。

参考

直接调用对方系统

在 9201 上启动账户系统夹具,把一笔正常转账的响应正文保存到 /root/eaimw/sync/core-ok.json。

API 在 partner.py 的开头。像 curl -s -XPOST -H 'Content-Type: application/json' -d '{...}' localhost:9201/v1/transfers 这样调用。guid 是 32 位小写十六进制,amount 是不带引号的整数。

编写响应码转换表

在 /root/eaimw/sync/rspmap.csv 中,把九个 source 转换为标准响应码。

请阅读定义书第 4 节的含义。关键是 READ_TIMEOUT 和 CONNECT_FAIL——一个是“发送了却不知道”,一个是“没能发送”。

骨架——读到底,不认识就拒绝

让 /root/eaimw/sync/relay.py 把被拆碎的报文也按长度全部读完,并以 E102 答复格式错误,以 E101 答复未登记的交易。

lhstd.read_frame(sock) 会读取 4 字节长度,并反复 recv,直到把其余部分读完。格式错误的报文,parse 会失败,所以请直接读取原文 80 字节的位置(4–12 交易码,12–44 GUID)来生成响应。

把正常转账向账户系统中继一次

把 BKTR0001 转成 JSON,调用账户系统一次,并返回 0000 和 45 字节的响应正文(包含 X-GUID 请求头)。

用 lhconv.fixed_to_json(REQ, h['BODY'], CODES) 转换并加入 guid。给 urllib.request.Request 加上 headers={'X-GUID': ...},再用 urlopen(req, timeout=ARGS.timeout)。响应用 lhconv.json_to_fixed(RSP, res) 生成正文,再用 lhstd.reply(h, '0000', body)。

把业务错误转为标准代码

把 422 的 result 转为 B201、B202、B203,把 500 转为 E500。错误响应不带正文。

urllib.error.HTTPError 带有状态码(e.code)和正文(e.read())。它是 URLError 的子类,所以 except 的顺序很重要。

等得不耐烦了——就答复不知道

等待响应超过 --timeout 时,以 E901 答复。不要一直等待账户系统。

urlopen 的 timeout 对连接和等待响应都起作用。等待响应时产生的超时,会以未被包裹的 TimeoutError 抛上来。请记住,账户系统在那之后仍会处理转账。

连发送都没能做到——就确定地说没有发送

无法连接账户系统时,以 E902 答复(与 E901 区分)。

连接阶段的失败(拒绝、连接超时)会被 urllib.error.URLError 包裹后抛上来。先捕获 HTTPError 和 TimeoutError,然后再捕获 URLError。

别让一笔缓慢的请求让别人排队

每个连接单独处理,使在处理一个缓慢的请求期间,其他请求不必等待。

socketserver.TCPServer 一次只处理一个连接。标准库中有为每个连接启动一个线程的服务器类。