webSocket 二进制传输基础准备-UTF-16和UTF-8转Unicode

3次阅读

共计 4710 个字符,预计需要花费 12 分钟才能阅读完成。

前言
今天来学习 UTF8 转 Unicode,UTF16 转 Unicode 以达成 UTF8,UTF16 之间的互转。提炼成函数的公式我并没有放出来,我的目的只是为了更加理解 字符编码之间的关系。如果你需要转码方式,可以找其他的库,或者根据我文章来进行提炼。基本利用按位操作符 符号运算符就可以完成。
今天这里只做 UTF8 转 Unicode,UTF16 转 Unicode,后续转换可以看前面的文章。
1. 基础准备工作 2.Unicode 转 UTF83.Unicode 转 UTF16
UTF16 转 Unicode
为了更好的理解,我们来使用 Unicode 转 UTF-16 那一期的结果来进行 UTF16 转 Unicode,U+22222 转 UTF-16 = [0xd848,0xde22] = ‘????'(这个字的长度为二,所以要获取他所有的 charCodeAt)
function charCodeAt(str){

var length = str.length,
num = 0,
utf16Arr = [];

for(num; num < length; num++){
utf16Arr[num] = ‘0x’+str[num].charCodeAt().toString(16);
}

return utf16Arr;
}
charCodeAt(‘????’);//[‘0xD848’, ‘0xDE22’]

计算 utf-16 4 字节的取值范围
上面代码获得了,这个字符的 UTF-16 编码数组,JS 的字符串全部使用的 UTF-16 编码格式回顾一下 UTF-16 的编码方式将 Unicode 值减去 0x10000, 得到 20 位长的值,再将其分为高 10 位和低 10 位,分别为 2 个字节,高 10 位和低 10 位的范围都在 0 ~ 0x3FF,高 10 位加 0xD800, 低十位加 0xDC00
首先我们先看字节问题,Unicode 值在 U +10000 ~ U+10FFFF 时,会分为 两个 2 字节, 二进制 8 位为一个字节, 所以 UTF-16 的四个字节的字符是两个 16 位的二进制并且根据 UTF-16 的编码方式的高位加 0xD800 低位加 0xDC00 得出最小范围值高 10 位最小值为 0xD800,低 10 为最小值为 0xDC00 再根据 高 10 位和低 10 位的范围都在 0 ~ 0x3FF 得出最大范围值高 10 位最大值为 0xD800+0x3FF,低 10 为最大值为 0xDC00+0x3FF
所以高 10 位的取值范围为 0xD800 ~ 0xdbff 低 10 位的取值范围为 高 10 位的取值范围为 0xDC00 ~ 0xdfff
我们已经得知了 UTF16 编码的高 10 位和低 10 位的取值范围所以可以进行判断 是否需要进行逆推转换
var strCode = charCodeAt(‘????’),
strCode0 = strCode[0],
strCode1 = strCode[1];

if(strCode0 >= 0xD800 && strCode0 <= 0xDBFF){
if(strCode1 !=undefined && strCode1 >= 0xDC00 && strCode1 <= 0xDFFF){
// 到了这里说明这个字符是四个字节就可以开始进行逆推了
// 高 10 位加 0xD800, 低十位加 0xDC00,所以减去
strCode0 = strCode0 – 0xD800 = 0xd848 – 0xD800 = 0x48 = 72;
strCode1 = strCode1 – 0xDC00 = 0xDE22 – 0xDC00 = 0x222 = 546;
// 高 10 位和低 10 位进行拼接。字符串或者乘法都行

//1 字符串的方式拼接 我用抽象的方式来展现过程
strCode0.toString(2)+strCode1.toString(2) = ‘1001000’ + ‘1000100010’ = ‘10010001000100010’
parseInt(Number(‘10010001000100010’),2).toString(16)//74274 = 0x12222
//Unicode 转 utf16 时 将 Unicode 值减去 0x10000,所以再进行加法
0x12222 + 0x10000 = 0x22222; // 答案是不是昨天选择的值呢

//2 利用数学的方式进行转换
// 先给高 10 位从末位补 10 个 0,也就是乘以 10000000000(二进制) = 0x400(16 进制) = 1024(十进制)
strCode0*0x400 = 0x48*0x400 = 1001000*10000000000 = 1001000 10000000000 = 0x12000
// 再加上减去 0xDC00 后的低 10 位
0x12000+0x222 = 0x12222
// 加上 Unicode 转 utf16 时 将 Unicode 值减去的 0x10000
0x12222+0x10000 = 0x22222;
//Unicode U+22222 = ‘????’;
return;
}
}
// 不满足上面条件时,说明 UTF16 转 Unicode 等于原值。不懂为什么就回顾上期的表格

UTF8 转 Unicode
这里一样 使用 Unicode 转 UTF8 那期例子运算出的结果 [0xe4, 0xb8,0x80] 进行转换
由于 JS 环境的字符串是 UTF16 编码所以我这里直接使用十六进制串来进行转换
怎么判断二进制数据的字符是 utf8 中的几字节
根据数据的第一个字节来进行判断 这个字符是几个字节。根据表格找到编码规则, 用来区分这个数据串的字符是几字节
js 是使用小端存储的,小端存储是符合我们的逻辑,大端是逻辑相反大小端模式
比如 小端存储是 0xxx xxxx 大端存储就是相反的 xxxx xxx0
utf8 编码规则
1 字节 0xxx xxxx
2 字节 110x xxxx 10xxxxxx
3 字节 1110 xxxx 10xxxxxx 10xxxxxx
4 字节 1111 0xxx 10xxxxxxx 10xxxxxx 10xxxxxx
js 是小端存储所以只需要按字节位进行对比即可。
utf8 各字节编码规则鲜明差异比较大的是首个字节,所以只需要对比首个字节,就可得知是几个字节。
对比规则
根据 按位与的特性,将原码的 x 对应,编码规则的位值转为 0 其他位保持不变(若有更好的判断方法,非常期待您的留言)
也可以使用 带符号右移操作符 >>(js 并不会转换正负符号 所以可以进行放心使用)
对应编码规则右移 n 个位来进行判断值是否为 0,110,1111。(只是猜想之一,并没有进行实际验证,目前仅实践了下面的方式)
推导过程
根据按位与用 1 来保留原码对应的编码规则位值以及 x 位值全部转换为 0 来进行判断是几字节
二进制 将 x 替换为 0 十六进制
1 字节 char0 & 1xxx xxxx = 0xxx xxxx char0 & 1000 0000 = 0000 0000 char0 & 0x80 = 0
2 字节 char0 & 111x xxxx = 110x xxxx char0 & 1110 0000 = 1100 0000 char0 & 0xE0 = 0xC0
3 字节 char0 & 1111 xxxx = 1110 xxxx char0 & 1111 0000 = 1110 0000 char0 & 0xF0 = 0xE0
4 字节 char0 & 1111 1xxx = 1111 0xxx char0 & 1111 1000 = 1111 0000 char0 & 0xF8 = 0xF0
上面的判断规则已经非常明了。
下面的转码 我就只进行三字节的转码规则,其他 若有兴趣,可自行参考 3 字节的方式进行推算(动手才是理解最好的方式)
var buffer = new ArrayBuffer(6);
var view = new DataView(buffer);
view.setUint8(0,0xe4);
view.setUint8(1,0xb8);
view.setUint8(2,0×80);
view.setUint8(3,0xe4);
view.setUint8(4,0xb8);
view.setUint8(5,0×80);
//[[Uint8Array]]: Uint8Array(6) [228, 184, 128, 228, 184, 128]

var byteOffset = 0,// 起点从 1 开始
char0,
length = view.byteLength;// 获取数据的字节数

while(byteOffset <length){
var char0 = view.getUint8(byteOffset),char1,char2,char3;
if((char0 & 0x80) == 0){
// 代表是一个字节
byteOffset++;
continue;
}
if((char0 & 0xE0) == 0xC0){
// 代表是 2 个字节
byteOffset+=2;
continue;
}
if((char0 & 0xF0) == 0xE0){
// 代表是 3 个字节
//3 字节编码规则 1110 xxxx 10xxxxxx 10xxxxxx
// 进入这个区间时,char0 是符合 1110 xxxx 的规则的
// 利用按位与来进行截取 char0 对应编码规则 x 的位值 也就是 0000 1111 0xF
// 我们先转换第一个字节, 二进制 速算法 先将二进制进行转换 16 进制(4 位二进制为一位十六进制)
//228 & 0xF 1110 0100 & 0000 1111 = 100 = 0x4 = 4

char0 = char0 & 0xF = 4

// 第二字节进行转换
// 第二字节编码规则 10xx xxxx 同理利用按位与 0011 1111 0x3F
//184 & 0x3F 1011 1000 & 0011 1111 = 11 1000 = 0x38 = 56
char1 = view.getUint8(byteOffset++);
char1 = char1 & 0x3F = 56

// 第三字节进行转换
// 第三字节编码规则 10xx xxxx 同理利用按位与 0011 1111 0x3F
//128 & 0x3F 1000 0000 & 0011 1111 = 00 0000 = 0x00 = 0

char2 = view.getUint8(byteOffset++)
char2 = char2& 0x3F = 0

// 下面才是重点, 我们已经按字节转码完成 那么如何进行组合呢。
// 第一种方法,利用字符串进行拼接。
//’100′ + ’11 1000′ + ’00 0000′ = ‘0100 1110 0000 0000’
//parseInt(100111000000000,2) = 19968
//String.fromCharCode(19968) = ‘ 一 ’
// 上面 我抽象的用二进制的过程来展现的,但是实际转换中 是看不到二进制的。
//parseInt(Number(char0.toString(2) + char1.toString(2) + char2.toString(2)),2)

// 第二种方式,利用左移操作符 <<
// 编码规则 1110 xxxx 10xxxxxx 10xxxxxx 第一字节后面有 12 个 x 所以第一字节末位补 12 个 0
//char0 >> 12 = 4 >> 12
//00000000 00000000 00000000 00000100 >> 12 = 0100 0000 0000 0000 = 0x4000 = 16384
// 第二自己后面有 6 个 x 所以第二字节补 6 个 0
//char1 >> 6 = 56 >> 12
//00000000 00000000 00000000 00111000 >> 6 = 1110 0000 0000 = 0xE00 = 3584
// 第三字节为最后一个字节所以不需要末位补 0
// 利用按位或 进行组合
16384 | 3584 | 0 = 0100 1110 0000 0000 = 0x4e00 = 19968
//19968 < 0x10000(U+10000), 不需要进行转码, 调用 String.fromCharCode 即可
//Unicode 码就转换完成了。
continue;
}

if((char0 & 0xF8) == 0xF0){
// 代表是 4 个字节
byteOffset+=4;
continue;
}
throw RangeError(‘ 引用错误 ’);
}
这里编码转换就完成了。

正文完
 0