ISO8583报文中的编、解码总结
1.pos收单流程
从POS机的角度看,一个典型的收单交易流程为:
1. 根据报文格式进行组包,向后台发起交易请求;
2. 后台收到请求后解包进行验证,验证完成后重新组包返回应答报文;
3. POS终端收到应答报文后解包,得到最终交易结果,整个流程结束。
其中,组包就是编码的过程,解包就是解码的过程。
2.计算机编码基础
计算机可以存储数字、字母、中文、特殊符号、图像等等各种复杂的数据,但不管是何种数据,最终都是通过底层的二进制数字(0和1)来表示,那计算机是怎么通过0和1的一长串组合来表示那么多复杂的数据呢?
2.1整型的存储
首先,我们来看一下最简单的数据:整型,在计算机中如何表示。
计算机通过补码来表示一个整型值,补码是在原码的基础上取反码加1。
- 原码:一个数在计算机中的二进制表示形式,其中最高位存放符号,正数为0,负数为1。比如-3用一个字节(8位)来表示就为10000011
- 反码:反码是在原码的基础上变化而来的,正数的反码和原码相同,负数的反码是在原码的基础上,符号位不变,其余各个位取反。比如-3的反码表示为11111100
- 补码:正数的补码和原码相同,负数的补码是在原码的基础上,符号位不变,其余各个位取反,最后+1。比如-3的补码表示为11111101
//原码转成补码的过程(正数不变,负数取反加1)
[+1] = [00000001]原 => [00000001]反 => [00000001]补
[-1] = [10000001]原 => [11111110]反 => [11111111]补
从以上可以看出,原码更接近人类的理解方式,那为什么不直接用原码而用补码表示数字的存储呢?
通过补码表示方式,可以让符号参与运算,统一数的加减法运算。此外,补码和原码相互转换,其运算过程是相同的,不要额外的硬件电路。
//补码方式展现3-2的运算过程
//第一步,将减法变成假发3-2转成3 + (-2)
-2的补码 [10000010]原 => [11111101]反 => [11111110]补
//第二步, 进行3 + (-2)的运算
[00000011]3
+[11111110]-2
=[00000001]运算结果
//将运算转成数字
[00000001] => +1
2.2字符的存储
字符是指计算机中使用的文字和符号,比如1,2,3,a,b,c,空格,换行,标点符号,中文,日文,图片等。
可以把字符的存储理解成一个“翻译”的过程,因为计算机只能存储一个个的字节数据,不能识别字符,所以我们需要将字符翻译成计算机可以理解的字节数据,这个将字符翻译成字节的过程我们称为编码。
举例来说ASCII码:
这是单字节的ASCII码表,可以表示0~127个字符数据,其中0~31是控制字符如回车、换行、删除等;32~126是打印字符,可以通过键盘输入并且能够显示出来。
现在,通过字节来存储不同字符的方法解决了,但人类有各种不同的语言,如英语、中文、拉丁文等等,需要用不同的编码方式来表示,不同的语言还要在计算机上同时显示,这就需要统一的编码格式规范了。
2.3常见的编码格式
ISO-8859-1
128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,在所有编码中应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。
GB2312
它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。
GBK
全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
GB18030
全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。
UTF-16
说到 UTF 必须要提到 Unicode(Universal Code 统一码),ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。
UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。
UTF-8
UTF-16 统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。
UTF-8 有以下编码规则:
如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 - 7F)。可见,所有 ASCII 编码已经是 UTF-8 了。
如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8 字符的首字节。
如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节。
3. ISO8583报文中的编解码
前面我们介绍常用的几种编码方式,那在ISO8583报文中会使用哪种编码方式呢?为什么要使用这种编码方式?
我的理解是报文是用于终端和后台端到端的交互,是通过网络进行传输的,网络中最重要的当然是带宽,所以报文编码的关注点主要是尽量减小报文所占的带宽,也就是说通过编码来尽量减小报文的字节数。
举个例子:
报文中的域2,存储主账号,表示为N..19(LLVAR),2个字节长度值+最大19个字节的主账号,压缩(也就是编码)时用BCD码(一种编码方式,后面会介绍)表示的1个字节的长度值+用左靠BCD码表示的最大10个字节的主账号。
//主账号(16位)
6225-8879-0261-0332
--编码后-->
//占9个字节
0x16 + 0x62 0x25 - 0x88 0x79 - 0x02 0x61 - 0x03 0x32
因主账号是数字(0~9)组成的,而一个字节可以表示最多256个字符,如果用1个字节来存储明显是浪费空间,这里用BCD编码,一个字节可以存储2位账号,相比节省了一半的空间。
理解了选用编码方式的原则,接下来我们来看一下8583报文中使用到的编码方式以及编解码转换方法。
3.1 BCD编码
BCD码是一种二进制的数字编码形式,用4位二进制来表示1位十进制数中的0~9这10个数码。这种编码形式利用了四个位来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的运行。
BCD编码在8583报文中使用最为广泛,像银联标准的域2,3,4,11,12,13,14等,都是使用这种编码方式。
/**
* 将str字符串转换成bcd编码的字节数组,比如字符串"123456"转成byte[] {0x12, 0x34, 0x56}
* @param asc
* @return
*/
private static byte[] str2Bcd(String asc) {
int len = asc.length();
len /= 2;
byte[] bbt = new byte[len];
byte[] abt = asc.getBytes(Charset.forName("utf-8"));
for (int p = 0; p < len; ++p) {
int j; //字节高4位
int k; //字节低4位
if ((abt[(2 * p)] >= 97) && (abt[(2 * p)] <= 122)) {
//字符a-z
j = abt[(2 * p)] - 97 + 10;
}
else if ((abt[(2 * p)] >= 65) && (abt[(2 * p)] <= 90)) {
//字符A-Z
j = abt[(2 * p)] - 65 + 10;
}
else {
//数字0-9
j = abt[(2 * p)] - 48;
}
if ((abt[(2 * p + 1)] >= 97) && (abt[(2 * p + 1)] <= 122)) {
k = abt[(2 * p + 1)] - 97 + 10;
}
else if ((abt[(2 * p + 1)] >= 65) && (abt[(2 * p + 1)] <= 90)) {
k = abt[(2 * p + 1)] - 65 + 10;
}
else {
k = abt[(2 * p + 1)] - 48;
}
int a = (j << 4) + k;
byte b = (byte) a;
bbt[p] = b;
}
return bbt;
}
/**
* 将bcd编码的字节数组转换成str字符串,比如byte[] {0x12, 0x34, 0x56}转成字符串"123456"
* @param bytes
* @return
*/
private static String bcd2Str(byte[] bytes) {
//str字符串的长度为原来的2倍
StringBuffer temp = new StringBuffer(bytes.length * 2);
for (int i = 0; i < bytes.length; ++i) {
//得到高4位二进制转换后的十进制值
byte left = (byte) ((bytes[i] & 0xF0) >>> 4);
//得到低4位二进制转换后的十进制值
byte right = (byte) (bytes[i] & 0x0F);
//根据ASCII码表转成str表示的数字
temp.append(String.format("%c",
new Object[]{Integer.valueOf(left + 48)}));
temp.append(String.format("%c",
new Object[]{Integer.valueOf(right + 48)}));
}
return temp.toString();
}
3.2 ASC编码
这个就是前面介绍过的ASCII编码,主要看一下它的编解码转换,需要注意的是如果字符中包含中文或者其他语言,则需要使用支持的编码方式去编码,然后再用相同方式去解码,否则会产生乱码。比如说用ISO-8859-1对中文进行编码,再解码会产生乱码,因为ISO-8859-1本身就不支持中文。
8583报文ASC编码主要应用于一些域中除了数字外,还有英文字母或者特殊符号,如域41,42,44等
//str转asc
byte[] asc = "123456".getBytes("utf-8");
//asc转str
String str = new String(asc, "utf-8");
3.3 其他
8583报文中除了以上这两种编码方式外,还存在以下两种情况:
- 一些域如域64报文鉴别码,它是一个固定的8字节的数据,通过前面的各个域计算而来,用于鉴别报文是否被篡改。
- 一些自定义域中需要上传图片信息,有特定的自己的编码方式。
这两种情况,一般的处理方法是将域数据直接转换成字节数组,不需要编码(没办法再进一步压缩),直接将数值原值上送就可以了。
4. 组解包自动化思路
在做收单交易中,每一笔交易都需要有多个的交易请求过程,每个过程都包含组解包即编解码的过程,那通过代码将编解码的方式自动化是很有必要的,下面提供一个组解包自动化的思路。
定义一个全局的配置文件,用于配置8583报文各个域的组解包方法,包括长度编码类型、数值编码类型、最大长度、是否是变长、BCD压缩时是左靠还是右靠等,尽量将配置文件弄得简洁一些。
<?xml version="1.0" encoding="utf-8"?>
<!--
This is the template for CUP ISO8583, it defines the necessary attributes for packing/unpacking.
With this, you can define the attributes with a friendly interface, instead of setting them one by one
in the code. Of course, you can also set all these attributes in the code when necessary.
The attributes are listed as follows:
1. global settings
1) secondary_bitmap(opt.)
indicating if secondary bitmap(i.e. filed 1) exists or not, "YES" for true, "NO" otherwise, default to "NO".
2) var_len_format(opt.)
the format of the varible length(i.e. L/LL/LLL), can be "BCD"/"ASC"/"BIN", default to "BCD"
i) BCD: BCD
* for LVAR, 1 byte, range 0~9;
* for LLVAR, 1 byte, range 0~99;
* for LLLVAR, 2 bytes, range 0~999;
ii) ASC: ASCII
* for LVAR, 1 byte, range 0~9;
* for LLVAR, 2 byte, range 0~99;
* for LLLVAR, 3 bytes, range 0~999;
iii) BIN: BINARY
* for LVAR, 1 byte, range 0~0xF;
* for LLVAR, 1 byte, range 0~0xFF;
* for LLLVAR, 2 bytes, range 0~0xFFF;
2. field settings
1) tag name(mandatory)
h: header, including TPDU and some other proprietary fields
m: msg id
fx: field x
2) format string (mandatory)
currently supports "A"/"N"/"S"/"AN"/"AS"/"NS"/"ANS"/"B"/"Z"
for variable length format, use ".x/..xx/...xxx"
e.g. Alpha 10 bytes: "A10"; Binary 64 bits: "B64"; Alphanumeric LLVAR with max length 80: "AN..80"
3) description (opt.)
this is optional, it's mainly for debug purpose.
NOTE:
If you need to define fields above 64, 'secondary_bitmap' MUST be set to "YES" first;
Field 65 is specially for tertiary bitmap, setting format or value to this field is ignored internally;
For Android, you MUST place this file into assets.
-->
<iso8583 secondary_bitmap="NO" var_len_format="BCD">
<h format="N22" description="header"/>
<m format="N4" description="msg_id"/>
...
</iso8583>