基于netty实现的支持任意个protoBuf编解码通信

要实现一个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 (每一个协议名都包含其外部类名称,并以下划线"分隔)。如图:

扫描二维码关注公众号,回复: 897466 查看本文章


  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,而proto3private,在获取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的测试用例。

猜你喜欢

转载自blog.csdn.net/weixin_37555076/article/details/80329382