3. 字节序与编码基础:大端序与小端序、ASCII与BCD编码、二进制与十六进制表示法
各位同学,欢迎来到第三讲。今天我们要聊的话题,是金融消息协议解码中最基础、也最容易踩坑的部分——字节序和编码。
说实话,我见过太多新手甚至老手,在这个环节翻车。明明报文解析对了,结果金额算出来差了好几个数量级。为什么?字节序搞反了。所以这一章,咱们把底子打扎实。
3.1 大端序与小端序:谁先谁后的问题
先问个问题:一个16位的整数 0x1234,在内存里是怎么存的?
答案取决于CPU的「脾气」。有的CPU喜欢把高位字节放在低地址,这叫大端序(Big-Endian)。有的CPU反过来,把低位字节放在低地址,这叫小端序(Little-Endian)。
举个例子:
数值:0x1234
大端序内存: [0x12] [0x34] (低地址存高位)
小端序内存: [0x34] [0x12] (低地址存低位)
我个人习惯把大端序想象成「正常书写顺序」——从左到右,高位在前。小端序嘛,就是反着来。
金融协议中的铁律:绝大多数金融消息协议(比如FIX、ISO 8583、SWIFT)都使用大端序(网络字节序)。这是TCP/IP协议栈定的规矩,金融行业沿用了这个传统。
我在项目中遇到过一件事:某次对接一家海外清算系统,对方文档里写的是「Big-Endian」,结果实际报文发过来是小端序。排查了整整两天,最后发现是对方开发人员把文档写错了。嗯,从那以后,我拿到任何新协议,第一件事就是写个测试用例验证字节序。
3.1.1 如何判断当前系统字节序?
写Python的同学有福了,标准库直接支持:
import sys
print(sys.byteorder) # 输出 'little' 或 'big'
但更常用的场景是:你收到一串字节流,需要按大端序或小端序解析。用 struct 模块:
import struct
# 大端序解析(网络字节序)
data = b'\x12\x34'
value = struct.unpack('!H', data)[0] # '!' 表示网络字节序(大端)
print(hex(value)) # 0x1234
# 小端序解析
value_le = struct.unpack('<H', data)[0] # '<' 表示小端
print(hex(value_le)) # 0x3412
避坑指南:我曾经在解析一个多字节长度字段时,忘了指定字节序,直接用 int.from_bytes() 默认参数。结果在Windows(小端)上跑得好好的,部署到Linux服务器(也是小端)也没问题,但换到一台嵌入式设备(大端)上就全崩了。记住:永远显式指定字节序。
3.2 ASCII与BCD编码:数字的两种面孔
金融报文里,数字的表示方式主要有两种:ASCII和BCD。说白了,一个是用字符表示数字,一个是用二进制表示数字。
3.2.1 ASCII编码
ASCII编码就是把每个数字字符转成对应的ASCII码。比如数字 123,在ASCII里就是三个字节:0x31 0x32 0x33。
优点:人类可读,调试方便。
缺点:浪费空间。一个数字占一个字节,实际上4位就够了。
举个例子,ISO 8583报文中的金额字段,很多就是用ASCII表示的:
# 金额 123.45 美元,通常表示为 "000000012345"
# 去掉小数点,左补零到12位
amount_ascii = b'000000012345'
print(amount_ascii) # b'000000012345'
3.2.2 BCD编码
BCD(Binary-Coded Decimal)就聪明多了。它用4位二进制表示一个十进制数字。所以一个字节可以存两个数字。
比如数字 12,BCD编码就是 0x12(高4位是1,低4位是2)。
数字 123 呢?需要两个字节:0x12 0x3F?不对,标准BCD是半字节对齐的。实际编码是 0x01 0x23(前面补0)。
金融协议中的BCD:很多老牌金融系统(尤其是IBM大型机体系)偏爱BCD。SWIFT MT报文中大量使用BCD编码的金额和日期。我当年第一次解析SWIFT报文时,看到一堆 0x12 0x34 还以为是加密数据,后来才反应过来是BCD。
Python实现BCD编码/解码:
def bcd_to_int(bcd_bytes):
"""BCD字节转整数"""
result = 0
for byte in bcd_bytes:
high = (byte >> 4) & 0x0F
low = byte & 0x0F
result = result * 100 + (high * 10 + low)
return result
def int_to_bcd(value, num_bytes=2):
"""整数转BCD字节"""
result = bytearray()
for i in range(num_bytes - 1, -1, -1):
digit_pair = (value // (10 ** (i * 2))) % 100
high = digit_pair // 10
low = digit_pair % 10
result.append((high << 4) | low)
return bytes(result)
# 测试
bcd_data = b'\x12\x34'
print(bcd_to_int(bcd_data)) # 1234
value = 5678
print(int_to_bcd(value, 2).hex()) # 5678
注意:BCD编码有个变种叫「Packed BCD」,最后一个字节的低4位可能用来存符号(0xC表示正,0xD表示负)。比如 0x12 0x3C 表示 +123。这个在金融系统中很常见,千万别忽略符号位。
3.3 二进制与十六进制表示法
搞金融协议解析,你天天要和十六进制打交道。为什么?因为二进制太长了,写起来费劲。十六进制是二进制的「缩写」,一个十六进制位对应4个二进制位。
举个例子:
二进制: 1101 1110 1010 1101
十六进制: D E A D
你看,16个二进制位,用十六进制4个字符就搞定了。而且转换非常直观——我一般直接在脑子里分组换算。
3.3.1 为什么金融协议偏爱十六进制?
原因很简单:位操作。金融报文里大量使用位图(Bitmap)来表示字段是否存在。比如ISO 8583的主位图,64位,用8个字节表示。每个位代表一个字段。
你想想看,如果用十进制看这8个字节,完全看不出哪个位是1。但用十六进制,一眼就能看出哪些字段被激活了。
# 假设收到一个8字节的位图
bitmap = b'\x80\x00\x00\x00\x00\x00\x00\x01'
# 十六进制:80 00 00 00 00 00 00 01
# 二进制: 1000 0000 ... 0000 0001
# 第1位和第64位被置1
我在调试ISO 8583报文时,习惯先把位图转成十六进制字符串,然后对照协议文档逐位检查。这个方法帮我快速定位过无数次字段缺失的问题。
3.3.2 Python中的进制转换
Python提供了非常方便的进制转换函数:
# 十进制转其他进制
value = 255
print(bin(value)) # 0b11111111
print(hex(value)) # 0xff
print(oct(value)) # 0o377
# 其他进制转十进制
print(int('0xff', 16)) # 255
print(int('11111111', 2)) # 255
# 字节转十六进制字符串
data = b'\x12\x34\xAB\xCD'
print(data.hex()) # 1234abcd
print(data.hex(' ')) # 12 34 ab cd
print(data.hex(':', 2)) # 12:34:abcd
我的调试习惯:每次收到原始报文,第一件事就是打印十六进制转储。用 binascii.hexlify() 或者 bytes.hex() 都行。然后对照协议文档,逐字节核对。这个方法虽然原始,但最可靠。
3.4 本章知识体系
说了这么多,咱们用一张图把核心逻辑串起来:
这张图把今天的内容串起来了。你从顶部开始,先搞清楚字节序,再掌握编码方式,最后用十六进制表示法去验证。三步走,基本不会出错。
好了,这一章的内容就到这里。字节序和编码是金融协议解析的「地基」,地基不稳,上层建筑再漂亮也是白搭。下一章我们会进入实战,用Python手写一个简单的报文解析器,到时候这些知识全都会用上。
公众号:蓝海资料掘金营,微信deep3321