C#与Java通过protobuf进行网络通信过程中遇到的问题

发表于2018-10-17
评论0 2.6k浏览
关于protobuf的概念这里就介绍了,使用protobuf的有点有很多,网上这方面的资料也不少。为了测试这个玩意,随便弄了一个客户端,拿C#写了一个简单的控制台程序请求服务端,服务端拿java的HttpServer做了一个简单的响应客户端请求。

Protobuf用的2.6.1版本。  
客户端下载地址:https://github.com/andyqingliu/TestHttpClient.git
服务端下载地址:https://github.com/andyqingliu/TestHttpServer.git
(Note: 写了很多测试代码,为了测试方便,所以代码比较混乱。)

记录一些重要的信息备忘,顺便梳理一下当时使用C#与Java通过protobuf进行网络通信过程思路和遇到的几个重要的问题及解决方案。

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字节short4字节intN字节内容4字节intN字节内容

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的字节数组,假如有一段内存地址,从左到右内存地址值变大,字节顺序是这样:

字节序列
0x0643E6900x0643E6910x0643E6920x0643E693
129000
对应的二进制为 00000000 00000000 00000000 10000001.十进制为129.

而对于java虚拟机,则会把上述字节数组翻译为“大端”,其在内存中的字节序列如下:
Java虚拟机内存字节序
0x0643E6900x0643E6910x0643E6920x0643E693
000-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.
来自:https://blog.csdn.net/andyqingliu/article/details/53997021

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引