最近编写一个游戏用到protobuf数据格式进行先后台传输,苦于protobuf接受客户端的数据时是须要数据类型的如xxx.parseForm(…),这样就要求服务器在接受客户端请求时必须知道客户端传递的数据类型。因为客户端的请求数据是多种多样的,服务器端又不知道客户端的请求究竟是哪一个类型,这样就使得服务器端编程带来不少麻烦,甚至步履维艰。难道就没有解决办法了吗,答案固然是有的。下面就说一下经常使用的方法。(在看本文以前建议先了解protobuf的一些基本语法,和基本用法) 1.第一种方法也是最简单的方法,就是在整个应用程序中只定义一个proto文件,那么全部的请求都是一种类型,那么服务器端就不用苦恼怎么解析请求数据了,由于无论哪一个请求数据都用同一个对象解析。以下面的列子: 首先贴一个PBMessage.proto文件 //客户端请求以及服务端响应数据协议 option java_outer_classname = “PBMessageProto”; package com.ppsea.message; import “main/resources/message/DataMsg.proto”; message PBMessage{ optional int32 playerId = 1; //玩家id required int32 actionCode = 2; //操做码id optional bytes data = 5; //提交或响应的数据 optional DataMsg dataMsg = 6; //服务器端推送数据 optional string sessionKey = 7; //请求的校验码 optional int32 sessionId = 8;//当前请求的标示 } 如上述代码,整个应用都基于PBMessage.proto传输,注意到protobuf 语法中 optional修饰符,他表示这个字段是非必须的,也就是说对于客户端的不一样请求,只须要为它填充其请求时用到的字段的值便可,其余的字段的值就不用管了,这样就能够模拟出各类请求来,那么接下来咱们就用: PBMessage.parseForm(byte_PBMesage) // byte_PBMesage表示客户端请求数据 ,这样请求的解析就完成了。同时咱们注意到: (required int32 actionCode = 2; // 操做码id) ,required表示该字段是必须的,前面请求已经解析好了,在这里咱们拿到 actionCode 就能够知道咱们该用哪一个Action事件来处理该请求了(前提是必须维护一张actionCode到Action的映射关系表:Map ),至此整个请求的解析和处理都完成了。 接下来讲一下第一种方式的优缺点,优势:整个应用消息格式一致统一,操做简单。缺点:太统一,就不灵活,对于请求不多,消息格式不多的小型应用倒还勉强能用,消息格式多的话再用这种方式就显得臃肿,不便于管理,失去了程序设计的意义。 2.第二种方法,也是我本次用到的方法。苦于提议中方式的局限性,本人经过在网上收集资料以及查看protobuf java版的源码,发现了一个折中的方式。首先咱们看下protobuf源码中提供的, DynamicMessage 类(顾名思义 动态消息类,眼前一亮有木有),它继承了AbstractMessage类,比较一下和第一种方式建立的PBMessage类的区别 咱们发现PBMessage类继承了GeneratedMessage类,而GeneratedMessage类继承了AbstractMessage类,至此咱们发现了共同类AbstractMessage,再次证实了DynamicMessage 管用,同时咱们再看看AbstractParser 类(在PBMessage类中持有AbstractParser类的对象,并用其来解析请求数据),它继承了Parser 接口,看看其部分方法: public abstract MessageType parseFrom(byte[] paramArrayOfByte) throws InvalidProtocolBufferException; public abstract MessageType parseFrom(InputStream paramInputStream) throws InvalidProtocolBufferException; public abstract MessageType parseFrom(InputStream paramInputStream,ExtensionRegistryLite paramExtensionRegistryLite) throws InvalidProtocolBufferException; ,再看看DynamicMessage 里面提供的方法: public static DynamicMessage parseFrom(Descriptors.Descriptor type,byte[] data) throws InvalidProtocolBufferException { return ((Builder) newBuilder(type).mergeFrom(data)).buildParsed(); } public static DynamicMessage parseFrom(Descriptors.Descriptor type,byte[] data, ExtensionRegistry extensionRegistry)throws InvalidProtocolBufferException { return ((Builder) newBuilder(type).mergeFrom(data, extensionRegistry)).buildParsed(); } public static DynamicMessage parseFrom(Descriptors.Descriptor type,InputStream input) throws IOException { return ((Builder) newBuilder(type).mergeFrom(input)).buildParsed(); } public static DynamicMessage parseFrom(Descriptors.Descriptor type,InputStream input, ExtensionRegistry extensionRegistry)throws IOException { return ((Builder) newBuilder(type).mergeFrom(input, extensionRegistry)).buildParsed(); } 发现了他们方法的类似点,在这里咱们能够用一个等量关系比喻:DynamicMessage=AbstractMessage+ AbstractParser=PBMessage,也就是说DynamicMessage继承AbstractMessage(请求消息对象)的同时又间接实现了AbstractParser(请求消息数据解析)对数据解析的功能。如今咱们惟一缺乏的就是 Descriptors.Descriptor(对消息的描述)对象,这个对象该怎么拿到呢,在这里确定的说,对于不一样的.proto请求这里的Descriptors.Descriptor是不同的。在这里咱们又回到PBMessage对象中,咱们发现了其中有这样一个方法: public final class PBMessageProto { ..................//此处省略若干行public static final class PBMessage extendscom.google.protobuf.GeneratedMessage implements PBMessageOrBuilder { ………..//此处省略若干行 public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { return com.ppsea.message.PBMessageProto.internal_static_com_ppsea_message_PBMessage_descriptor; } } } 这不就是咱们苦苦寻找的东西吗,经过这个方法就能够拿到Descriptor了,不是吗。在这里重点来了,再来理解一下,首先有了 PBMessage对象(这里用其来作表明,可使其余的.proto对象)就能够得到 Descriptors.Descriptor 对象,有了Descriptors.Descriptor对象就能够建立DynamicMessage对象了,有了DynamicMessage就能够解析对应请求了。下面看代码: //存放消息操做码和消息对象 Map descriptorMap=new Map ; //把消息描述对象添加进来 descriptorMap.add(100,PBMessage.getDescriptor()); descriptorMap.add(xxx,xxx); 这样Descriptor有了,其实还能够作得更好一点,经过反射机制,Map里面只存放操做码和对应的proto对象类名,再经过反射方式建立proto对象在得到其getDescriptor()方法。这样就能够在配置文件中配置操做码和proto对象的关系了。 好接下来咱们来接受客户端的请求试一下, //客户端伪代码 client.send(byte_PBMessage); 而后服务器接受请求并解析, //服务器伪代码 byte[] date=server.accept(); //客户端操做码 int actionCode; Descriptor descriptor=descriptorMap.get(actionCode); //解析请求 DynamicMessage req=DynamicMessage.parseFrom(descriptor, date); 在这里咱们发现彷佛还少了点什么,好像 actionCode还不知道,怎么办呢,好吧,咱们在客户端发送的请求消息头上再加上个actionCode,即把操做码和proto消息合并为一个新的请求发送给客户端,请求2位为操做码,那么如今客户端应该这么发送消息了: //客户端伪代码 short actionCode=100; //两个字节来存放actionCode byte [] actionCodeByte=new byte [2]; // 转换成字节流 actionCodeByte.set(actionCode.toByteArray());//伪代码,请勿当真 //带请求头的消息的总长度 int length=actionCodeByte.length+byte_PBMEssage.length; byte [] messageByte=new byte[length]; //把操做码和proto消息合并 messageByte=actionCodeByte+byte_PBMEssage; client.send(messageByte); 下来是服务器了: //服务器伪代码 byte[] data=server.accept(); //把前两位取出来 byte[] actionCodeByte=data.read(0,2); // actionCode有了 int actionCode=actionCodeByte.readShort(); // 取出proto消息 byte[] byte_PBMessage=data.read(2,data.length); …..接下来就和前面的服务器伪代码同样了 DynamicMessage req=DynamicMessage.parseFrom(descriptorMap.get(actionCode, byte_PBMessage); …. 至此动态建立对象完成了,接下来就是按照第一种方式维护的ActionMap经过actionCode取到action来处理DynamicMessage 解析好的请求了 ,固然actionCode也能够换成actionName,相似的。到这里彷佛差很少了,当时始终不完美,由于咱们尚未把DynamicMessage 转换成PBMessage对象,在后续的action里处理DynamicMessage老是不舒服,解决办法是经过DynamicMessage对象得到Descriptor对象,在得到其全部字段名和值, 而后看一下这个地址的这篇文章(经过字段反射对象部分):http://liufei-fir.iteye.com/blog/1160700,经过反射来还原PBMessage,以上是通过试验成功的,因为时间缘由就不把源码贴上来了。有什么问题但愿你们指正。java