網路位元組序和主機位元組序

一、前言

如今的通訊方式已經趨向與多樣化,異構通訊也已經很普遍了,如手機和電腦中的 QQ 進行互聯互通。

同時,在電腦設計之初,對記憶體中數據的處理也有不同的方式(如「低位數據存儲在低位地址處」或者「高位數據存儲在低位地址處」);然而,在通訊的過程中,數據被一步步封裝,當傳到目的地址時,再被一步步解封,然後獲取數據。

從上面我們可以看出,數據在傳輸的過程中,一定有一個標準化的過程,也就是說從「主機 a」到「主機b」進行通訊:

a 的固有數據存儲格式——-標準化——–轉化成 b 的固有格式

如上而言:a 或者 b 的固有數據存儲格式就是自己的主機位元組序,上面的標準化就是網路位元組序:

a的主機位元組序———-網路位元組序 ———b的主機位元組序

二、位元組序

2.1 主機位元組序

自己的主機內部,記憶體中數據的處理方式,可以分為兩種:

  1. 大端位元組序( big-endian):按照記憶體的增長方向,高位數據存儲於低位記憶體中(最直觀的位元組序 )
  2. 小端位元組序(little-endian):按照記憶體的增長方向,低位數據存儲於低位記憶體中

如果我們要將0x12345678這個十六進位數放入記憶體中:

image-20220917182832008

2.2 網路位元組序

網路數據流也有大小端之分。
網路數據流的地址規定:先發出的數據是低地址,後發出的數據是高地址。
發送主機通常將發送緩衝區中的數據按記憶體地址從低到高的順序發出,為了不使數據流亂序,接收主機也會把從網路上接收的數據按記憶體地址從低到高的順序保存在接收緩衝區中。
TCP/IP協議規定:網路數據流應採用大端位元組序,即低地址高位元組。

三、測試主機位元組序

我們可以通過程式來驗證我們所使用的主機用的是哪一種位元組序,編寫程式前先來談一談測試思路:

  1. 用無符號整形保存數據「0x12345678」,即unsigned int a = 0x12345678
    • 十六進位下的一位 = 4b,那麼 \(0x12345678=8\times4=32b\),故可以考慮用無符號整形保存
  2. unsigned char *p保存 a 的地址,並通過輸出p[0]、p[1]、p[2]、p[3]來觀察主機位元組序

3.1 demo 1.0

#include <stdio.h>

void Print(unsigned char *p)/* 輸出主機的位元組序 */
{
    if (0x12 == p[0]) //判斷高位數據 0x12 是否存儲在低位記憶體中
    {
        printf("   big-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
    else
    {
        printf("little-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
}

int main()
{
    unsigned int a = 0x12345678;
    unsigned char *p = (unsigned char *)(&a);
    Print(p);

    return 0;
}

輸出結果如下:

image-20220917160918599

3.2 demo 2.0

其實,我們也可以利用union記憶體共享的特點改寫一下上邊的 demo:

#include <stdio.h>
#include <stdlib.h>

union
{
    unsigned int u32a;
    char p[4]; //用於觀察 u32a 的記憶體分布情況
} un;

void Print(unsigned char *p)/* 輸出主機的位元組序 */
{
    if (0x12 == p[0]) //判斷高位數據 0x12 是否存儲在低位記憶體中
    {
        printf("   big-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
    else
    {
        printf("little-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
}

int main()
{
    if (4 != sizeof(un.u32a)) // 判斷 unsigned int 是否為 32 位,如果不是,則退出
    {
        exit(0);
    }

    un.u32a = 0x12345678;
    Print(un.p);

    return 0;
}

輸出結果同上。

四、大小端轉換

常用的轉換函數:

  1. htons() : 將 16 位的主機位元組序轉換為網路位元組序;
  2. ntohs() : 將 16 位的網路位元組序轉換為主機位元組序;
  3. htonl() : 將 32 位的主機位元組序轉換為網路位元組序;
  4. ntohl() : 將 32 位的網路位元組序轉換為主機位元組序。

h 是主機 host,n 是網路 net,l 是長整形 long,s是短整形short,所以上面這些函數還是很好理解的。

下面,我們通過程式碼深入理解一下:

#include <stdio.h>
#include <stdlib.h>

union
{
    unsigned int u32a;
    char p[4]; //用於觀察 u32a 的記憶體分布情況
} un;

void Print(unsigned char *p)/* 輸出主機的位元組序 */
{
    if (0x12 == p[0]) //判斷高位數據 0x12 是否存儲在低位記憶體中
    {
        printf("   big-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
    else
    {
        printf("little-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
}

int main()
{
    if (4 != sizeof(un.u32a)) // 判斷 unsigned int 是否為 32 位,如果不是,則退出
    {
        exit(0);
    }

    un.u32a = 0x12345678;
    Print(un.p);

    printf("\n");

    un.u32a = htonl(0x12345678);
    Print(un.p);

    return 0;
}

輸出結果如下:

image-20220917173553580

比較奇怪的是,經過我的測試發現,htonl的實際作用其實是「將 32 位的大端位元組序與小端位元組序進行互轉」:

#include <stdio.h>
#include <stdlib.h>

union
{
    unsigned int u32a;
    char p[4]; //用於觀察 u32a 的記憶體分布情況
} un;

void Print(unsigned char *p)/* 輸出主機的位元組序 */
{
    if (0x12 == p[0]) //判斷高位數據 0x12 是否存儲在低位記憶體中
    {
        printf("   big-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
    else
    {
        printf("little-endian[%0x %0x %0x %0x]\n", p[0], p[1], p[2], p[3]);
    }
}

int main()
{
    if (4 != sizeof(un.u32a)) // 判斷 unsigned int 是否為 32 位,如果不是,則退出
    {
        printf("======> [%s][%s-%lu] u32a[%d]\n", __FILE__, __FUNCTION__, __LINE__, sizeof(un.u32a));
        exit(0);
    }

    un.u32a = 0x12345678;
    Print(un.p);

    un.u32a = htonl(un.u32a);/* 一次轉換,將主機默認的小端位元組序轉化為大端位元組序 */
    Print(un.p);

    un.u32a = htonl(un.u32a);/* 再次轉換,將轉換後的大端位元組序轉換為小端位元組序 */
    Print(un.p);
    
    return 0;
}

輸出結果如下:

image-20220917175545464

驗證成功!其餘的轉換函數同理,可自行測試驗證。

聲明

參考資料: