一起谈.NET技术,用c#实现Protocol Buffers的变长字节整形编码

  摘要

  int在.net里固定占4个字节,如果我们存储和传输大量的int数据,并且大部分数的值比较小,我们就会浪费很多的网络流量和磁盘存储。Protocol Buffers对整数的编码是让值小的数占少量几个的字节,值大的数占多个字节。

  编码算法

  首先看如下链接,了解Protocol Buffers对整形的编码算法。http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/encoding.html

  它举了个对300的编码,编码后是两个字节:


1010 1100 0000 0010

  它这个例子是每个字节左边是高位,可以看到每个字节的最高位是一个标识位,从左到右第一个字节是10101100,最高位是1,说明后面还有字节需要解码,然后第二个字节是00000010,最高位是0,后面没字节了。所以这两个字节就需要解码成一个整数,再往下看。

代码


1010 1100 0000 0010
→ 010 1100 000 0010 //每个字节去掉最高位
→ 000 0010 010 1100 //字节序转换,两个字节互换位置
→ 000 0010 ++ 010 1100 //两个字节进行链接操作(不是相加)
→ 100101100 //合并后的结果,高位位0的部分截取掉
→ 256 + 32 + 8 + 4 = 300 //每个值为1的bit位乘以以2为底的幂,得出编码后的值

  这个例子是以一个正数为例的,负数也是一样的,不过以补码表示负数的话,有个符号位,而且高位都是1,编码比较复杂,它没有给举例。不过后面咱们用c#实现的变字节编码是支持对负数进行编码的,只是对负数的编码没有压缩的效果。

  c#的的二进制基本操作

  我们先来熟悉下c#的二进制的位操作,位了查看方便,我们先写一个按二进制打印字节数组的辅助函数:  

代码


static string ByteToString(byte[] bytes)
{
BitArray array = new BitArray(bytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.Length; i++)
{
sb.Append(array[i] ? '1' : '0');
if ((i + 1) % 8 == 0)
sb.Append(' ');
}
return sb.ToString();
}

  来测试查看几个常见的位操作,具体情况详见注释:

代码


private static void StudyBitOperation()
{
//16进制换算成二进制,每个16进制位转换成4个二进制位
//高位的3是0011,低位的2是0010,因为.net下是小字节序
//所以0x32的二进制表示是01001100
byte[] temp = new byte[] { 0x32 };
Console.Write("0x32的原始二进制表示:");
Console.WriteLine(ByteToString(temp));

//左移运算符 (<<) 将第一个操作数向左移动第二个操作数指定的位数。
//第一个操作数的高序位被放弃,低序空位用 0 填充。移位操作从不导致溢出。
//01001100左移3位应该是00001001
temp[0] <<= 3;
Console.Write("左移3位表示:");
Console.WriteLine(ByteToString(temp));

//00001001右移2位应该是01001000
temp[0] >>= 3;
Console.Write("右移2位表示:");
Console.WriteLine(ByteToString(temp));

//判断高位第4位是否为1,0x80二进制表示是00000001
//右移3位是00001000,高位(从右向左以1开始数)第4位是1
//如果要检查的数高位第4位是1的话,那么两个数按位与后
//得出的数就只有这位是1,也就是还是00001000
int checkInt = 0x80 >> 3;
Console.WriteLine("高位第4位是:{0}", (temp[0] & checkInt) == checkInt);

//把高位第4位设置为0,0x80右移3位是00001000
//取反后是11110111,和原始字节01001000按位与后
//就是01000000
checkInt = ~(0x80 >> 3);
temp[0] &= (byte)checkInt;
Console.Write("把高位第4位设置为0:");
Console.WriteLine(ByteToString(temp));

//把高位第3位设置为1,0x80右移2位是00000100
//和原始字节01000000按位或后就是01000100
checkInt = 0x80 >> 2;
temp[0] |= (byte)checkInt;
Console.Write("把高位第3位设置为1:");
Console.WriteLine(ByteToString(temp));
}

  输出结果:


0x32的原始二进制表示:01001100
左移3位表示:00001001
右移2位表示:01001000
高位第4位是:True
把高位第4位设置为0:01000000
把高位第3位设置为1:01000100

  实现编码及单元测试

  响应刀总号召,先写单元测试,我们设计了GetBytesFromInt和GetIntFromBytes分别用来对int数据进行编码和解码,然后单元测试里对各种整数进行编码,然后解码查看是否还是原始数据。

代码


private static void UnitTest()
{
UnitTest(300);
UnitTest(-1);
UnitTest(int.MaxValue);
UnitTest(255);
UnitTest(1024);
UnitTest(12234234);
UnitTest(-12234234);
}

private static void UnitTest(int input)
{
byte[] bytes = GetBytesFromInt(input);
if (_debug)
Console.WriteLine(ByteToString(bytes));

int result = GetIntFromBytes(bytes);
System.Diagnostics.Debug.Assert(input == result);

if (_debug)
Console.WriteLine(result);
}

  代码实现,详细算法见注释:

代码


static byte[] GetBytesFromInt(int input)
{
//1、得到int的原始字节,.net的整数的二进制表示高位在右边
byte[] bytes = BitConverter.GetBytes(input);
BitArray array = new BitArray(bytes);
if (_debug)
Console.WriteLine(ByteToString(bytes));

//2、得到多字节编码的字节数,并初始化输出数组
//原始二进制的每7位要编码到一个字节里,根据这个信息
//计算出需要多少个字节编码,因为高位在右边,我们就
//从右往左数,找到第一个为1的二进制位,从这位往
//左数就都是有效数据位了。
int firstOneIndex = 0;
for (firstOneIndex = array.Length - 1; firstOneIndex >= 0; firstOneIndex--)
{
if (array[firstOneIndex])
break;
}
int resultBytesLength = (int)Math.Ceiling((double)(firstOneIndex + 1) / 7);
byte[] resultBytes = new byte[resultBytesLength];

//3、设置多字节编码中的每一位,
//每个字节的低7位表示数据位,最高位如果是1表示后面还有数据需要解码
for (int i = 0; i <= firstOneIndex; i++)
{
//3.1、计算出本次要编码的目标字节
int byteindex = i / 7;
byte b = resultBytes[byteindex];

//3.2、设置本次编码字节的每一位
int checkInt = i % 7;
b |= array[i] ? (byte)(0x01 << checkInt) : (byte)0;

//3.3、设置最高位,如果后面还有字节编码则为1,否则为0
if (byteindex < resultBytes.Length - 1)
b |= 0x80;

resultBytes[byteindex] = b;
}

return resultBytes;
}

static int GetIntFromBytes(byte[] input)
{
//1、初始化目标整数的字节数组,
//因为编码数据里有冗余数据,所以要多出一个字节来
byte[] result = new byte[5];
for (int i = 0; i < input.Length; i++)
{
//2、先把多字节编码的每个字节拷贝到目标整数字节中
//并完成移位,移位是因为多字节编码每字节是7个有效
//数据位,而.NET的整数的二进制表示是每个字节8个有效
//数据位,这样目标字节填充几次后,高位就会空出多个
//位的无效数据
result[i] = input[i];
result[i] >>= i;

//3、从下一个字节的最低位拷贝N位到本次编码的最高位
//因为本字节右移了i位,所以要从下一个字节拷贝i位数据
//到本字节
for (int j = 0; j <= i; j++)
{
//3.1、得到下一个字节要拷贝的位
int checkInt = 0x80 >> (7 - j);
if (i + 1 < input.Length)
{
//3.2、如果要拷贝的位是1,则设置本字节相应的位为1
if ((input[i + 1] & checkInt) == checkInt)
{
checkInt = 0x80 >> (i - j);
result[i] |= (byte)checkInt;
}
else //3.3、如果要拷贝的位是0,则设置本字节相应的位为1
{
checkInt = ~(0x80 >> (i - j));
result[i] &= (byte)checkInt;
}
}
}
}

if (_debug)
Console.WriteLine(ByteToString(result));

return BitConverter.ToInt32(result, 0);
}

  单元测试均通过。

  存储测试

  我们假设要编码的数据都是几万,几千的值,只有很少是大于ushort.max的,随机生成一些整数,用变长字节编码看看是否能起到压缩的效果。

代码


private static void StoreTest()
{
Random rnd = new Random();
const int MAX = 1000;
int sum = 0;
for (int i = 0; i < MAX; i++)
{
sum += GetBytesFromInt(rnd.Next(0, ushort.MaxValue)).Length;
}
Console.WriteLine(".net编码{0}个int需要{1}字节", MAX, MAX * 4);
Console.WriteLine("Protocol Buffer编码{0}个int需要{1}字节", MAX, sum);
Console.WriteLine("节省了{0:p}的存储空间", 1 - (double)sum / (MAX * 4));
}

  测试输出:


.net编码1000个int需要4000字节
Protocol Buffer编码1000个int需要2787字节
节省了30.33%的存储空间

  当然这个测试数据并不具有代表性,如果要编码的整数大多都是负数,或者很大的正数,那么这种编码不仅不会比原始的int编码小,反而会比.net对整形的定长编码要大。

  小节

  计算机组成原理讲的知识离我们并不遥远,没准什么时候有个需求来了,你就得用到这些知识。

时间: 2024-09-26 20:27:00

一起谈.NET技术,用c#实现Protocol Buffers的变长字节整形编码的相关文章

用c#实现Protocol Buffers的变长字节整形编码

摘要 int在.net里固定占4个字节,如果我们存储和传输大量的int数据,并且大部分数的值比较小,我们就会浪费很多的网络流量和磁盘存储.Protocol Buffers对整数的编码是让值小的数占少量几个的字节,值大的数占多个字节. 编码算法 首先看如下链接,了解Protocol Buffers对整形的编码算法.http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/encoding.html 它举了个对300的编码,编码后是两个字

MessagePack, Protocol Buffers和Thrift序列化框架原理和比较说明

第1部分 messagepack说明 1.1messagepack的消息编码说明 为什么messagepack比json序列化使用的字节流更少, 可通过图1-1.图1-2有个直观的感觉.   图1- 1 messagepack与json的格式对比1 图1- 2 messagepack与json的格式对比2 messagepack的具体的消息格式如图1-3所示,messagepack的数据类型主要分类两类:固定长度类型和可变长度类型.   图1- 3 messagepack的消息格式 messag

《创业家》牛文文:少谈点模式多谈点技术

"模式"如同当年的"主义",流行于各种创业大赛.创业励志节目.论坛的"街头"式秀场 文/创业家 牛文文 "美国某某公司你知道吧?就是刚被戴尔.惠普.思科十几亿美元抢购的那家.我们的模式和它的一样,现在还没赢利,可将来起码有十几亿人民币的市值." "我开了小煤矿,但煤运不出去,上商学院之后受到启发,想搞模式创新,具体讲就是想在铁路边上搞个煤炭物流开发区,建一个大的物流和信息流平台,把分散的煤炭集中在我这个园区,这样和铁

一起谈.NET技术,Visual C++2010深度体验:Coding是享受

非常高兴有机会在这里跟大家分享和交流关于Visual C++ 2010的一些观点和看法,我希望我的这些展示,能够让你从另外一个角度重新认识Visual C++ 2010,能够让你爱上Visual C++ 2010! Visual C++ 2010深度探索 我们期待已久的Visual Studio 2010已经发布一个月了,相信在这一个月中,大家都已经通过各种途径下载并试用了Visual Studio 2010.我想问问大家,Visual Studio 2010给你的第一感觉是什么? 界面很酷!

一起谈.NET技术,JAVA与.NET的相互调用——通过Web服务实现相互调用

JAVA与.NET是现今世界竞争激烈的两大开发媒体,两者语言有很多相似的地方.而在很多大型的开发项目里面,往往需要使用两种语言进行集成开发.而很多的开发人员都会偏向于其中一种语言,在使用集成开发的时候对另一种语言感觉到畏惧.在这里在下向各位介绍一下,JAVA与.NET相互调用的例子.下面的介绍主要包括三方面:一是通过常用Web服务进行相互调用,二是使用TCP/IP套接字进行相互调用,三是使用Remote实现远程对象相互调用. 在这章里面先为大家介绍一下最简单,最常用的Web服务相互调用方式.首先

使用 Protocol Buffers 代替 JSON 的五个原因 【已翻译100%】

在Ruby和Rails开发者中,面向服务(Service-Oriented)架构有一个当之无愧的名声,它是一个缓解程序规模恶性增长的一个强有力的途径,可在大量应用程序中提取关注点.这些新生小巧的服务通常继续使用Rails或Sinatra,并使用JSON在HTTP上通信.尽管JSON作为一个数据相互交换格式,有很多优点:人类可读.可理解,并通常表现出色. 浏览器和JS并不直接处理数据--尤其是遇到内部服务时.我的观点是,结构化格式,例如谷歌的Protocol Buffers,是一个比JSON在编码

使用 Protocol Buffers 代替 JSON 的五个原因

国内私募机构九鼎控股打造APP,来就送 20元现金领取地址:http://jdb.jiudingcapital.com/phone.html内部邀请码:C8E245J (不写邀请码,没有现金送)国内私募机构九鼎控股打造,九鼎投资是在全国股份转让系统挂牌的公众公司,股票代码为430719,为"中国PE第一股",市值超1000亿元.  ------------------------------------------------------------------------------

Google Protocol Buffers快速入门(带生成C#源码的方法)

Google Protocol Buffers是google出品的一个协议生成工具,特点就是跨平台,效率高,速度快,对我们自己的程序定义和使用私有协议很有帮助. Protocol Buffers入门: 1.去 http://code.google.com/p/protobuf/downloads/list 下载一个源代码包和一个已编译好的二进制包 2.找一个Proto示例代码,使用命令 protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/address

C++程序员Protocol Buffers基础指南

这篇教程提供了一个面向 C++ 程序员关于 protocol buffers 的基础介绍.通过创建一个简单的示例应用程序,它将向我们展示: 在 .proto 文件中定义消息格式 使用 protocol buffer 编译器 使用 C++ protocol buffer API 读写消息 这不是一个关于在 C++ 中使用 protocol buffers 的全面指南.要获取更详细的信息,请参考Protocol Buffer Language Guide 和 Encoding Reference.