上周体验了一把protobuf,google大佬搞的东西据说很多人用,优点自然不用多说,随便搜搜结果一大堆。为了测试这个玩意,随便弄了一个客户端,拿C#写了一个简单的控制台程序请求服务端,服务端拿java的HttpServer做了一个简单的响应客户端请求。
Protobuf用的2.6.1版本。
客户端下载地址:https://github.com/andyqingliu/TestHttpClient.git
服务端下载地址:https://github.com/andyqingliu/TestHttpServer.git
(Note: 写了很多测试代码,为了测试方便,所以代码比较混乱。)
记录一些重要的信息备忘,顺便梳理一下当时的思路和遇到的几个重要的问题及解决方案。
1.客户端数据准备
a.用C#自带的WebClient类异步发送数据
WebClient wc = new WebClient();
wc.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
wc.Encoding = Encoding.UTF8;
wc.UploadDataCompleted += new UploadDataCompletedEventHandler(OnUploadDataCompleted);
wc.UploadDataAsync(uri, "POST", buff);
b.利用MemoryStream类来存放要发送给服务端的字节流。
原来MemoryStream有ToArray的方法直接把字节流转换为字节数组。刚开始自己写读取流方法一直有问题,写不了数据,后来请教同事发现有现成的方法,绕了一大圈。同样,MemoryStream也有字节数组的构造函数,很是方便。
c.客户端协议如下表.
客户端协议格式
协议第一部分 | 协议第二部分 | 协议第三部分 |
---|---|---|
4字节的int,标记协议长度 | 2字节的short,标记协议个数 | N字节的协议内容 |
其实这里更合理的组织方式是第一部分与第二部分对调。这里仅仅测试,写的比较随意。此表只是为了说清楚协议的组织方式。通过与同事讨论,觉得以如下方式组织协议更为合理。
合理的协议组织方式
协议包头 | 协议长度 | 协议内容 | 协议长度 | 协议内容 |
---|---|---|---|---|
2字节short | 4字节int | N字节内容 | 4字节int | N字节内容 |
协议包头定义2字节short来存放协议个数,然后根据协议个数,分别是协议长度和协议内容。
d.Protobuf的C#Api提供了对象的序列化与反序列化方法如下。
ProtoBuf.Serializer.Serialize(stream, T);
ProtoBuf.Serializer.Deserialize<T>(stream);
2.服务端数据准备
a.利用HttpServer来创建服务端监听。
final InetSocketAddress sa = new InetSocketAddress(8888);
HttpServer server = null;
try {
server = HttpServer.create(sa, 0);
} catch (IOException e) {
e.printStackTrace();
}
server.createContext("/",new MyResponseHandler());
server.setExecutor(null);
server.start();
b.通过MyResponseHandler类的handle方法的httpExchange参数的getRequestBody方法来获取客户端的请求的流信息,并进行解析成对应的对象。
c.服务端解析协议方式对应客户端协议格式。
d.Java对Protobuf字节数组处理方式比较蛋疼。
每个协议对象都有一个parseFrom方法来序列化。这一点比较方便,也是蛋疼的地方。
反序列化则是每个对象都会生成一个Protobuf的类型,比较麻烦。
Person.Builder person = Person.newBuilder();
person.setValue(12345);
C2S_GetFriendList_message.Builder build = C2S_GetFriendList_message.newBuilder();
build.setResult(54321);
build.setP(person);
C2S_GetFriendList_message friendList_message = build.build();
————————————————————————————————————————————
一段空白之后遇到了传说中在网络传输过程中的大小端问题。个人理解是这样的:网络协议规定低内存地址存放高字节,高内存地址存放低字节。不同处理器处理字节的方式各有不同,X86处理器以小端方式处理字节序列,发送字节数组,即低内存地址存放低字节,高内存地址存放高字节。而java虚拟机则以大端的顺序来存放。即低内存地址存放高字节,高内存地址存放高字节。
举例说明,比如有一个int = 129,其转换为字节数组为{129,0,0,0},一个大小为4的字节数组,假如有一段内存地址,从左到右内存地址值变大,字节顺序是这样:
字节序列
0x0643E690 | 0x0643E691 | 0x0643E692 | 0x0643E693 |
---|---|---|---|
129 | 0 | 0 | 0 |
对应的二进制为 00000000 00000000 00000000 10000001 .十进制为129.
而对于java虚拟机,则会把上述字节数组翻译为“大端”,其在内存中的字节序列如下:
Java虚拟机内存字节序
0x0643E690 | 0x0643E691 | 0x0643E692 | 0x0643E693 |
---|---|---|---|
0 | 0 | 0 | -127 |
对应的二进制为 10000001 00000000 00000000 00000000. 由于Java没有无符号数,最高位代表符号位,这里的二进制最高位为1,代表负数,而Java虚拟机用补码表示负数,所以此数的绝对值代表的二进制为:01111111 00000000 00000000 00000000.加上符号,转换为十进制为-2130706432。
于是,如果没有对大小端进行统一,就会出现发送方的数据与接收方的数据不一致的问题。发送129,收到的却是-2130706432。
解决办法是:需要在客户端进行转换,可以用 IPAddress.HostToNetworkOrder把需要发送的字节序列转换为网络字节序列,即转换为大端序列。代码如下:
int testInt = 129;
byte[] ints = System.BitConverter.GetBytes(IPAddress.HostToNetworkOrder(testInt));
然后在服务端也进行转换,转换为大端序列,如下:
ByteBuffer bbBuffer = ByteBuffer.wrap(bs);
bbBuffer.order(ByteOrder.BIG_ENDIAN);
这样就能保证发送方与接收方都采用大端的方式,避免得到不想要的结果。
可能有些同学会问一个问题,为什么c#发送的字节数组是{129,0,0,0},而到了java端变成了{-127,0,0,0},这是因为c#的byte是无符号的8位字节,而java端的byte是有符号的,对于c#,byte的取值范围是(0,255),而Java端的byte取值范围是(-128,127),129的二进制为10000001 ,对于c#而言,对应的十进制是129,而对于java而言,最高位为1,表示负数,负数用补码来表示,所以其绝对值的二进制为011111111,十进制为127,所以java端认为这个值是-127.