要实现一个decoder解码任意的protoBuf协议,那么必须要有所约定(使得协议带有一定的解释性),这里的解决方案是在协议中带有一个唯一的int值,以找出其对于的协议。大致如下:
【协议内容长度字段】【protoIndex】【protoBuf序列化内容】
看一下详细的约定:
1.首先所有的proto文件导出时,需要导出到同一个包下 。
目的:当我们解析协议时,就可以在固定文件夹搜索(约定1、2、3都是为了方便获得消息类的全限定名)。
例如: server_server.proto server_client.proto 文件中 package 属性都为 com.wjybxx.proto;
2.所有的协议名称都可以以某种方式获得其外部类的名称。
目的:因为同一个.proto文件里面的所有协议在导出后会生成在同一个java文件中。要获得一个消息类的全限定名,必须先获得其外部类的全限定名。使用约定1中的包名,以及约定2,就可以获得其外部类的全限定名。
此外:建议1个proto文件管理一系列相关的协议,比如server_server.proto 文件定义所有服务器通信的协议。而server_client.proto定义所有的客户端与服务器通信的协议。但是一个文件也不要定义太多消息,protoBuf导出的类文件太大,打开类文件的时候会很卡,可以适当的拆分为几个文件。
例如:外部类为: server_client ,那么所有的协议名称为:server_client_xxx (每一个协议名都包含其外部类名称,并以下划线"分隔)。如图:
3.所有的protoBuf协议拼写都保持小写,ProtoBufEnum由完整的protoBuf协议名称转大写生成。
目的:通过枚举名可以方便的定位到对象的消息名,联合 约定1和约定2 以方便获取消息的全局限定名和int值。
例如:SERVER_CLIENT_XXX (这样就可以通过协议的枚举名称找到对应的proto协议,并获得他的parser),如上图。
例外:如果你的枚举名 就是 消息名,那么这个约定可以没有。
4.每一个枚举(协议)都对应一个唯一的int值,且通信的双方保持一致性。(重要)
注意:并不建议使用ordinal()方法的返回值作为索引,协议在开发过程中经常会变动,ordinal()方法可能导致双方协议映射不一致,进而导致通信异常!你应当自定义1个int字段作为索引,而不是使用ordinal()返回参数。
4.1也可以以keyValue的形式写在某个文件中,通信双方在启动时建立好索引(记得做好重复检测))。
4.2也可以做一个工具,为通信双方的协议生成枚举(这种情况一般是两种不同的语言)。
这里没有示例:因为博主偷懒,直接使用的ordinal(),但是并不建议你们使用。
protoBuf版本:3.5.1 (2和3有一定的差别,在获取parser时需要注意) protoc工具: 3.5.1
netty版本:4.1.23.Final
先上demo下载地址:demo下载地址(5积分)
//=============================完整示例开始============================
首先看一下咱们的ProtoBufEnum枚举类:
它有两个重要的功能:
1.通过消息名获取它的枚举/int值。
2.通过int值获取它的解析器(parser)。
package netty.protobufcodec; import com.google.protobuf.MessageLite; import com.google.protobuf.Parser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; //这些消息来自于两个proto文件 public enum ProtoBufEnum { /** * 第一个测试协议(大写,转换为小写得到它的类名称,前两个字段获得它的外部类名称,加上包路径,因而找到它的全限定名) */ SERVER_CLIENT_FIRST_MESSAGE, /** * 客户端发给服务器的PING包 */ SERVER_CLIENT_PING, /** * 客户端发给服务器的请求 */ SERVER_CLIENT_ONE_REQUEST, /** * 服务器的请求处理结果 */ SERVER_CLIENT_ONE_REQUEST_RESULT, /** * 服务器之间的第一个消息 */ SERVER_SERVER_FIRST_MESSAGE, /** * 服务器之间的PING包 */ SERVER_SERVER_PING, ; private static final Logger logger= LoggerFactory.getLogger(ProtoBufEnum.class); private static ProtoBufEnum[] values= ProtoBufEnum.values(); /** * 这里采用的懒汉式管理,所以使用threadLocal, * 采用饿汉式的话,可以在类初始化的时候,直接索引好所有的映射,成为线程安全的不可变对象。 */ private static final ThreadLocal<ProtoParser> threadLocalParser=new ThreadLocal<ProtoParser>(){ @Override protected ProtoParser initialValue() { return new ProtoParser(); } }; /** * 通过protoIndex获得它的消息解析器 * @param protoIndex * @return 若protoIndex无对应的parser,则返回null */ public static Parser parserOfProtoIndex(final int protoIndex){ return threadLocalParser.get().getParser(protoIndex); } /** * 通过消息获取它的索引 * @param messageLite * @return 若不存在对应的索引,返回 -1,存在对应的索引,则返回 [0,?) */ public static int protoIndexOfMessage(final MessageLite messageLite) throws UnsupportedOperationException{ return threadLocalParser.get().getProtoIndex(messageLite); } private static class ProtoParser{ private final Parser[] parsersArray=new Parser[values.length]; private final Map<Class,ProtoBufEnum> messageLiteToEnumMap=new HashMap<>(); /** * protoBuf文件导出的java包路径 */ private final String protoBufPackagePath; private ProtoParser() { //TODO 这里我只是简单的写一下,大家可以根据情况优化,读取配置或者别的方式 protoBufPackagePath ="com.wjybxx.proto"; } /** * 通过协议索引 获得它的解析器 * 这里使用ordinal(),仅仅是为了方便,做一个展示,说明一下大致的思路。而你不应该这么做 * @param protoIndex * @return */ private Parser getParser(final int protoIndex){ try { if (parsersArray[protoIndex]!=null){ return parsersArray[protoIndex]; } ProtoBufEnum protoBufEnum =values[protoIndex]; String innerClassName = getInnerClassName(protoBufEnum); String outerClassName = getOuterClassName(innerClassName); String className= protoBufPackagePath +"."+outerClassName + "$" + innerClassName; Class messageClass=Class.forName(className); //proto2 PARSER 字段是pubic,而proto3是private,在获取parser是有一定差异 // Parser parser= (Parser)=messageClass.getField("PARSER").get(null);//PROTO2 Parser parser= (Parser) messageClass.getMethod("parser").invoke(null);//PROTO3 parsersArray[protoIndex]=parser; return parser; }catch (Exception e){ logger.info("",e); return null; } } /** * 通过枚举获取内部类名称 * @param protoBufEnum * @return */ private String getInnerClassName(ProtoBufEnum protoBufEnum) { return protoBufEnum.name().toLowerCase(); } /** * 通过内部类名称获得外部类名称 * @param innerClassName * @return */ private String getOuterClassName(String innerClassName){ String[] temp=innerClassName.split("_"); return temp[0]+"_"+temp[1]; } /** * 通过消息 获得它的索引 * @param messageLite * @return 若不存在对应的索引,返回 -1,存在对应的索引,则返回 [0,?) */ private int getProtoIndex(final MessageLite messageLite) throws UnsupportedOperationException{ if (messageLiteToEnumMap.containsKey(messageLite.getClass())){ return messageLiteToEnumMap.get(messageLite.getClass()).ordinal(); } String enumName=messageLite.getClass().getSimpleName().toUpperCase(); for (ProtoBufEnum protoBufEnum :values){ if (enumName.equals(protoBufEnum.name())){ messageLiteToEnumMap.put(messageLite.getClass(), protoBufEnum); return protoBufEnum.ordinal(); } } return -1; } } }
其次,咱们约定一下编码格式:
它是基于长度字段编码的,前4个字节(第一个int)表示接下里的内容长度,接下来的4个字节(第二个int)表示协议的唯一int值,剩下的内容是真正的protoBuf协议序列化的内容。看一下encoder的实现:
package netty.protobufcodec; import com.google.protobuf.MessageLite; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.codec.MessageToByteEncoder; import java.io.IOException; import java.io.UnsupportedEncodingException; /** * 将protoBuf协议编码为字节流 * * //解码方式 * pipeline.addLast(new {@link LengthFieldBasedFrameDecoder}(8192, 0, 4, 0, 4)); * pipeline.addLast(new {@link ByteToProtoBufDecoder}()); * * //编码方式 * pipeline.addLast("ProtoBufToByteEncoder", new {@link ProtoBufToByteEncoder}()); */ public class ProtoBufToByteEncoder extends MessageToByteEncoder<MessageLite>{ /** * 将protoBuf协议编码为网络字节流 * @param ctx * @param msg 需要编码的protoBuf协议 * @param out 写入的字节流 * @throws Exception */ @Override protected void encode(ChannelHandlerContext ctx, MessageLite msg, ByteBuf out) throws Exception { encode(msg, out); } /** * 这里是为了测试方便 * @param msg * @param out * @throws IOException */ public static void encode(MessageLite msg, ByteBuf out) throws IOException { //先获取消息对应的枚举编号 int protoIndex = ProtoBufEnum.protoIndexOfMessage(msg); if (protoIndex==-1){ throw new UnsupportedEncodingException("UnsupportedEncodingProtoBuf " + msg.getClass().getSimpleName()); } //protoLength 表示有效内容的长度(不包括自身) int protoLength = 4 + msg.getSerializedSize(); out.writeInt(protoLength); out.writeInt(protoIndex);//4 try(ByteBufOutputStream byteBufOutputStream=new ByteBufOutputStream(out)){ msg.writeTo(byteBufOutputStream);//msg.getSerializedSize() } } }
最后,既然编码格式已经确定了,那么解码就很容易确定了。在正式处理解码之前,我们需要LengthFieldBasedFrameDecoder帮我们处理一下拆包和粘包问题。 即在添加我们的解码器之前,需要添加一个LengthFieldBasedFrameDecoder(谨慎的继承LengthFieldBasedFrameDecoder),此外顺带帮我们跳过前面4个我们用于表示消息长度的字节。
pipeline.addLast(new LengthFieldBasedFrameDecoder(8192,0,4,0,4)) .addLast(new ByteToProtoBufDecoder());
看一下decoder:
package netty.protobufcodec; import com.google.protobuf.MessageLite; import com.google.protobuf.Parser; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; /** * 解码字节流为 google protoBuf * * //解码方式 * pipeline.addLast(new {@link LengthFieldBasedFrameDecoder}(8192, 0, 4, 0, 4)); * pipeline.addLast(new {@link ByteToProtoBufDecoder}()); * * //编码方式(这里没有使用LengthFieldPrepender是为了提高可读性) * pipeline.addLast("ProtoBufToByteEncoder", new {@link ProtoBufToByteEncoder}()); */ public class ByteToProtoBufDecoder extends SimpleChannelInboundHandler<ByteBuf>{ /** * 自动释放msg */ public ByteToProtoBufDecoder() { super(true); } /** * 在这里,我们负责的将一个完整的数据包,解码为对应的协议。 * (这里收到的msg是已经经过 new {@link LengthFieldBasedFrameDecoder}(8192, 0, 4, 0, 4) 拆包、粘包处理之后,并跳过了前4个字节的数据包) * @param ctx * @param msg 一个完整的、待解码的数据包 * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { //消息的前4个字节已被跳过,所以这里已经没有了长度字段,只剩下内容部分 //编码时使用了一个int标记protoBuf协议的类,那在解码时需要先取出该 int protoIndex=msg.readInt(); //通过索引获得该协议对应的解析器(客户端与服务器需要保持索引的一致性) Parser parser= ProtoBufEnum.parserOfProtoIndex(protoIndex); if (parser==null){ throw new IllegalArgumentException("illegal protoIndex " + protoIndex);//自己决定如何处理协议无法解析的情况 } try (ByteBufInputStream bufInputStream=new ByteBufInputStream(msg)){ MessageLite messageLite= (MessageLite) parser.parseFrom(bufInputStream); ctx.fireChannelRead(messageLite);//将消息传递下去,或者在这里将消息发布出去 } } }
在解码结束时候,可以自己发布,或者交给下一个inBoundHandler实现发布,进而由我们的应用程序进行消费。
测试用例代码(使用了EmbeddedChannel,如果不熟悉可以了解一下,方便单元测试的channel):
package netty.start; import com.google.protobuf.MessageLite; import com.wjybxx.proto.server_client; import com.wjybxx.proto.server_server; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelPipeline; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import netty.protobufcodec.ByteToProtoBufDecoder; import netty.protobufcodec.ProtoBufToByteEncoder; import java.io.IOException; /** * protoBuf编码解码测试 */ public class ProtoBufCodecTest { /** * 编码解码测试 * @param args * @throws InterruptedException * @throws IOException */ public static void main(String[] args) throws InterruptedException, IOException { EmbeddedChannel channel=new EmbeddedChannel( new LengthFieldBasedFrameDecoder(8192,0,4,0,4), new ByteToProtoBufDecoder(), new ProtoBufToByteEncoder() ); //正式的使用应该是在 initChannel中添加handler // ChannelPipeline pipeline=channel.pipeline(); // pipeline.addLast(new LengthFieldBasedFrameDecoder(8192,0,4,0,4)) // .addLast(new ByteToProtoBufDecoder()) // .addLast(new ProtoBufToByteEncoder()); //模拟接收protoBuf协议,EmbeddedChannel可以不建立链接直接测试channelHandler,你可以debug下追踪详细的编码解码过程 { server_client.server_client_first_message.Builder b1= server_client.server_client_first_message.newBuilder(); b1.setUid(123); b1.setName("read"); sendMessage(channel,b1.build()); server_server.server_server_first_message.Builder b2=server_server.server_server_first_message.newBuilder(); b2.setServerId(1); b2.setIp("127.0.0.1"); sendMessage(channel,b2.build()); server_client.server_client_one_request.Builder b3=server_client.server_client_one_request.newBuilder(); b3.setRequestId(10086); sendMessage(channel,b3.build()); server_server.server_server_ping.Builder b4 = server_server.server_server_ping.newBuilder(); sendMessage(channel,b4.build()); } //模拟发送protoBuf协议 { server_client.server_client_first_message.Builder builder= server_client.server_client_first_message.newBuilder(); builder.setUid(567); builder.setName("send"); channel.writeOneOutbound(builder.build()); } } private static void sendMessage(EmbeddedChannel channel,MessageLite messageLite) throws IOException, InterruptedException { ByteBuf buffer=channel.alloc().buffer(); ProtoBufToByteEncoder.encode(messageLite,buffer); channel.writeOneInbound(buffer).sync(); Object msg = channel.readInbound(); System.out.println("channel read:"); System.out.println("className: " + msg.getClass().getSimpleName()); System.out.println("content: " + msg.toString()); } }
每发送一个消息,会将最终收到的消息输出到控制台,测试用例的输出如下:
channel read:
className: server_client_first_message
content: uid: 123
name: "read"
channel read:
className: server_server_first_message
content: serverId: 1
ip: "127.0.0.1"
channel read:
className: server_client_one_request
content: requestId: 10086
channel read:
className: server_server_ping
content:
2018年5月16日 修改:补充一些细节,提高代码的可读性,已经添加更多的消息,用作展示,更新demo的测试用例。