使用C#處理基於比特流的數據

来源:http://www.cnblogs.com/durow/archive/2016/10/13/5957306.html
-Advertisement-
Play Games

使用C#處理基於比特流的數據 0x00 起因 最近需要處理一些基於比特流的數據,電腦處理數據一般都是以byte(8bit)為單位的,使用BinaryReader讀取的數據也是如此,即使讀取bool型也是一個byte。不過藉助於C#基礎類庫中提供的一些方法,也實現了對基於比特的數據的讀取。任務完成後 ...


使用C#處理基於比特流的數據

 

0x00 起因

最近需要處理一些基於比特流的數據,電腦處理數據一般都是以byte(8bit)為單位的,使用BinaryReader讀取的數據也是如此,即使讀取bool型也是一個byte。不過藉助於C#基礎類庫中提供的一些方法,也實現了對基於比特的數據的讀取。任務完成後覺得基於比特的數據挺有意思,自己試了下用7比特和6比特編碼常用ASCII字元。最後把一點新的寫成博客,一方面做個記錄,另一方面希望對有類似需求的園友有所幫助。

0x01 比特流數據的讀取

假設我們有一個byte b = 35,而我們需要把其中的前4bit和後4bit分別讀取為兩個數字,那麼應該怎麼做呢。雖然沒有在基礎類庫中找到現成的方法,但用二進位字元串中轉一下,分兩步也可以做到。

1、先把b表示為二進位字元串00100011

2、分別取其前後4bit轉為數字,核心方法就是:

Convert.ToInt32("0010");

這樣就實現了基於比特的數據讀取了。

關於第一步中把byte轉化為二進位字元串有很多種方法,

1、最簡單的Convert.ToString(b,2)。不夠8位就在高位用0補足。

2、也可以把byte分別與1,2,4,8 … 128做與運算,由低到高取出各位。

3、也可以把byte和32做與運算,然後把byte左移再次與128做與運算。

其中第一種方法會產生大量的字元串對象,在第2、3種方法沒有找到太大區別,我選擇的3,純靠感覺。代碼如下:

public static char[] ByteToBinString(byte b)
{
  var result = new char[8];
  for (int i = 0; i < 8; i++)
  {
    var temp = b & 128;
    result[i] = temp == 0 ? '0' : '1';
    b = (byte)(b << 1);
  }
  return result; }

為了能將byte[]轉化為二進位字元串,可以

Public string BitReader(byte[] data)
{
    BinString = new StringBuilder(data.Length * 8);
    for (int i = 0; i < data.Length; 
    {
         BinString.Append(ByteToBinString(data[i]));
    }
    return BinString.ToString();
}    

這樣一來當拿到byte[]數據時,可以轉換為二進位字元串保存起來,根據偏移的bit位置和bit長度從中讀取二進位字元串,並保轉換為bool,Int16,Int32等。基於這個思路,可以寫一個BitReader類,其中用StringBuilder存儲二進位字元串,並提供Read方法從二進位字元串中讀取數據。為了能夠更好的處理數據流,在此基礎上添加一個Position記錄當前偏移,當使用某些Read方法讀取數據時,Position也會相應移動。例如使用ReadInt16讀取數據,BitReader會從Position當前位置,讀取16bit並轉換為Int16返回,同時Position向後移動16bit。區分方式就是當讀取數據時需要指定起始的偏移位置時,Position不移動,直接從當前Position讀取時Position移動,BitReader類部分代碼如下:

 1 public class BitReader
 2 {
 3     public readonly StringBuilder BinString;
 4     public int Position { get; set; }
 5 
 6     public BitReader(byte[] data)
 7     {
 8         BinString = new StringBuilder(data.Length * 8);
 9         for (int i = 0; i < data.Length; i++)
10         {
11             BinString.Append(ByteToBinString(data[i]));
12         }
13         Position = 0;
14     }
15 
16     public byte ReadByte(int offset)
17     {
18         var bin = BinString.ToString(offset, 8);
19         return Convert.ToByte(bin, 2);
20     }
21 
22     public byte ReadByte()
23     {
24         var result = ReadByte(Position);
25         Position += 8;
26         return result;
27     }
28 
29     public int ReadInt(int offset, int bitLength)
30     {
31         var bin = BinString.ToString(offset, bitLength);
32         return Convert.ToInt32(bin, 2);
33     }
34 
35     public int ReadInt(int bitLength)
36     {
37         var result = ReadInt(Position, bitLength);
38         Position += bitLength;
39         return result;
40     }
41 
42     public static char[] ByteToBinString(byte b)
43     {
44         var result = new char[8];
45         for (int i = 0; i < 8; i++)
46         {
47             var temp = b & 128;
48             result[i] = temp == 0 ? '0' : '1';
49             b = (byte)(b << 1);
50         }
51         return result;
52      }
53 }
View Code

使用BitReader按照4bit從byte[] buff= {35,12};中讀取數據可以這樣:

var reader = new BitReader(buff); //二進位字元串為0010001100001100

var num1 = reader.ReadInt(4);   //從當前Position讀取4bit為int,Position移動4bit,結果為2,當前Position=4

var num2 = reader.ReadInt(5,6);  //從偏移為5bit的位置讀取6bit為int,Position不移動,結果為48,當前Position=4

var b = reader.ReadBool();  //從當前Position讀取1bit為bool,Position移動1bit,結果為False,當前Position=5

0x02 比特流數據的寫入

把數據寫入比特流就是一個相反的過程,我們用BitWriter類實現,在其中存儲StringBuilder保存二進位字元串,當寫入數據時,需要傳入數據並指定保存這個數據所需要的bit數。當寫入完畢後可以將StringBuilder中保存的二進位字元串按照8bit轉換為byte[]並返回。BitWriter的核心部分如下:

 1 public class BitWriter
 2 {
 3     public readonly StringBuilder BinString;
 4 
 5     public BitWriter()
 6     {
 7         BinString = new StringBuilder();
 8     }
 9 
10     public BitWriter(int bitLength)
11     {
12         var add = 8 - bitLength % 8;
13         BinString = new StringBuilder(bitLength + add);
14     }
15 
16     public void WriteByte(byte b, int bitLength=8)
17     {
18         var bin = Convert.ToString(b, 2);
19         AppendBinString(bin, bitLength);
20     }
21 
22     public void WriteInt(int i, int bitLength)
23     {
24         var bin = Convert.ToString(i, 2);
25         AppendBinString(bin, bitLength);
26     }
27 
28     public void WriteChar7(char c)
29     {
30         var b = Convert.ToByte(c);
31         var bin = Convert.ToString(b, 2);
32         AppendBinString(bin, 7);
33     }
34 
35     public byte[] GetBytes()
36     {
37         Check8();
38         var len = BinString.Length / 8;
39         var result = new byte[len];
40 
41         for (int i = 0; i < len; i++)
42         {
43             var bits = BinString.ToString(i * 8, 8);
44             result[i] = Convert.ToByte(bits, 2);
45         }
46 
47         return result;
48     }
49 
50     public string GetBinString()
51     {
52         Check8();
53         return BinString.ToString();
54     }
55 
56 
57     private void AppendBinString(string bin, int bitLength)
58     {
59         if (bin.Length > bitLength)
60             throw new Exception("len is too short");
61         var add = bitLength - bin.Length;
62         for (int i = 0; i < add; i++)
63         {
64             BinString.Append('0');
65         }
66         BinString.Append(bin);
67     }
68 
69     private void Check8()
70     {
71         var add = 8 - BinString.Length % 8;
72         for (int i = 0; i < add; i++)
73         {
74             BinString.Append("0");
75         }
76     }
77 }
View Code

下麵舉個簡單的例子:

var writer = new BitWriter();

writer.Write(12,5);  //把12用5bit寫入,此時二進位字元串為:01100

writer.Write(8,16);  //把8用16bit寫入,此時二進位字元串為:011000000000000001000

var result = writer.GetBytes(); //8bit對齊為011000000000000001000000
                                //返回結果為[96,0,64]

0x03 7比特字元編碼

我們常用的ASCII字元是使用8bit編碼的,但其中真正常用的那些字元只有7bit,最高位為0,所以對於一篇英文文章,我們可以使用7bit重新編碼而不損失信息。編碼的過程就是把文章字元依次取出,並用BitWriter按照7bit寫入,最後獲取新編碼的byte[]。為了能夠正確讀取,我們規定當讀到8bit數據為2時代表數據開始,接下來16bit數據為後面字元個數。代碼如下:

    public byte[] Encode(string text)
    {
        var len = text.Length * 7 + 24;

        var writer = new BitWriter(len);
        writer.WriteByte(2);
        writer.WriteInt(text.Length, 16);

        for (int i = 0; i < text.Length; i++)
        {
            var b = Convert.ToByte(text[i]);
            writer.WriteByte(b, 7);
        }

        return writer.GetBytes();
    }

同樣讀取數據的時候,我們先尋找開始標識符,然後讀出字元個數,根據字元個數依次讀取字元,代碼如下:

    public string Decode(byte[] data)
    {
        var reader = new BitReader(data);
        while (reader.Remain > 8)
        {
            var start = reader.ReadByte();
            if (start == 2)
                break;
        }
        var len = reader.ReadInt(16);
        var result = new StringBuilder(len);
        for (int i = 0; i < len; i++)
        {
            var b = reader.ReadInt(7);
            var ch = Convert.ToChar(b);
            result.Append(ch);
        }

        return result.ToString();
    }

由於數據頭的存在,當編碼幾個字元時編碼後數據反而更長了

 

不過隨著字元越多,編碼後節省的越多。

 

0x04 6比特字元編碼

從節省數據量的角度,如果允許損失部分信息,例如損失掉字母大小寫,是可以進一步減少編碼所需比特數的。26個字母+10個數字+符號,可以用6bit(64)進行編碼。不過使用這種編碼方式就不能用ASCII的映射方式了,我們可以自定義映射,例如0-10映射為十個數字等等,也可以使用自定義的字典,也就是傳說中的密碼本。經常看國產諜戰片的應該都知道密碼本吧,密碼本就是一個字典,把字元進行重新映射獲取明文,算是簡單的單碼替代,加密強度很小,在獲取足量數據樣本後基於統計很容易就能破解。下麵我們就嘗試基於自定義字典用6bit重新編碼。

編碼過程:

仍然像7bit編碼那樣寫入消息頭,然後依次取出文本中的字元,從字典中找到對應的數字,把數字按照6bit長度寫入到BitWriter

    public byte[] Encode(string text)
    {
        text = text.ToUpper();
        var len = text.Length * 6 + 24;

        var writer = new BitWriter(len);
        writer.WriteByte(2);
        writer.WriteInt(text.Length, 16);

        for (int i = 0; i < text.Length; i++)
        {
            var index = GetChar6Index(text[i]);
            writer.WriteInt(index, 6);
        }

        return writer.GetBytes();

    }

    private int GetChar6Index(char c)
    {
        for (int i = 0; i < 64; i++)
        {
            if (Dict.Custom[i] == c)
                return i;
        }
        return 10; //return *
    }

解碼過程:

解碼也很簡單,找到消息頭,依次按照6bit讀取數據,並從字典中找到對應的字元:

public string Decode(byte[] data)
{
    var reader = new BitReader(data);
    while(reader.Remain > 8)
    {
        var start = reader.ReadByte();
        if (start == 2)
            break;
    }
    var len = reader.ReadInt(16);
    var result = new StringBuilder(len);
    for (int i = 0; i < len; i++)
    {
        var index = reader.ReadInt(6);
        var ch = Dict.Custom[index];
        result.Append(ch);
    }

    return result.ToString();
}

同樣一段文本用6bit自定義字典編碼後數據長度更短了,不過損失了大小寫和換行等格式。

如果從加密的角度考慮,可以設置N個自定義字典(假設10個),在消息頭中用M bit(例如4bit)表示所用的字典。這樣在每次編碼時隨機選擇一個字典編碼,解碼時根據4bit數據選擇相應字典解碼,並且定時更換字典可以增大破解難度。感興趣的園友可以自行嘗試。

0x05 寫在最後

以上是我處理比特流數據的一點心得,僅僅是我自己能想到的一種方法,滿足了我的需求。如果有更效率的更合理的方法,希望賜教。另外編碼和解碼的兩個例子是出於有趣寫著玩的,在實際中估計也用不到。畢竟現在帶寬這麼富裕,數據加密也有N種可靠的多的方式。

示例代碼:https://github.com/durow/TestArea/tree/master/BitStream


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

-Advertisement-
Play Games
更多相關文章
  • truncate table page_frame_mst; select setval('page_frame_mst_id_seq', 1, false); select setval('image_group_mst_id_seq', (select max(id) from image_gr ...
  • 最近接手些mysql資料庫維護,發現mysql在批量操作方面就是個渣渣啊,比起MS SQL SERVER簡直就是“不可同日而語”。 咨詢了下MySQL的高手,對於數據遷移這種問題,一種處理方式就是直接“一步到位” ,一次性將所有數據查詢插入到另外一個表,然後再刪除原表數據;另外一種處理方式就是使用p ...
  • 最近做項目在部署到阿裡雲伺服器上之後出現了兩個問題: 1、亂碼問題。 2、ajax的php處理頁面裡面利用json_encode()函數返回json數據,則資料庫返回的數據只能是UTF8,如果是gbk則json也無法返回。 發現是資料庫編碼格式問題,網站使用的編碼格式為UTF8,資料庫的編碼格式調為 ...
  • 下麵,主要是驗證在MySQL主從複製環境下,存儲過程,函數,觸發器,事件的複製情況,這些確實會讓人混淆。 首先,創建一張測試表 存儲過程 創建存儲過程 通過查看二進位日誌,可以看到該DDL語句已被記錄 執行存儲過程 查看二進位日誌中,記錄的是還是call p1('tom',10)操作記錄對應的SQL ...
  • 1.出錯結果:資料庫表視圖有多條數據,在使用EF框架進行查詢時卻只得到一條數據(註:攔截EF得到的sql語句在資料庫進行查詢並沒有任務問題)。 2.出錯原因:該視圖中沒有ID或者主鍵,EF查詢時進行反射預設都是同一條數據。 3.總結:EF框架查詢視圖時需要註意加入ID或者主鍵 以上僅是個人簡單分析。 ...
  • 求迷宮從入口到出口的所有路徑是一個經典的程式設計問題,求解迷宮,通常採用的是“窮舉+回溯”的思想,即從入口開始,順著某一個方向出發,若能夠走通,就繼續往前走;若不能走通,則退回原路,換一個方向繼續向前探索,直到所有的通路都探尋為止。因此本文依據這種“窮舉+回溯”的思想,設計一個求解迷宮的程式。 1 ...
  • 當初說這個需求的時候,在網上找了一點資料,但是基本上感覺不符合項目中的需求。參照牛人的項目,和同事的改造,終於是像點樣子了 截圖大致截為3個像素,每個像素使用的地方也不同,考慮圖片不會是很多,分別壓縮保存下來。 根據截取的像素位置,對應的壓縮成相應的圖片: 首先需要下載Jcrop.js與upload ...
  • 最近寫代碼,遇到一個問題,微軟基於List<T>自帶的方法是public bool Remove(T item);,可是有時候我們可能會用到諸如RemoveAll<IEnumerable<T>>的方法,坦白的說,就是傳入的參數是一個IEnumerable<T>,而不是一個T,這種情景是隨時可能用到的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...