webSocket 二進位制傳輸基礎準備-UTF-16和UTF-8轉Unicode

咕咕gu發表於2019-04-10

前言

今天來學習UTF8轉Unicode,UTF16轉Unicode以達成UTF8,UTF16之間的互轉。 提煉成函式的公式我並沒有放出來,我的目的只是為了更加理解 字元編碼之間的關係。 如果你需要轉碼方式,可以找其他的庫,或者根據我文章來進行提煉。 基本利用按位操作符 符號運算子就可以完成。

今天這裡只做UTF8轉Unicode,UTF16轉Unicode, 後續轉換可以看前面的文章。

1.基礎準備工作

2.Unicode轉UTF16和UTF8

3.UTF-16和UTF-8轉Unicode

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

這裡一樣 使用上期例子運算出的結果[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,0x80);
view.setUint8(3,0xe4);
view.setUint8(4,0xb8);
view.setUint8(5,0x80);
//[[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('引用錯誤');
}
複製程式碼

這裡編碼轉換就完成了。

相關文章