近日在公司研究开发小程序版应用的可行性,某天收到一段Java代码,是接口的加密解密代码,大致流程是逐字符(编码范围在33~127的,也就是说对汉字没有影响)对编码进行变换,变换的方式取决于字符串的长度,某个随机数,以及字符在字符串中的位置。那好说啊,去掉各种类型转换就好了,于是我很快搞了一版 JavaScript 的出来,试了下加密解密,OK的。于是我又拿到一个接口,写个 AJAX 请求解密下试试。嗯?嗯?乱的?(O_O)?
之后我核对了一遍我的代码,然后又整理了代码的逻辑自己写了一份,结果还是不行。之后继续确定,请求到的内容是完整的,没有被改动。也没有不该有的转义字符啊什么的。
于是我决定把那份Java代码在本地跑一遍,新建个 Hello World 的模板,按照错误提示把类塞进去,把常用的符号写个字符串,在 JS 版本和 Java 版本上都跑跑看。后来在含有汉字的字符串的时候发现了问题。打个断点发现,Java 中 byte[] bytes = str.getBytes();,这个地方的 bytes 比我 JS 中的结果长了很多。因为我 JS 代码这个地方错误的取成了 str.charCodeAt() 的值。对于汉字“中”来说,前者是字节码,而后者是字符编码。
好了,前面都是废话,下面是正文。
Java 中
String.getBytes()获取到的内容是字符串的字节码。不指定编码的话,使用平台的默认字符集将字符串编码为 byte 序列,并将结果存储到一个新的 byte 数组中。 From: RUNOOB - Java getBytes() 方法
JavaScript 中 String.charCodeAt() 获取到的是:
返回指定位置的字符的 Unicode 值。 From: MSDN - charCodeAt 方法 (String) (JavaScript)
返回值是一表示给定索引处字符的 UTF-16 代码单元值的数字;如果索引超出范围,则返回 NaN。 From: MDN - String.prototype.charCodeAt()
charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。 方法 charCodeAt() 与 charAt() 方法执行的操作相似,只不过前者返回的是位于指定位置的字符的编码,而后者返回的是字符子串。 From: W3School - JavaScript charCodeAt() 方法
就是 JavaScript 中 String.charCodeAt() 获取到的是字符的 Unicode 字符编码,而 Java String.getBytes() 获取到的是字节码,据说编码方式如果不指定的话跟平台有关,当时用 MAC 得到的是 UTF-8,服务端 Linux 也应该是 UTF-8,当时写的时候,完全没去考虑 byte 的意义。
然后我在 StackOverflow 看到一段字符码转字节码的代码 。
function toUTF8Array(str) {
var utf8 = [];
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f));
} else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(
0xe0 | (charcode >> 12),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f),
);
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode =
0x10000 + (((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
utf8.push(
0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f),
);
}
}
return utf8;
}
但是没有找到反过来的成熟轮子。于是后来决定自己看下编码方式自己写一段。
然后维基百科可查:
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。
UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):
| 码点的位数 | 码点起值 | 码点终值 | 字节序列 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 |
|---|---|---|---|---|---|---|---|---|---|
| 7 | U+0000 | U+007F | 1 | 0xxxxxxx | - | - | - | - | - |
| 11 | U+0080 | U+07FF | 2 | 110xxxxx | 10xxxxxx | - | - | - | - |
| 16 | U+0800 | U+FFFF | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | - | - | - |
| 21 | U+10000 | U+1FFFFF | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | - | - |
| 26 | U+200000 | U+3FFFFFF | 5 | 111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | - |
| 31 | U+4000000 | U+7FFFFFFF | 6 | 1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
以上内容来自 维基百科 - UTF-8
关于这个编码方式,引用阮老师的总结:
UTF-8的编码规则很简单,只有二条: 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。 From: 阮一峰的网络日志 - 字符编码笔记:ASCII,Unicode和UTF-8
于是我写了一段代码:
function strToUtf8Array(str) {
let utf8 = [];
for (var i = 0; i < str.length; i++) {
let code = str.charCodeAt(i);
if (code <= 0x007f) {
utf8.push(code | 0b00000000);
} else if (code < 0x07ff) {
utf8.push((code >> 6) | 0b11000000, (code & 0x3f) | 0x80);
} else if (code < 0xffff) {
utf8.push(
(code >> 12) | 0b11100000,
((code >> 6) & 0x3f) | 0x80,
(code & 0x3f) | 0x80,
);
} else if (code < 0x1fffff) {
utf8.push(
(code >> 18) | 0b11110000,
((code >> 12) & 0x3f) | 0x80,
((code >> 6) & 0x3f) | 0x80,
(code & 0x3f) | 0x80,
);
} else if (code < 0x3ffffff) {
utf8.push(
(code >> 24) | 0b11111000,
((code >> 18) & 0x3f) | 0x80,
((code >> 12) & 0x3f) | 0x80,
((code >> 6) & 0x3f) | 0x80,
(code & 0x3f) | 0x80,
);
} else if (code < 0x7fffffff) {
utf8.push(
(code >> 30) | 0b11111100,
((code >> 24) & 0x3f) | 0x80,
((code >> 18) & 0x3f) | 0x80,
((code >> 12) & 0x3f) | 0x80,
((code >> 6) & 0x3f) | 0x80,
(code & 0x3f) | 0x80,
);
}
}
return utf8;
}
function utf8ArrayToCodes(utf8) {
let str = "",
arr = [];
for (var i = 0; i < utf8.length; i++) {
var code = utf8[i],
flag = code < 0b10000000 ? 1 : code.toString(2).indexOf(0),
tmp = [];
switch (flag) {
case 6:
tmp.unshift(utf8[i + 5].toString(2).slice(2));
case 5:
tmp.unshift(utf8[i + 4].toString(2).slice(2));
case 4:
tmp.unshift(utf8[i + 3].toString(2).slice(2));
case 3:
tmp.unshift(utf8[i + 2].toString(2).slice(2));
case 2:
tmp.unshift(utf8[i + 1].toString(2).slice(2));
case 1:
tmp.unshift(utf8[i].toString(2).slice(flag === 1 ? 0 : flag + 1));
}
arr.push(+("0b" + tmp.join("")));
i += flag - 1;
}
return arr;
}
然后问题就解决了 ┑( ̄Д  ̄)┍