都知道Base64,Base32你能實現嗎?

牛初九 發表於 2021-01-26

很長時間沒有更新個人部落格了,因為前一段時間在換工作,入職了一家新的公司,剛開始需要適應一下新公司的節奏,開始階段也比較忙。新公司還是有一定的技術氣氛的,每週都會有技術分享,而且還會給大家留一些思考題,這次的思考題就是讓我們回去實現一個Base32的編碼和解碼。

這可怎麼辦?Base64也就知道個大概,Base32怎麼實現呀?回去一頓惡補,查資料,看Base64原始碼,最後終於將Base32實現了。

Base64是幹什麼用的

要寫Base32,就要先理解Base64,那麼Base64是幹什麼用的呢?為什麼要有Base64呢?這個是根本原因,把Base64產生的過程搞清楚了,那麼Base32,我們就可以依葫蘆畫瓢了。

我們知道在計算機中,資料的單位是位元組byte,它是由8位2進位制組成的,總共可以有256個不同的數。那麼這些二進位制的資料要怎麼進行傳輸呢?我們要將其轉化為ASCII字元,ASCII字元中包含了33個控制字元(不可見)和95個可見字元,我們如果能將這些二進位制的資料轉化成這95個可見字元,就可以正常傳輸了。於是,我們從95個字元中,挑選了64個,將2進位制的資料轉化為這個64個可見字元,這樣就可以正常的傳輸了,這就是Base64的由來。那這64個字元是什麼呢?

都知道Base64,Base32你能實現嗎?

這就是Base64的那64個字元。那麼如果我們要實現Base32呢?對了,我們要挑選出32個可見字元,具體如下:

private static final char[] toBase32 = {
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
  '0', '1', '2', '3', '4', '5'
};

我們挑選了大寫的A-Z,再加上0-5,一共32個可見字元。

Base32是什麼規則

好了,32個可見字元已經選好了,接下來就是將2進位制轉化成這32個字元的過程。我們先來看一下Base64是一個什麼樣的轉化過程,我們一個位元組是8位,而64是2的6次方,也即是一個位元組(8位)的資料,我們要擷取其中的6位進行編碼,取到其可見字元。那麼剩餘的2位數怎麼辦呢?它將和下一個自己的前4位組成一個6位的資料進行編碼。那麼我們需要多少位元組才能得到一個完整的不丟位的編碼呢?我們要取6和8的最小公倍數,也就是24,24位恰好是3個位元組,如果取6位進行編碼,則可以取到4個編碼。我們看看下面的圖就可以更好地理解了,

都知道Base64,Base32你能實現嗎?

  • M,a,n對應的ASCII碼分別是77,97,110。
  • 對應的二進位制是01001101,01100001,01101110。
  • 然後我們按照6位擷取,恰好能夠擷取4個編碼,對應的6位二進位制分別為:010011,010110,000101,101110。
  • 對應的64位編碼為:T,W,F,u。

同理,如果我們要實現Base32怎麼辦呢?32是2的5次方,那麼我們再進行2進位制截位時,要一次擷取5位。那麼一個位元組8位,擷取了5位,剩下的3位怎麼辦?同理和下一個位元組的前2位組成一個新的5位。那麼多少個位元組按照5位擷取才能不丟位呢?我們要取5和8的最小公倍數,40位,按照5位擷取,正好得到8個編碼。40位,正好5個位元組,所以我們要5個位元組分為一組,進行Base32的編碼。如下圖:

都知道Base64,Base32你能實現嗎?

對比前面的Base64,Base32就是按照5位去擷取,然後去編碼表中找到對應的字元。好了,原理我們明白了,下面進入程式階段。

寫程式階段

原理明白了,程式怎麼寫呢?這也就是程式猿的價值所在,把現實中的規則、功能、邏輯用程式把它實現。但是實現Base32也是比較難的,不過有先人給我們留下了Base64,我們參照Base64去實現Base32就容易多了。

Base32編碼

首先,我們要根據輸入位元組的長度,確定返回位元組的長度,以上面為例,輸入位元組的長度是5,那麼Base32轉碼後的位元組長度就是8。那麼如果輸入位元組的長度是1,返回結果的位元組長度是多少呢?這就需要補位了,也就是說輸入位元組的長度不是5的倍數,我們要進行補位,將其長度補成5的倍數,這樣編碼以後,返回位元組的長度就是8的倍數。這樣做,我們不會丟失資訊,比如,我們只輸入了一個位元組,是8位,編碼時,擷取了前5位,那麼剩下的後3位怎麼辦?不能捨棄吧,我們要在其後面補足40位,補位用0去補,前面擷取有剩餘的位數再加上後面補位的0,湊成5位,再去編碼。其餘的,全是0的5位二進位制,我們編碼成“=”,這個和Base64是一樣的。

好了,我們先來看看編碼後返回位元組的長度怎麼計算。

//返回結果的陣列長度
int rLength = 8 * ((src.length + 4) / 5);
//返回結果
byte[] result = new byte[rLength];
  • 其中src是輸入的位元組陣列;
  • 返回長度的公式我們要仔細看一下,對5取整,再乘以8,這是一個最基本的操作,我們用上面的例子套一下,輸入位元組的長度是5個位元組,8*(5/5) = 8,需要返回8個位元組。我們再來看看加4的作用,比如我們輸入的是1個位元組,那麼返回幾個位元組呢?按照前面的要求,如果二進位制長度不滿40位,要補滿40位,也就是輸入位元組的長度要補滿成5的整數倍。這裡先加4再對5取整,就可以補位後可以進行完整編碼的個數,然後再乘以8,得到返回的位元組數。大家可以隨便想幾個例子,驗證一下結果對不對。
  • 然後我們定義返回結果的陣列。

返回結果的陣列長度已經確定了,接下來我們做什麼呢?當然是編碼的工作了,這裡我們分為兩個步驟:

  1. 先處理可以正常進行編碼的那些位元組,也就是滿足5的倍數的那些位元組,這些位元組可以進行5位元組到8位元組轉換的,不需要進行補位。
  2. 然後處理最後幾位,這些是需要補位的,將其補成5個位元組。

編碼的步驟已經確定了,下面要確定可以正常編碼的位元組長度,以及需要補位的長度,如下:

//正常轉換的長度
int normalLength = src.length / 5 * 5;
//補位長度
int fillLength = (5 - (src.length % 5)) % 5;

又是兩個計算公式,我們分別看一下:

  1. 可以正常編碼的位元組長度,對5取整,再乘以5,過濾掉最後不滿足5的倍數的位元組,這些過濾掉的位元組需要補位,滿足5個位元組;
  2. 這一步就是計算最後需要補幾位才能滿足5的倍數,最後可以得到需要補位的長度,如果輸入位元組的長度恰好是5的倍數,不需要補位,則計算的結果是0,大家可以驗證一下這兩個公式。

接下來,我們處理一下可以正常編碼的位元組,如下:

//輸入位元組下標
int srcPos = 0;
//返回結果下標
int resultPos = 0;
while (srcPos < normalLength) {
  long bits = ((long)(src[srcPos++] & 0xff)) << 32 |
    (src[srcPos++] & 0xff) << 24 |
    (src[srcPos++] & 0xff) << 16 |
    (src[srcPos++] & 0xff) << 8  |
    (src[srcPos++] & 0xff);

  result[resultPos++] = (byte) toBase32[(int)((bits >> 35) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)((bits >> 30) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)((bits >> 25) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)((bits >> 20) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)((bits >> 15) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)((bits >> 10) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)((bits >> 5) & 0x1f)];
  result[resultPos++] = (byte) toBase32[(int)(bits & 0x1f)];

}
  1. 我們先定義輸入位元組的下標和返回結果的下標,用作取值與賦值;
  2. 再寫個while迴圈,只要輸入的位元組下標在正常轉換的範圍內,就可以正常的編碼;
  3. 接下來看看while迴圈的處理細節,我們先要將5個位元組拼成一個40位的二進位制,在程式中,我們通過位移運算和 | 或運算得到一個long型的數字,當然它的二進位制就是我們用5個位元組拼成的。
  4. 這裡有個坑要和大家說明一下,我們第一個位元組位移的時候用long轉型了,為什麼?因為int型在Java中佔4個位元組,32位,我們左移32位後,它會回到最右側的位置。而long佔64位,我們左移32位是不會迴圈的。這一點大家要格外注意。
  5. 接下來就是將這40位的二進位制進行分拆,同樣通過位移操作,每次從左側擷取5位,我們分別向右移動35、30、25、20、15、10、5、0,然後將其和0x1f進行與操作,0x1f是一個16進位制的數,其二進位制是0001 1111,對了,就是5個1,移位後和0x1f進行與操作,只留取最右側的5位二進位制,並計算其數值,然後從32位編碼表中找到對應的字元。

可以正常編碼的部分就正常結束了,大家要多多理解位移符號的運用。接下來,我們再看看結尾位元組的處理。先上程式碼:

if (fillLength > 0) {
  switch (fillLength) {
    case 1:
      int normalBits1 = (src[srcPos] & 0xff) << 24 |
        (src[srcPos+1] & 0xff) << 16 |
        (src[srcPos+2] & 0xff) << 8  |
        (src[srcPos+3] & 0xff);
      result[resultPos++] = (byte) toBase32[(normalBits1 >> 27) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits1 >> 22) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits1 >> 17) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits1 >> 12) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits1 >> 7) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits1 >> 2) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits1 << 3) & 0x1f];
      result[resultPos++] = '=';
      break;
    case 2:
      int normalBits2 = (src[srcPos] & 0xff) << 16 |
        (src[srcPos+1] & 0xff) << 8 |
        (src[srcPos+2] & 0xff);
      result[resultPos++] = (byte) toBase32[(normalBits2 >> 19) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits2 >> 14) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits2 >> 9) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits2 >> 4) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits2 << 1) & 0x1f];
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      break;
    case 3:
      int normalBits3 = (src[srcPos] & 0xff) << 8 |
        (src[srcPos+1] & 0xff);
      result[resultPos++] = (byte) toBase32[(normalBits3 >> 11) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits3 >> 6) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits3 >> 1) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits3 << 4) & 0x1f];
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      break;
    case 4:
      int normalBits4 = (src[srcPos] & 0xff) ;
      result[resultPos++] = (byte) toBase32[(normalBits4 >> 3) & 0x1f];
      result[resultPos++] = (byte) toBase32[(normalBits4 << 2) & 0x1f];
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      result[resultPos++] = '=';
      break;
  }
}
  1. fillLength就是需要補位的位數,如果等於0,我們就不需要補位了。大於0就需要進行補位。
  2. 需要補位的情況,我們分為4種,分別為:補1位、補2位、補3位和補4位。
  3. 我嗯先看看補1位的情況,需要補1位,說明之前剩下4個位元組,我們先將這4個位元組拼起來,那麼第一個位元組要向左移動24位,這個和正常情況下第一個位元組向左移動的位數是不一樣的。剩餘的位元組分別向左移動相應的位數,大家可以參照程式計算一下。
  4. 然後將得到的32位二進位制數,從最高位每次擷取5位,每次向右移動位數分別為27、22、17、12、7、2,注意,最後剩下2位,不足5位,我們要向左移動3位。移位後要和0x1f進行與操作,這個作用和前面是一樣的,這裡不贅述了。然後將得到的數字在32位編碼表中,去除對應的字元。
  5. 剩下的位數我們統一使用=進行補位。
  6. 其他的需要補1位、補2位和補3位的情況,我們重複步驟3-步驟5,裡邊具體的移動位數有所區別,需要大家仔細計算。

整個的編碼過程到這裡就結束了,我們將result陣列返回即可。

總結

到這裡,Base32的編碼就實現了,大家可以執行一下,這裡就不演示了。整個的實現過程大家感覺怎麼樣,我們總結一下,

  1. 原理,不知道其原理,我們就沒有辦法寫程式。
  2. 定義32位字元編碼表,大家可以根據個人喜好進行定義,沒有標準,只要是可見字元就可以。
  3. 寫程式時,要注意正常位數的計算,補位位數的計算,以及左移右移,都是需要大家仔細思考的。

好了,Base32編碼的過程就結束了,還缺少解碼的過程,我們有時間再補上吧~