中継層はヘッダーだけを読む
一言でいうと
EAIのような中間層は、電文の共通ヘッダーだけを見て経路を決めます。そのためヘッダーは、業務と無関係な少数のフィールド(長さ・取引コード・グローバルID・送受信機関・リクエスト/レスポンス区分・応答コード)に固定されます。このフィールドを1バイトでも違う位置で切るシステムが1つでもあると、その先のすべての区間が間違います。
なぜ必要なのか
銀行1行には、チャネル(インターネットバンキング・モバイル・窓口)、勘定系(元帳)、情報系、カード、外部システムがそれぞれ数十個あります。システム同士を直接つなぐと線はn×(n-1)/2本に増え、あるシステムの形式が変わるたびに、そのシステムとつながるすべての相手を直さなければなりません。中間層(EAI、チャネル側は一般にMCI、外部側はFEPと呼びます)は、この線をハブ1つにまとめます。各システムはハブとだけ取り決めを結び、ハブが、受け取った電文をどこへ送るか、どの形式に変えるか、失敗したら何を返すかを代わりに判断します。
ところがハブが判断するには、業務の中身を知らなくても読める場所が必要です。口座振込の電文と保険請求の電文は、業務部(本文)の形がまったく違います。取引ごとの本文レイアウトをハブが知らなければ経路を決められないとしたら、新しい取引が1つ増えるたびにハブを直すことになります。そこで、すべての電文の先頭に同じ形の共通ヘッダーを付け、ハブはヘッダーだけを読みます。本文は宛先が解釈します。この分離が、このコース全体の出発点です。
どう動くのか
このコースは、架空のLabHub銀行標準(LH-STD)を使います。ヘッダーは80バイトで、すべてASCIIです。実在の機関の規格は公開されていないことが多く、フィールドの順序や長さもまちまちですが、含まれているフィールドの役割はほぼ同じです。
| フィールド | 長さ | 役割 |
|---|---|---|
| 電文長(MSG_LEN) | 4 | このフィールドを除いた残りのバイト数。TCPで電文の境界を見つける唯一の根拠 |
| 取引コード(TX_CODE) | 8 | どの取引か。ルーティング(モジュール2)の鍵 |
| グローバルID(GUID) | 32 | この取引1件を最後まで追跡するための番号。追跡(モジュール7)と重複防止(モジュール8)の鍵 |
| 送信・受信機関 | 3+3 | 誰から誰へ。外部機関なら外部システムへ送る |
| リクエスト/レスポンス区分 | 1 | Qはリクエスト、Rはレスポンス |
| 応答コード | 4 | リクエストは空白、レスポンスは0000またはエラーコード |
| 送信日時 | 14 | この区間で送った時刻 |
長さはバイトです。本文はEUC-KRです。EUC-KRはKS X 1001文字集合を収めるエンコーディングで、ハングル1文字が2バイトになり、同じ文字がUTF-8では3バイトになります。誰かが電文ファイルをエディターで開いてUTF-8で保存すると、文字は同じに見えるのにバイト数が増え、電文長フィールドは古い値のまま残ります。受信側は長さ分だけ読み、残りを次の電文の先頭と取り違えます。またKS X 1001には完成形ハングル2,350字しか入っていないので、名前にこの2,350字に含まれない音節の字が入った顧客には、2バイトの完成形コードがありません。このとき実装ごとに動作が分かれます。ラボイメージのglibcのiconvは変換を拒否し、Pythonのeuc_krコーデックはエラーなしで8バイトの結合シーケンスに変換します(この作成過程で実測した結果です)。黙って8バイトになると、固定長の位置計算がまるごとずれます。こうした顧客をどう扱うかは、エンコーディングを選ぶ時点で決めておくべき業務ルールです(モジュール3で実際に扱います)。
TCPは電文を知りません。TCPは、バイトを順序どおり欠けなく届けるストリームです(RFC 9293)。送信側がsendを2回呼んでも、受信側のrecvが2回に分かれて返るとは限りません。1回のrecvに電文の半分が来ることも、2つ半が来ることもあります。そのため受信側は常に、まず長さの4バイトをちょうど読み、次にその長さ分だけちょうど読みます。電文長フィールドがヘッダーの先頭にある理由です。このルールを破ったコードは、開発環境(同じマシン、短い電文)ではほぼ常に動いていて、本番の長い電文と遅い回線でだけ壊れます。
GUIDは1回だけ作り、最後まで載せて運びます。取引を最初に作ったシステム(チャネル)が発行し、ハブ・勘定系・外部システムは受け取った値をそのまま渡します。区間ごとに作り直すと、1つの取引が4つの番号に散らばり、障害のときにつなぐ方法がなくなります。LH-STDはGUIDを、W3C Trace Contextのtrace-idと同じ形(小文字16進数32文字、すべて0は無効)に定めました。そうすれば、HTTPで渡す区間ではtraceparentヘッダーにそのまま載せられます。発行には予測できない乱数(secrets、uuid4)を使います。時刻と連番で作ると、サーバー2台が同じミリ秒に同じ値を出します。
レスポンスはリクエストを裏返して作ります。取引コードとGUIDはそのまま、送信機関と受信機関は入れ替え、区分はR、応答コードを埋め、送信日時は現在に、長さは再計算します。長さを手で入れるコードは、いつか必ず間違います。作るときに計算します。
現場での姿
最もよくある事故は、長さの基準の勘違いです。長さフィールド自身を含める機関もあれば、含めない機関もあります。インターフェース仕様書に1行で書かれているこの違いのせいで、外部機関1つとの連携が開通日にまるごと止まります(モジュール6で機関ごとの違いを実際に扱います)。2つ目は、形式エラーを黙って通してしまうパーサーです。リクエスト/レスポンス区分がXの電文を「リクエスト」と推測して処理すると、その電文がどこでなぜ壊れたのか誰にもわからないまま、元帳が書き換わります。形式が誤った電文は拒否し、拒否した理由をコード(E102)で残します。3つ目は、GUIDを区間ごとに新しく採番するシステムです。障害対応の会議で「その取引番号がこちらでは見えない」という言葉が出たら、たいていこれです。
次のラボですること
インターフェース仕様書(SPEC.md)を読んでヘッダーレイアウトをオフセットまで書き出し、受信箱の電文8件をバイトで照合します。そのあとパーサー(hdr.py)・レスポンス生成器(reply.py)・GUID発行器(guid.py)・ストリーム分割器(split.py)を順に作り、受信箱全体の判定レポートを出します。モジュール2からは、この処理を担う社内共通ライブラリ(lhstd.py)を使います。ヘッダーは1セットだけでなければなりません。