作者:薛8
來源:https://ddnd.cn/2019/02/16/byte-hex-ascii/
前言
最近在學習中涉及到計算機儲存、傳輸數字和字符等操作,由于對字節、2進制、10進制、16進制、ASCII碼的概念以及它們之間的關系和轉換理解的不夠透徹,導致在通訊、MD5消息摘要算法等時候出現問題,是因為數據轉成計算機認識的01的這個環節出現問題。由于這個問題并不是那么容易發現,所以我也算是花了挺多時間才解決了這個問題,記錄下解決過程,順便也當復習一下計算機組成原理。
ASCII碼
在計算機中,所有的數據在存儲和運算時都要使用二進制數表示(因為計算機用高電平和低電平分別表示1和0),例如,像a、b、c、d這樣的52個字母(包括大寫)以及0、1等數字還有一些常用的符號(例如*、#、@等)在計算機中存儲時也要使用二進制數來表示,而具體用哪些二進制數字表示哪個符號,當然每個人都可以約定自己的一套(這就叫編碼),而大家如果要想互相通信而不造成混亂,那么大家就必須使用相同的編碼規則,于是美國有關的標準化組織就出臺了ASCII編碼,統一規定了上述常用符號用哪些二進制數來表示。
ASCII 碼一共規定了128個字符(0000 0000-0111 1111)的編碼,比如空格SPACE是32(二進制0010 0000),大寫的字母A是65(二進制0100 0001 )。這128個符號(包括32個不能打印出來的控制符號),只占用了一個字節的后面7位(低7位),最前面的一位(高1位)統一規定為0(不要和數字的符號位搞混)。
當然除了ASCII碼,還有UTF-8、GBK等。
字節
字節(Byte)普通計算機系統能讀取和定位到最小信息單位,即我們通過計算機儲存和傳輸數據的時候都是先把數據轉成字節。
字節即Byte,一個字節代表8個比特(Bit),字節通常縮寫為B,比特通常縮寫為b。字節的大小是8Bit,即字節的范圍是0000 0000 - 1111 1111,對于無符號型,它表示的十進制范圍是[0,255],對于有符號型,高一位表示符號位,它表示的十進制范圍是[-128,127]。
計算機若何儲存數據
計算機只認識0和1(因為計算機只有高低電平兩個狀態),數據要想通過計算機儲存或者傳輸,首先是要把數據轉成計算機能認識的格式即01數據。
我們舉個例子,以儲存十進制數字28和-28為例,首先將十進制數轉成二進制。
需要注意的是: 數字在計算機中儲存的是補碼,而字符是在計算機中儲存的是字符對應的編碼(不要和數字的補碼搞混)。
數字
儲存十進制數字28和-28為例,首先將十進制數轉成二進制,高1位為0代表正數,為1代表負數
28(10) = 0001 1100(2)(原碼)
-28(10) = 1001 1100(2)(原碼)
然后計算機將二進制數字進行補碼運算,運算結果如下
28(10) = 0001 1100(2)(原碼) = 0001 1100(2)(補碼)
-28(10) = 1001 1100(2)(原碼) = 1110 0100(2)(補碼)
然后計算機保存的就是補碼,當要取出數據的時候,就將補碼逆運算一下,即可求出原碼,再將原碼轉換一下就可以得到真實的數據了。
下面以JAVA語言演示這個過程,首先我們要清楚Java的byte、short、int、long都是有符號的(signed)

運行輸出:
28儲存到計算機后為:11100
-28儲存到計算機后為:11111111111111111111111111100100
取出儲存的28 以無符號表示:28
取出儲存的-28 以無符號表示:4294967268
我們驗證一下結果,驗證了計算機確實是以補碼的方式儲存數字。這里有個小問題,就是我們知道int型有4個字節即32個比特,但是28卻輸出了111005個比特而已,是因為toBinaryString()方法把11100前面的0給忽略了。
取出的時候,我們以無符號的標準去處理,導致取出存入的-28結果是4294967268和我們存入的不一樣,這是因為-28是負數,負數的補碼和原碼不一樣,而用無符號處理的話就是直接將11111111111111111111111111100100轉成結果了。而為什么28用有無符號處理結果都一樣是因為正數的原碼和補碼一樣,這樣驗證了Java的數據類型都是有符號的。
至于計算機為什么用補碼來儲存數字,而不是原碼,原因是:
拿單字節整數來說,無符號型,其表示范圍是[0,255],總共表示了256個數據。有符號型,其表示范圍是[-128,127]。
先看無符號,原碼和補碼都一樣,0表示為0000 0000,255表示為1111 1111,剛好滿足了要求,可以表示256個數據。
再看有符號的,若是用原碼表示,0表示為0000 000。因為咱們有符號,所以應該也有個負0(雖然它還是0)1000 0000。這樣的話那就有2個0,也就是只能表示255個數據,不能夠滿足我們的要求。而用補碼則很好的解決了這個問題。
字符
在計算機中,對非數值的字符進行處理時,要對字符進行數字化,即用二進制編碼來表示字符。其中西文字符最常用到的編碼方案有ASCII編碼和EBCDIC編碼。對于漢字,我國也制定的相應的編碼方案,比如 GBK,GB2312等。
比如字符a的ASCII碼十進制值為97,在計算機中用二進制表示就是 01100001。下面同樣用Java來演示計算機是如何儲存字符的。
- 采用UTF-8和GBK兩種編碼儲存漢字


我們調試看看,發現GBK編碼采用2個字節儲存,儲存的數據分別是10進制的-42和-48對應的二進制分別是11010110和11010000(補碼),即漢字中對應的二進制為1101011011010000,即16進制的D6D0,查看GBK對照表,發現16進制編碼D6D0對應的漢字確實是中

而UTF-8編碼采用3個字節儲存,同理將對應的二進制111001001011100010101101轉成16進制,為E4B8AD,通過UTF-8編碼查詢,發現漢字中對應的16進制編碼確實是E4B8AD

- 儲存字符


調試看看,字符串EF有E和F兩個字符,它們對應的十進制ASCII碼分別是69和70

我們發現Java的getBytes()方法是將字符串的每一個字符都儲存到一個字節的,如果我們想把EF儲存在一個字節里面,即EF是一個整體的,一個字節,不能拆分,那我們可以把EF放在一個字節里面(byte)(0xEF),聲明它是一個字節,不是字符,不用再將它轉成字符對應的編碼。
下面說說我在進行MD5消息摘要算法時候遇到的坑,我要對QQ號對應的Hex進行MD5算法散列,這里我舉例QQ號的10進制為12345678,對應的16進制為00BC614E(因為QQ號固定長度4個字節,所以前面補了2個0),一開始我是以下面的方式進行MD5算法的


調試可以看到上面的代碼其實是將字符串00BC614E轉成了8個字節,然后再對這8個字節進行散列,這也是基于字符串進行的MD5散列,和通過網上一些網站散列得到的值是一樣的

但是這個哈希值和預想的結果不一致,后來才知道預想的結果是基于字節進行的MD5散列,也就是00BC614E應該分成4個字節(00、BC、61、4E)而不是8個字節(0、0、B、C、6、1、4、E),然后通過修改代碼


使用(byte)聲明是一個字節,不是字符,不用再將它轉成字符對應的編碼。00、BC、61、4E分別是一個字節,當然因為字節為8個比特,能表示256個數字,因為Java的數據類型是有符號的,所以8個比特能表示的10進制范圍是[-128,127],所以(byte)(x) x不能小于-128和不能大于127,否則會溢出,溢出的部分數據會丟失。