비트스트림 기반 데이터를 처리하려면 C#을 사용하세요
0x00 원인
최근 컴퓨터에서 처리하는 데이터는 바이트(8bit) 단위로 처리해야 하는 경우가 있습니다. BinaryReader를 사용하여 읽은 데이터의 경우에도 마찬가지입니다. bool 유형을 읽어도 여전히 바이트입니다. 그러나 C# 기본 클래스 라이브러리에서 제공되는 일부 메서드를 사용하면 비트 기반 데이터도 읽을 수 있습니다. 작업을 마치고 나니 비트 기반의 데이터가 꽤 흥미롭다는 생각이 들어서 일반적인 ASCII 문자를 인코딩하기 위해 7비트와 6비트 인코딩을 사용해 보았습니다. 마지막으로 블로그로 새로운 글을 쓰겠습니다. 한편으로는 기록이 되기도 하고, 한편으로는 비슷한 요구를 갖고 있는 정원사들에게 도움이 되기를 바랍니다.
0x01 비트 스트림 데이터 읽기
바이트 b = 35가 있고 처음 4비트와 마지막 4비트를 각각 두 개의 숫자로 읽어야 한다고 가정하면 어떻게 해야 합니까? 해? 기본 클래스 라이브러리에는 미리 만들어진 메서드가 없지만 이진 문자열을 사용하여 두 단계로 수행할 수 있습니다.
1. 먼저 b를 이진 문자열 00100011
2. 핵심 방법은 다음과 같습니다.
Convert.ToInt32("0010");
작동 방식은 비트 기반 데이터 읽기가 수행됩니다.
첫 번째 단계에서 바이트를 바이너리 문자열로 변환하는 방법은 여러 가지가 있습니다.
1. 가장 간단한 Convert.ToString(b,2). 8비트가 충분하지 않으면 상위 비트에 0을 추가합니다.
2. 각각 1,2,4,8...128로 바이트를 AND하여 낮은 비트에서 높은 비트로 꺼낼 수도 있습니다.
3. 바이트와 32에 AND 연산을 수행한 다음 바이트를 왼쪽으로 이동하고 128에 다시 AND 연산을 수행할 수도 있습니다.
첫 번째 방법은 많은 수의 문자열 객체를 생성합니다. 두 번째와 세 번째 방법에서는 큰 차이를 찾지 못했습니다. 순전히 내 느낌대로 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[] 데이터를 얻을 때 , 바이너리로 변환할 수 있습니다. 문자열이 저장되고 오프셋 비트 위치와 비트 길이에 따라 바이너리 문자열을 읽어 bool, Int16, Int32 등으로 변환합니다. 이 아이디어를 바탕으로 StringBuilder를 사용하여 이진 문자열을 저장하고 이진 문자열에서 데이터를 읽는 Read 메서드를 제공하는 BitReader 클래스를 작성할 수 있습니다. 데이터 흐름을 더 잘 처리하기 위해 현재 오프셋을 기록하기 위해 위치가 추가됩니다. 특정 읽기 메서드를 사용하여 데이터를 읽는 경우 위치도 그에 따라 이동합니다. 예를 들어 ReadInt16을 사용하여 데이터를 읽는 경우 BitReader는 Position의 현재 위치에서 16비트를 읽어 Int16으로 변환하여 반환하고 동시에 Position은 16비트 뒤로 이동합니다. 구별하는 방법은 데이터를 읽을 때 시작 오프셋 위치를 지정해야 하는 경우 위치가 이동하지 않는다는 것입니다. 현재 위치에서 직접 읽을 경우 BitReader 클래스 코드의 일부는 다음과 같습니다.
public class BitReader { public readonly StringBuilder BinString; public int Position { get; set; } public BitReader(byte[] data) { BinString = new StringBuilder(data.Length * 8); for (int i = 0; i < data.Length; i++) { BinString.Append(ByteToBinString(data[i])); } Position = 0; } public byte ReadByte(int offset) { var bin = BinString.ToString(offset, 8); return Convert.ToByte(bin, 2); } public byte ReadByte() { var result = ReadByte(Position); Position += 8; return result; } public int ReadInt(int offset, int bitLength) { var bin = BinString.ToString(offset, bitLength); return Convert.ToInt32(bin, 2); } public int ReadInt(int bitLength) { var result = ReadInt(Position, bitLength); Position += bitLength; return result; } 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; } }
다음과 같이 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가 저장됩니다. 데이터를 쓸 때 데이터를 전달하고 필요한 비트 수를 지정해야 합니다. 이 데이터를 저장하려면. 쓰기가 완료되면 StringBuilder에 저장된 바이너리 문자열을 8bit에 맞게 byte[]로 변환하여 반환할 수 있습니다. BitWriter의 핵심 부분은 다음과 같습니다.
public class BitWriter { public readonly StringBuilder BinString; public BitWriter() { BinString = new StringBuilder(); } public BitWriter(int bitLength) { var add = 8 - bitLength % 8; BinString = new StringBuilder(bitLength + add); } public void WriteByte(byte b, int bitLength=8) { var bin = Convert.ToString(b, 2); AppendBinString(bin, bitLength); } public void WriteInt(int i, int bitLength) { var bin = Convert.ToString(i, 2); AppendBinString(bin, bitLength); } public void WriteChar7(char c) { var b = Convert.ToByte(c); var bin = Convert.ToString(b, 2); AppendBinString(bin, 7); } public byte[] GetBytes() { Check8(); var len = BinString.Length / 8; var result = new byte[len]; for (int i = 0; i < len; i++) { var bits = BinString.ToString(i * 8, 8); result[i] = Convert.ToByte(bits, 2); } return result; } public string GetBinString() { Check8(); return BinString.ToString(); } private void AppendBinString(string bin, int bitLength) { if (bin.Length > bitLength) throw new Exception("len is too short"); var add = bitLength - bin.Length; for (int i = 0; i < add; i++) { BinString.Append('0'); } BinString.Append(bin); } private void Check8() { var add = 8 - BinString.Length % 8; for (int i = 0; i < add; i++) { BinString.Append("0"); } } }
간단한 예는 다음과 같습니다.
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 Encoded를 사용하지만 실제로 많이 사용되는 문자는 7비트에 불과하고, 가장 높은 비트가 0이므로 영문 기사의 경우 7비트를 사용하면 정보 손실 없이 다시 인코딩할 수 있습니다. 인코딩 과정은 기사 문자를 순차적으로 꺼내어 BitWriter를 사용하여 7비트로 작성한 다음 마지막으로 새로 인코딩된 바이트[]를 얻는 것입니다. 올바르게 읽을 수 있도록 8비트 데이터를 2로 읽으면 데이터의 시작을 의미하고 다음 16비트 데이터는 이후의 문자 수라고 규정합니다. 코드는 다음과 같습니다.
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