构建同步中继服务器 — 不知道就说不知道
目标
制作一个同步中继服务器:接收 LH-STD 报文,调用账户系统(HTTP JSON),并把结果转换成标准响应码返回。区分读取超时(结果未知)和连接失败(确定未发送)。
为什么重要
渠道不必了解目标系统的约定,而是要凭一个标准响应码来判断。这种翻译由中继层承担。而且中继会多出一个失败点,所以“能不能重发”取决于停在了哪里。如果弄错这个区分,就会发生重复转账。
步骤
- 启动账户系统夹具:
nohup python3 /opt/lab/fixtures/eaimw/partner.py core --log /root/eaimw/sync/core.log > /root/eaimw/sync/core.out 2>&1 &(端口 9201)。用 curl 发送一笔正常转账(wdBankLHB、wdAcct11002003004005、amount整数等——参见partner.py开头的 API 说明),并把响应正文原样保存到/root/eaimw/sync/core-ok.json。 - 根据定义书
/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。 /root/eaimw/sync/relay.py骨架:用python3 relay.py --port <P> --core <계정계URL> --timeout <초>(占位符依次为账户系统 URL、秒数)通过 TCP 接收报文。准确读取 4 字节长度,再按该长度读取(即使是分片到达的)。格式错误以 E102 答复,交易码不是 BKTR0001 则以 E101 答复(相同的 GUID、标志 R、互换机构)。BKTR0001 还没有接到账户系统上,所以以 E902 答复。- 把 BKTR0001 中继到账户系统。用
lhconv把正文转成 JSON,加入guid,执行POST /v1/transfers,GUID 也放在X-GUID请求头里。如果是 200,就返回 0000 和响应正文(BKTR0001.rsp.layout,45 字节)。账户系统恰好只调用一次。 - 转换业务错误:按 422 的 result 转为 B201、B202、B203,500 转为 E500。错误响应没有正文。
- 等待响应超过
--timeout时,以 E901 答复。不要一直等待账户系统。 - 如果无法连接账户系统(连接被拒绝、连接超时),以 E902 答复。要与第 6 步的 E901 区分开。
- 每个连接单独处理。在处理一个缓慢的请求期间,其他请求不应该等待。
参考
- 公共库:
import sys; sys.path.insert(0, "/opt/lab/fixtures/eaimw/lib"); import lhstd, lhconv——lhstd.read_frame(sock)·parse·reply(req, code, body),lhconv.load_layout·load_codemap·fixed_to_json·json_to_fixed。 - 故障重现开关:如果正文摘要以
SLOW开头,账户系统会在--delay秒之后处理(会处理),以FAIL开头则返回 500。 - 账户系统统计:
curl -s localhost:9201/_stats(调用数、按 GUID 的次数),初始化curl -s -XPOST localhost:9201/_reset。 - 异常区分:连接阶段的问题是
urllib.error.URLError,等待响应的超时是TimeoutError,HTTP 错误状态是urllib.error.HTTPError(它是 URLError 的子类,所以要先捕获)。 - 常见错误:用一个
except Exception把全部转成同一个代码。E901 和 E902 就被混为一谈了。
直接调用对方系统
在 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 一次只处理一个连接。标准库中有为每个连接启动一个线程的服务器类。