博客 / 詳情

返回

hex,base64,urlencode編碼方案對比

原創:打碼日記(微信公眾號ID:codelogs),歡迎分享,轉載請保留出處。

簡介

在工作過程中,我們慢慢會了解到hex、base64、urlencode這3種常見的字節編碼方案,它們是如此的熟悉,可是經常我們自己也説不清為啥要使用它們,下面我會詳細解釋下。

hex編碼

hex編碼,又稱十六進制編碼(也稱base16),一般用於方便人們查看二進制文件內容,它將字節數據中的每4個bit使用數字(0-9)、字母(A-F)共16個字符等效表示,由於一個字節有8個bit,所以一個字節會被編碼為2個hex字符,具體規則如下: 
hex編碼

Linux中可使用xxd來做hex編解碼,如下: 

# abc這3個英文字符會被echo編碼為3個字節,然後被xxd編碼為6個hex字符
$ echo -n abc|xxd -ps
616263 
# 解碼hex數據
$ echo 616263|xxd -ps -r
abc

base64編碼

base64編碼,它將字節數據中的每6個bit使用字母(a-zA-Z)、數字(0-9)、+、/總共64個字符等效表示,故每3個字節(8bit)會被編碼為4個base64中的字符。 
由於數據中的字節數不一定是3的整數倍,當字節數對3求模後,多1個字節時,那個字節會被編碼為2個字符加2個=號(填充字符),多2個字節時,這2個字節會被編碼為3個字符加1個=號(填充字符),剛好整除時,則不需要=號填充,具體規則如下: 
base64編碼
Linux下可以使用base64這個命令做base64編解碼

# 3個字母等於3個字節,所以會編碼為4個base64字符,並沒有=號
$ echo -n abc | base64
YWJj 
# 1個字節會被編碼為2個base64字符,另加2個=號填充
$ echo -n a | base64
YQ==
# 2個字節會被編碼為3個base64字符,另加1個=號填充
$ echo -n ab|base64
YWI=
# 解碼base64數據
$ echo YWI= | base64 -d
ab

另外,base64編碼有一些常見的變種,以下3種是常見的:

  1. MimeBase64
    每76個字符後會添加換行符\r\n,便於閲讀。
  2. UrlBase64
    由於Base64編碼使用了+ /兩個字符,這與url命名規則衝突(/在url中是路徑分隔符,+會被urldecode為空格字符),這個變種將+ /這兩個字符更換為- _,如下:
    urlbase64
  3. NoPaddingBase64
    由於Base64編碼是對6bit進行編碼,數據以8bit存儲,當字節數不是3的整數倍時需要=號填充,這種方案就是去掉了=號,從上面的編碼示例中也可以看出,加=號填充純粹是為了保持base64編碼字符串長度為4的整數倍,去掉=號其實不影響解析。   

urlencode編碼

urlencode編碼,看名字就知道是設計用來給url編碼的,對於a-zA-Z0-9.-_ ,urlencode都不會做任何處理原樣輸出,而其它字節會被編碼為%xx(16進制)的形式,其中xx就是這個字節對應的hex編碼。 

Linux下gridsite-clients包實現了urlencode命令,如下:

$ sudo apt install gridsite-clients
$ urlencode 'a b'
a%20b
$ urlencode -d a%20b
a b

使用python也很容易實現urlencode,可將其定義為 Linux 命名別名,方便使用,如下:  

alias urlencode='python -c "import sys, urllib as ul; print ul.quote_plus(sys.argv[1])"'
alias urldecode='python -c "import sys, urllib as ul; print ul.unquote_plus(sys.argv[1])"'

另外,不同的urlencode實現上也有些差異,比如某些urlencode會將空格編碼為+(W3C標準規定),而另外一些實現中,空格會被編碼為%20(RFC 2396)。

注:java中的URLEncoder、javascript中的encodeURIComponent、html表單提交中的application/x-www-form-urlencode,這些都會將空格編碼為+,而一些web服務器在進行某些urldecode時會不認識+號,所以在使用這些函數進行urlencode編碼時,最好將編碼後的+替換為%20,如URLEncoder.encode(bytes, "UTF-8").replace("+", "%20")

兩次urlencode解決亂碼

在最開始遇到亂碼問題時,在網上搜到一種 “客户端兩次urlencode,服務端一次urldecode” 的亂碼解決方案,並聲稱這樣能徹底解決亂碼。
然後很長一段時間我都是這樣實踐的,但一直不知道為什麼,直到有一次我調試亂碼問題調試到tomcat裏面去才發現真相,原來web服務器對url都會自動做一次urldecode,urldecode後的字節使用server.xml中配置的uri-encoding字符編碼轉換成字符串,而如果uri-encoding這個字符編碼配置與客户端使用的不同,就會出現亂碼,下面用2個示例模擬一下: 

  1. 客户端使用UTF-8進行一次urlencode,服務端tomcat使用uri-encoding的默認編碼ISO-8859-1為例: 

    String sendParam = "好";
    // 使用UTF-8進行urlencode,'好'編碼為 %E5%A5%BD
    String urlencodeSendParam = URLEncoder.encode(sendParam, "UTF-8");
    
    //....這裏表示數據從客户端傳至服務端
    String urlencodeReceivedParam = urlencodeSendParam;
    // 使用ISO-8859-1進行urldecode後,%E5%A5%BD解碼為亂碼 好 ,注意這個解碼是web服務器自動進行的
    String receivedParam = URLDecoder.decode(urlencodeReceivedParam, "ISO-8859-1");
    // 會輸出亂碼 好
    System.out.println(receivedParam);
  2. 如果客户端做兩次urlencode,服務端做一次urldecode,過程如下:

    String sendParam = "好";
    // 使用UTF-8進行urlencode,'好'編碼為%E5%A5%BD
    String urlencodeSendParam = URLEncoder.encode(sendParam, "UTF-8");
    // 再使用UTF-8進行urlencode,%E5%A5%BD 編碼為 %25E5%25A5%25BD
    String urlencodeSendParam2 = URLEncoder.encode(urlencodeSendParam, "UTF-8");
    
    //....這裏表示數據從客户端傳至服務端
    String urlencodeReceivedParam2 = urlencodeSendParam2;
    // 使用ISO-8859-1進行urldecode後,%25E5%25A5%25BD 解碼為%E5%A5%BD,注意這個解碼是web服務器自動進行的
    String urlencodeReceivedParam = URLDecoder.decode(urlencodeReceivedParam2, "ISO-8859-1");
    // 使用UTF-8進行urldecode後,%E5%A5%BD解碼為'好'
    String receivedParam = URLDecoder.decode(urlencodeReceivedParam, "UTF-8");
    // 會輸出正確的'好'字
    System.out.println(receivedParam);

    從上面的兩個示例中,應該不難看出,之所以前端2次編碼,後端1次解碼不會出現亂碼,是因為前端在第1次urlencode後,數據就已經變成了純英文,而純英文先使用UTF-8的urlencode編碼,再使用ISO-8859-1的urldecode解碼,是可以完全還原數據的。另外,由於服務端的第二次urldecode是你自己寫的,字符編碼當然會和前端使用一致的UTF-8,故字被無誤的還原回來了。

為什麼説英文可以先使用UTF-8的urlencode編碼,再使用ISO-8859-1的urldecode解碼呢?原因是java中的URLEncoder類其實是做了兩件事,先使用字符編碼將字符串轉換為字節,然後對字節進行urlencode編碼,因為urlencode算法本質作用就是將字節數據編碼為等效的英文字符表示,只不過URLEncoder類將其封裝為一步了,等效代碼如下:

// 1. 使用字符編碼,將字符串轉換為字節串,因為urlencode是用來處理字節數據的
byte[] bytes=str.getBytes(charset);
// 2. 將字節數據,使用urlencode算法,編碼為英文字符串
String urlencodeStr = urlencode(bytes);

而對於UTF-8與ISO-8859-1來説,它們都是兼容ASCII碼的,所以對於純英文的urlencode編解碼,編碼數據是可以正確解碼的,不信你可以把ISO-8859-1變成UTF-16試試,由於UTF-16是不兼容ASCII的,所以上面的方案處理後依然為亂碼。

ps,雖然這種方案基本可以完美解決亂碼(基於大多數主流字符編碼兼容ASCII),但由於第二次urlencode編碼又會將%編碼為%25,使得數據體積增大不少,所以非必要情況下,還是不要濫用比較好,能都用UTF-8就都用UTF-8吧。我以前經歷過的項目都比較奇葩,多種編碼混搭,才導致我要如此瞭解編碼機制[-_-]

注:深入理解字符編碼方面,可以看看我寫的這篇文章字符編碼解惑

這些編碼有啥用?

這些編碼的本質作用都是將字節數據轉換為等效的純英文形式,主要用在那些不方便查看、存儲或傳輸原始字節數據的地方。
比如在html中,因為html本身就是純文本的,不能直接放入原始字節數據,這時,我們可以將一些小圖標(非文本數據)通過base64編碼的方式內嵌到html中,以使得html頁面與圖標數據能在一次網絡交互中返回,這種方案也稱Data URI。

對比

  1. hex編碼
    就算原文件是純英文內容,編碼後內容也和原文完全不一樣,普通人難以閲讀,但由於只有16個字符,聽説一些程序員大牛能夠記下他們的映射關係,從而達到讀hex編碼和讀原文一樣的效果。另外,數據在經過hex編碼後,空間佔用變成了原來的2倍。
  2. base64編碼
    由64個字符組成,比hex編碼更難閲讀,但由於每3個字節會被編碼為4個字符,所以,空間佔用會是原來的4/3,比hex要節省空間。另外要注意的是,雖然Base64編碼後的數據難以閲讀,但不能將其做為加密算法使用,因為它解碼都不需要你提供密鑰啊。
  3. urlencode編碼
    由於英文字符原樣保留,對於以英文為主的內容,可讀性最好,空間佔用幾乎不變,而對於非英文內容,每個字節會被編碼為%xx的3個字符,空間佔用是原來的3倍,所以urlencode是一個對英文友好的編碼方案。 

總結

除了hex,base64,urlencode編碼之外,其實還有base32,base58這樣的編碼,但它們只是編碼方式不同罷了,本質作用是相同的,即將字節數據轉換為等效的純英文表示,方便傳輸與存儲

往期內容

字符編碼解惑
真正理解可重複讀事務隔離級別
Linux文本命令技巧(下)
Linux文本命令技巧(上)
常用網絡命令總結

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.