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

2022-09-17 21:04:15

一、前言

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

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

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

a 的固有資料儲存格式-------標準化--------轉化成 b 的固有格式

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

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

二、位元組序

2.1 主機位元組序

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

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

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

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;
}

輸出結果如下:

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;
}

輸出結果如下:

比較奇怪的是,經過我的測試發現,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;
}

輸出結果如下:

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

宣告

參考資料: