评论

收藏

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

游戏开发 游戏开发 发布于:2021-07-17 23:11 | 阅读数:442 | 评论:0

上周体验了一把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.

关注下面的标签,发现更多相似文章