實踐:二進位數據處理與封裝

来源:https://www.cnblogs.com/zhe-si/archive/2022/08/04/16550833.html
-Advertisement-
Play Games

最近在研究所做網路終端測試的項目,包括一些嵌入式和底層數據幀的封裝調用。之前很少接觸對二進位原始數據的處理與封裝,所以在此進行整理。 ...


實踐:二進位數據處理與封裝

作者:哲思

時間:2022.8.4

郵箱:[email protected]

GitHub:zhe-si (哲思) (github.com)

前言

最近在研究所做網路終端測試的項目,包括一些嵌入式和底層數據幀的封裝調用。之前很少接觸對二進位原始數據的處理與封裝,所以在此進行整理。

以下例子主要以 c++ 語言進行說明。

什麼是二進位數據

在電腦上一切數據都是通過二進位(0或1)進行存儲的,通過多位二進位數據可以進而表示整形、浮點型、字元、字元串等各種基礎類型數據或者一些更複雜的數據格式。

針對日常中一般的需求進行編程,我們通常無需關註底層的二進位數據。但如果要處理二進位文件(音頻、視頻、圖片等)、設計空間上更高效的數據結構(網路數據幀、位元組碼、protobuf)或者處理某些底層時,需要我們處理這些二進位數據。

電腦中,稱每一個二進位位為比特(bit,也稱:位),是電腦中的最小存儲單位。

每 8 比特組成一個位元組(byte),一般是電腦實際存儲和處理的最小單位(可以是它的倍數),也就是說,電腦是以位元組為最小單位分配空間或進行計算的,不能分配比位元組更小的存儲空間(如,最小的數據類型是char,長度 1 位元組,不支持申請 6 比特存儲空間)或者直接處理小於位元組單位的數據(如,兩個 4 比特的數據相加減)。

若幹位元組構成一個電腦字(簡稱:字,word),表示電腦一次性處理事務的固定長度二進位數據,字的位數為字長。電腦是以字為單位處理或運算的,兩個常見的概念是CPU位數操作系統位數

CPU 的位數就是指 CPU 執行一次指令能處理的最大位數(一個字長),和 CPU 中的寄存器的位數對應。其中,地址寄存器 MAR 限制了電腦的定址範圍,數據寄存器 MDR 限制了一次處理的數據長度。更多的位數帶來了更大的定址空間和更強的運算能力。

說明:定址範圍不等於記憶體大小,定址對象有記憶體條、顯卡記憶體、音效卡、網卡和其他設備。之所以常把定址範圍當作記憶體上限,是因為記憶體是CPU的主要定址對象。

這裡解釋一下常見的指令架構:x86 是 intel 推出的一種指令集架構(複雜指令集 CISC 架構),一開始只有32位的,叫 x86_32;後來 AMD 公司推出了相容 x86_32 的 64 位指令集 amd64,被業界接受,intel 將其改名為 x86_64,簡稱 x64,而 x86_32 和 x86_64 可統稱為 x86。與 x86 相對的是基於精簡指令集RISC架構的 ARM 指令集架構,多用於移動設備。

操作系統基於 CPU 指令集實現,所以操作系統位數也直接對應 CPU 位數。由於 CPU 指令集的向下相容性,所以 32 位操作系統也可以運行在 64 位的 CPU 上,但反過來不行。操作系統對軟體提供了向下相容的能力,64 位的操作系統支持 64 和 32 位的程式,但 32 位的操作系統只支持 32 位的程式。

處理二進位數據

在大多語言中,最小的數據類型是 char,一個位元組,二進位數據多用 unsigned char 表示,並寫作 uint8。語言底層常把它當作 int 進行運算。

二進位常數以“0b”開頭,如:0b001。二進位數據也常用8進位(以“0”開頭)和 16 進位(以“0x”開頭)表示,如:0257(175,八進位)、0x1f(31,16進位)。8 進位 1 個數字表示 3 位二進位數據,16 進位 1 個數字表示 4 位二進位數據,一個位元組可以用 2 個 16 進位數表示。

若要處理小於一位元組的數據,就要使用位運算符(&、|、^、~、>>、<<)。

位運算符 描述 運算規則 用途
& 兩個位都為1時,結果才為1 二進位位清零或得到指定位數據
| 兩個位都為0時,結果才為0 二進位位設置為1;與對應位為0的數據相加
^ 異或 兩個位相同為0,相異為1 反轉指定位
~ 取反 0變1,1變0 二進位位全部取反
<< 左移 各二進位全部左移若幹位,高位丟棄,低位補0 \(x*2^n\);將數據移到高位
>> 右移 各二進位全部右移若幹位,對無符號數,高位補0,有符號數,各編譯器處理方法不一樣,有的補符號位(算術右移),有的補0(邏輯右移) \(x/2^n\);將數據移到低位

舉個例子,判斷某個位元組的第3位是否是1:

// 先清0其他位,再判斷是否等於0b100
bool isOne = (byte & 0b100) == 0b100;

再舉個例子,電腦網路 IP 協議中的 control flag 和 fragment offset 合起來存儲在 IP 頭部的第 7、8 位元組,flag 占前三位,後 13 位為 fragment offset,可以通過以下運算獲得 flag 和 offset:

// 獲得flag要截取byte7前3位數據:先清空後5位,保留前3位數據,再右移5位將前3位數據移到起始
uint8_t flag = (byte7 & 0b11100000) >> 5;
// 此處以大端存儲,獲得offset要截取byte7的低5位作為高位,byte8作為低位,求和:先清空byte7前3位,保留後5位數據,把它移到高8位上,再通過全0的低8位與byte8按位求或來求二者之和
((byte7 & 0b00011111) << 8) | byte8;

補充說明,當需要多個位元組表示一個數據類型時,需要定義數據的高位位元組是存儲在高位地址空間還是低位地址空間,這就是大小端的定義。大端指高位位元組存在低位地址,這是人的手寫習慣;小端指低位位元組存高位地址。在處理用多個位元組表示的數據時,首先要搞清楚數據是大端還是小端。

所以,我們可以基於上述知識寫一個無符號整形與位元組流相互轉換的通用方法:

// true為大端,低位地址存高位位元組
bool ENDIAN = true;

/**
 * 將data轉換為無符號整形數字(無符號char,short,int,long,long long等)
 * @tparam T 目標類型,預設為 uint32_t
 * @param data 載荷數據 byte數組
 * @param valueSize 數據長度,單位:byte,-1表示根據T類型自動計算
 * @param default_value 預設值,預設為0
 * @return 根據data轉換的無符號整形數據
 */
template<typename T = uint32_t>
T payloadToUnsignedInt(std::vector<uint8_t> data, int valueSize = -1, T default_value = uint32_t(0)) {
    if (valueSize == -1) valueSize = sizeof(T);
    if (valueSize > data.size()) return default_value;
    T value = 0;
    for (int i = 0; i < valueSize; i++) {
        if (ENDIAN) {
            value |= (data[i] & 0xff) << ((valueSize - 1 - i) << 3);
        } else {
            value |= (data[i] & 0xff) << (i << 3);
        }
    }
    return value;
}

/**
 * 無符號整形轉換為載荷 byte數組
 * @param value 無符號整形數據
 * @param valueSize 數據長度,單位:byte,-1表示根據T類型自動計算
 * @return 載荷 byte數組
 */
template<typename T>
std::vector<uint8_t> uintToPayload(T value, int valueSize = -1) {
    if (valueSize == -1) valueSize = sizeof(T);
    std::vector<uint8_t> data(valueSize, 0);
    for (int i = 0; i < valueSize; i++) {
        if (ENDIAN) {
            data[i] = (value >> ((valueSize - 1 - i) << 3)) & 0xff;
        } else {
            data[i] = (value >> (i << 3)) & 0xff;
        }
    }
    return data;
}

封裝二進位數據

掌握了二進位數據的處理方法,接下來就是對二進位數據的封裝,將其封裝為人可以理解的對象。

二進位數據通常以 uint8_t 數組表示,不同位有不同的含義,需要根據實際含義進行解析後得到有意義的目標信息。所以重點就是描述每一位的含義,並基於該描述解析二進位數據,提供二進位數據與有含義的對象的相互轉換。

思路1:基於配置文件

此處以自定義的二進位指令封裝為例進行說明(項目地址),但該配置項目適用於任意二進位數據封裝場景。面對這個需求,首先想到的是通過配置文件描述二進位流每一位的含義,載入配置文件後根據一些過濾條件配置確定當前二進位流段實際對應的配置並解析為字典。

由於項目包括一些嵌入式的內容,需要把所有文件編譯後燒入板子,不支持存儲普通文件格式的配置文件,所以採用變數形式的配置,全局聲明配置的類型信息和配置對象(cmd_manager),項目內任意位置定義該配置對象即可。在其他場景也可選擇 Json、xml 等配置格式。

本文設計的配置對象定義方式如下:

/**
 * 載荷配置項
 */
const CmdManager cmd_manager = { 2, {  // 指令個數,下麵是每一個指令的配置
        {"TCRQ", 3, {  // 配置項名,配置項對應的欄位數
            {"TE_SEQ_NO", -1, &FT_SHORT, 0},  // 具體配置項內欄位配置(欄位名,欄位偏移,欄位類型,配置項該欄位過濾條件
            {"CMD", -1, &FT_CHARS_4, "TCRQ"},  // 配置項要求該欄位等於"TCRQ",數據不滿足則不匹配該配置項
            {"REPEAT_COUNT", -1, &FT_SHORT, 0}}}
}};

項目會自動載入該配置對象,之後針對原始二進位數據通過 PayloadObjectMapFactory 工廠匹配對應配置並生成數據對象,可從數據對象獲得該對象類型(配置項名)並讀寫其中的欄位值。或者指定配置項創建空的數據對象,進行數據設置後獲得其原始二進位數據載荷。

評價:

該思路通過配置文件可以自由且動態的調整解析方式,易於復用、拓展或調整。其難點在於配置格式的設計,同時字典類型數據無法如直接聲明類型結構那樣清晰易用。

思路2:基於數據底層存儲方式

此處以電腦網路數據幀封裝為例進行說明。c++ 底層對對象/結構體的成員欄位採用類型對齊連續存儲方式,使用該特性可以基於實際含義自然聲明、使用欄位,同時可以直接作為二進位數據流處理。實現示例如下:

/**
 * 數據抽象類,提供二進位流到對象的相互轉化能力
 * 內部類,只復用代碼,不用於多態
 * @tparam size 數據位元組長度
 */
template<int size>
class DataType {
public:
    DataType() { resetData(); }
    // 初始化所有數據
    void resetData() const { memset((void *) (this), 0, size); }
    // 從二進位流載入數據
    bool loadData(const std::vector<uint8_t>& data, int startIndex=0) {
        auto * p = (uint8_t *) this;  // 將自身當作二進位數組處理
        for (int i = 0; i < size; i++) {
            *p = data[i + startIndex];
            p++;
        }
        return true;
    }
    // 基於自身生成新的二進位數據流
    [[nodiscard]] std::vector<uint8_t> createData() const {
        std::vector<uint8_t> result;
        auto p = (uint8_t const *) this;
        for (int i = 0; i < size; i++) {
            result.push_back(*p);
            p++;
        }
        return result;
    }
    [[nodiscard]] int getSize() const { return size; }
};

// 以順序聲明方式定義具體的二進位數據類型,支持嵌套聲明
class MACHeader : public DataType<14> {
public:
    // 通過上述無符號整形與位元組流相互轉化的方法將netType的讀寫進行封裝
    [[nodiscard]] uint16_t getNetType() const {
        return payloadToUnsignedInt(std::vector<uint8_t>(netType.begin(), netType.end()), 2, uint16_t(0));
    }
    void setNetType(uint16_t _netType) {
        auto data = uintToPayload(_netType, 2);
        std::copy(data.begin(), data.end(), netType.begin());
    }

    // 提供與json互轉的能力,為了提供映射為python對象的能力
    bool loadJson(const Json::Value& json);
    [[nodiscard]] Json::Value createJson() const;

    std::array<uint8_t, 6> desMac;  // 占多個位元組的數據採用std::array數組描述,可避免類型丟失,同時保證數據類型仍然一致對其
    std::array<uint8_t, 6> srcMac;
    std::array<uint8_t, 2> netType;
};

本項目還需要提供 c++ 的數據幀對象映射到 python 對象的能力,為了簡化 CPython 的拓展方法介面,c++ 層提供從 json 載入或生成 json 的能力,在 python 層實現一個 json 緩存,通過緩存提交和更新實現數據管理。為了致敬git,項目實際提交和更新方法命名為 push 和 pull,(╯▔^▔)╯。

評價:

該思路通過一種類似順序聲明的方式(有點像配置)定義數據流每個位置的實際含義,使用時清晰直接,並巧妙的通過其底層原理便捷的在對象和二進位數據流之間提供轉化操作。但由於其需要實際聲明類型,不如思路1動態靈活易復用。

本文來自博客園,作者:_哲思,轉載請註明原文鏈接:https://www.cnblogs.com/zhe-si/p/16550833.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 三、項目實現讀寫分離 實現方式跟同一個目錄下的01-讀寫分離測試案例基本一致,只不過是將資料庫替換成了項目使用的資料庫 ==同時還有非常重要的一點,ShardingSphere-JDBC的作用不止是讀寫分離,更重要的是其能通過配置文件配置指定演算法,可以自動化的完成對資料庫進行分庫分表操作,且不需要更 ...
  • 一、string 成員函數大全 構造 string()//構造空字元串 string(const char* s);//拷貝s所指向的字元串序列 string(const char* s, size_t n);//拷貝s所指向的字元串序列的第n個到結尾的字元 string(size_t n, cha ...
  • 25 使用Python處理JSON數據 25.1 JSON簡介 25.1.1 什麼是JSON JSON全稱為JavaScript Object Notation,一般翻譯為JS標記,是一種輕量級的數據交換格式。是基於ECMAScript的一個子集,採用完全獨立於編程語言的文本格式來存儲和表示數據。簡 ...
  • 一、簡介Spring Cloud Feign Client 是一個方便的聲明式 REST 客戶端,我們用它來實現微服務之間的通信。 在這個簡短的教程中,我們將展示如何設置自定義的 Feign 客戶端連接超時,包括全局和每個客戶端。 2. 預設值Feign Client 是相當可配置的。 在超時方面, ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • “生產環境伺服器變慢?如何診斷處理” 這是最近一些工作5年以上的粉絲反饋給我的問題,他們去一線大廠面試,都被問到了這一類的問題。 今天給大家分享一下,面試過程中遇到這個問題,我們應該怎麼回答。 這個問題高手部分的回答,我整理到了一個10W字的文檔裡面,大家可以在我的主頁加V領取。 來看看高手的回答。 ...
  • 1、jsp表達式和EL標簽 1.1 獲取值的區別 1.用法el表達式更加簡潔 2.獲取參數不存在時,jsp表達式時null,el表達式是空; <% request.setAttribute("userName", "kh96"); %> <p>獲取作用域中存在的值:userName_jsp = <% ...
  • SpringBoot 2.7.2 學習系列,本節通過實戰內容講解如何集成 MyBatisPlus 本文在前文的基礎上集成 MyBatisPlus,並創建資料庫表,實現一個實體簡單的 CRUD 介面。 MyBatis Plus 在 MyBatis 做了增強,內置了通用的 Mapper,同時也有代碼生成 ...
一周排行
    -Advertisement-
    Play Games
  • JWT(JSON Web Token)是一種用於在網路應用之間傳遞信息的開放標準(RFC 7519)。它使用 JSON 對象在安全可靠的方式下傳遞信息,通常用於身份驗證和信息交換。 在Web API中,JWT通常用於對用戶進行身份驗證和授權。當用戶登錄成功後,伺服器會生成一個Token並返回給客戶端 ...
  • 老周在幾個世紀前曾寫過樹莓派相關的 iOT 水文,之所以沒寫 Nano Framework 相關的內容,是因為那時候這貨還不成熟,可玩性不高。不過,這貨現在已經相對完善,老周都把它用在項目上了——第一個是自製的智能插座,這個某寶上50多塊可以買到,搜“esp32 插座”就能找到。一種是 86 型盒子 ...
  • 引言 上一篇我們創建了一個Sample.Api項目和Sample.Repository,並且帶大家熟悉了一下Moq的概念,這一章我們來實戰一下在xUnit項目使用依賴註入。 Xunit.DependencyInjection Xunit.DependencyInjection 是一個用於 xUnit ...
  • 在 Avalonia 中,樣式是定義控制項外觀的一種方式,而控制項主題則是一組樣式和資源,用於定義應用程式的整體外觀和感覺。本文將深入探討這些概念,並提供示例代碼以幫助您更好地理解它們。 樣式是什麼? 樣式是一組屬性,用於定義控制項的外觀。它們可以包括背景色、邊框、字體樣式等。在 Avalonia 中,樣 ...
  • 在處理大型Excel工作簿時,有時候我們需要在工作表中凍結窗格,這樣可以在滾動查看數據的同時保持某些行或列固定不動。凍結窗格可以幫助我們更容易地導航和理解複雜的數據集。相反,當你不需要凍結窗格時,你可能需要解凍它們以獲得完整的視野。 下麵將介紹如何使用免費.NET庫通過C#實現凍結Excel視窗以鎖 ...
  • .NET 部署 IIS 的簡單步驟一: 下載 dotnet-hosting-x.y.z-win.exe ,下載地址:.NET Downloads (Linux, macOS, and Windows) (microsoft.com) .NET 部署 IIS 的簡單步驟二: 選擇對應的版本,點擊進入詳 ...
  • 拓展閱讀 資料庫設計工具-08-概覽 資料庫設計工具-08-powerdesigner 資料庫設計工具-09-mysql workbench 資料庫設計工具-10-dbdesign 資料庫設計工具-11-dbeaver 資料庫設計工具-12-pgmodeler 資料庫設計工具-13-erdplus ...
  • 初識STL STL,(Standard Template Library),即"標準模板庫",由惠普實驗室開發,STL中提供了非常多對信息學奧賽很有用的東西。 vector vetor是STL中的一個容器,可以看作一個不定長的數組,其基本形式為: vector<數據類型> 名字; 如: vector ...
  • 前言 最近自己做了個 Falsk 小項目,在部署上伺服器的時候,發現雖然不乏相關教程,但大多都是將自己項目代碼複製出來,不講核心邏輯,不太簡潔,於是將自己部署的經驗寫成內容分享出來。 uWSGI 簡介 uWSGI: 一種實現了多種協議(包括 uwsgi、http)並能提供伺服器搭建功能的 Pytho ...
  • 1 文本Embedding 將整個文本轉化為實數向量的技術。 Embedding優點是可將離散的詞語或句子轉化為連續的向量,就可用數學方法來處理詞語或句子,捕捉到文本的語義信息,文本和文本的關係信息。 ◉ 優質的Embedding通常會讓語義相似的文本在空間中彼此接近 ◉ 優質的Embedding相 ...