netty+Protobuf (整合一)

 疯狂创客圈,一个Java 高并发研习社群 【博客园 总入口 】
 疯狂创客圈,倾力推出:  《Netty Zookeeper Redis 高并发实战》  面试必备 + 面试必备 + 面试必备 的基础原理+实战书籍
html

书籍 


netty+Protobuf 整合实战

疯狂创客圈 死磕Netty 亿级流量架构系列之12 【博客园 总入口

本文说明

本篇是 netty+Protobuf 整合实战的 第一篇,完成一个 基于Netty + Protobuf 实战案例。java

要实现高并发、大流量,首先须要高传输效率的协议,Protobuf 是迄今为止最高性能之一的传输格式,咱们首先将 Protobuf 和Netty整合起来。python

本案例源代码

源代码下载连接: netty+protobuf (整合源代码)c++

What is Protobuf ?

 

protocolbuffer(如下简称PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和python,每一种实现都包含了相应语言的编译器以及库文件。因为它是一种二进制的格式,比使用 xml进行数据交换快许多。能够把它用于分布式应用之间的数据通讯或者异构环境下的数据交换。做为一种效率和兼容性都很优秀的二进制数据传输格式,能够用于诸如网络传输、配置文件、数据存储等诸多领域。 git

Why Protobuf ?

Protobuf是由谷歌开源而来,在谷歌内部久经考验。它将数据结构以.proto文件进行描述,经过代码生成工具能够生成对应数据结构的POJO对象和Protobuf相关的方法和属性。github

特色以下:web

  • 结构化数据存储格式(XML,JSON等)面试

  • 高效的编解码性能json

  • 语言无关、平台无关、扩展性好bootstrap

数据交互xml、json、protobuf格式比较
  1. json: 通常的web项目中,最流行的主要仍是json。由于浏览器对于json数据支持很是好,有不少内建的函数支持。

  2. xml: 在webservice中应用最为普遍,可是相比于json,它的数据更加冗余,由于须要成对的闭合标签。json使用了键值对的方式,不只压缩了必定的数据空间,同时也具备可读性。

  3. protobuf:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。由于profobuf是二进制数据格式,须要编码和解码。数据自己不具备可读性。所以只能反序列化以后获得真正可读的数据。

相对于其它protobuf更具备优点
  1. 序列化后体积相比Json和XML很小,适合网络传输

  2. 支持跨平台多语言

  3. 消息格式升级和兼容性还不错

  4. 序列化反序列化速度很快,快于Json的处理速速

结论: 在一个须要大量的数据传输的场景中,若是数据量很大,那么选择protobuf能够明显的减小数据量,减小网络IO,从而减小网络传输所消耗的时间。

于是,对于打造一款高性能的通信服务器来讲,protobuf 传输格式,是最佳的解决方案。

windows 下安装 protoc

1,去这里 https://github.com/google/protobuf/releases

下载对应的protoc,本实例使用的 zip文件是老版本: protoc-2.6.1-win32.zip (本人对老版本比较属性,你们能够换成最新版本) 此工具在源代码包中已经有,能够直接解压缩源码包,直接使用

2,下好以后解压就行,而后把bin里面的 protoc.exe 加入到环境变量

三、或者,把protoc.exe拷贝到C:\Windows\System32

实战第1步:proto文件的创建

前面讲了那么多,都是一些知识铺垫,和前期的准备。

整合protobuf 的第一步,是准备一个消息的协议文件。 协议文件的后缀名称为 .proto , 该文件的定义咱们须要传输的协议。实例以下:

//定义protobuf的包名称空间
option java_package = "com.crazymakercircle.chat.common.bean.msg";
// 消息体名称
option java_outer_classname = "ProtoMsg";
//.....
​
/*聊天消息*/
message MessageRequest{
    uint64 msg_id = 1;       //消息id
    string from = 2;     //发送方uId
    string to = 3;          //接收方uId
    uint64 time = 4;     //时间戳(单位:毫秒)
    required uint32 msg_type = 5; //消息类型  1:纯文本  2:音频 3:视频 4:地理位置 5:其余
   string content = 6; //消息内容
    string url = 7;      //多媒体地址
    string property = 8;  //附加属性
    string from_nick = 9; //发送者昵称
    optional string json = 10;       //附加的json串
}

说明:

  • 协议文件中,主要定义了最终生成的Java 代码 对应的包的名称、类的名称。分别使用 java_package、 java_outer_classname 来指定。

  • 协议文件中,每一个具体的协议message对应于一个最终的Java类,协议的字段对应到类的属性。

    实际上生成的Java代码,远远不止这些。具体请参见源码包。

关于的.proto文件的格式,请你们参考 史上最简明的proto语法教程

关于的.proto消息的规则,请你们参考 史上最简明的proto消息规则

实战第2步:生成 proto 消息 Java代码

建立好.proto文件以后,就须要按照好了对应版本的 protoc.exe工具protoc.exe工具是生成Java文件的工具软件。 安装的方法,前面已经讲了。

这里须要提示一下版本。Java 的maven 配置文件中 proto 包的版本,和 .proto文件的版本, 以及生成java 代码的protoc.exe的版本,三者须要一致。

下面开始生成 消息的 Java代码。 须要用到下面的指令:

 

protoc.exe --java_out=输出的Java文件路径名称   .proto文件路径名称

例如:

protoc.exe --java_out=./src/main/java/ ./proto/ProtoMsg.proto

输入完以后,回车便可在目标目录看到已经生成好的Java文件,而后将该文件放到项目中该文件指定的路径下便可。

本案例的工程中,以及给你们准备好了.bat windows 的命令文件,在 .bat 目录 下执行.bat 文件便可。 .bat 文件以下:

d:
cd D:\\crazymakercircleJava\nettydemo\chatcommon
protoc.exe --java_out=./src/main/java/ ./proto/ProtoMsg.proto

使用的时候,注意调整为实际的目录。

加上对protobuf 的maven依赖

修改maven 的pom.xml文件,加上对protobuf 的依赖,代码以下:

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>${protobuf.version}</version>
</dependency>

实战第3步:构建 ProtoMsg.Message 消息

生成代码后,若是须要构建对应的消息,须要取得Java消息类型的 Builder 实例,在设置了Builder 实例的字段属性值,而后执行 Builder 实例的build() 方法。

嵌套的消息,能够经过顶层消息的 buildPartial() 取得基础部分的 Builder实例 ,而后再设置内嵌消息属性,最后执行build() 方法。

好比: mb.buildPartial().toBuilder().setMessageRequest(cb).build();

具体以下面的例子所示:

/**
 * 基础 Builder
 */
private static class BaseBuilder
{
    private  User user;
    protected ProtoMsg.HeadType type;
    private long seqId;
​
    public BaseBuilder(ProtoMsg.HeadType type,User user)
    {
        this.type = type;
        this.user=user;
    }
   /**
     * 构建消息 基础部分
     */
    public ProtoMsg.Message buildPartial()
    {
        seqId = genSeqId();
​
        ProtoMsg.Message.Builder mb = ProtoMsg.Message.newBuilder()
                .setType(type)
                .setSequence(seqId)
                .setSessionId(user.getSessionId());
        return mb.buildPartial();
    }
​
}
​
​
/**
 * 聊天消息Builder
 */
private static class ChatMsgBuilder extends BaseBuilder
{
   //...
    public ProtoMsg.Message build()
    {
        //基础部分
        ProtoMsg.Message message = buildPartial();
        //内嵌部分
        ProtoMsg.MessageRequest.Builder cb
                = ProtoMsg.MessageRequest.newBuilder();
        //组合起来,而后构建
     return message.toBuilder().setMessageRequest(cb).build();
    }
}
​

实战第4步:编码器

在发出ProtoMsg.Message 消息前,还须要对二进制消息进一步封装。

使用2字节消息长度+Message(二进制数据)+(2字节CRC校验(可选))

其中2字节的内容,只包含Message的长度,不包含自身和CRC的长度。若是须要也能够包含,当要记得通讯双方必须一致。

编码器以下:

​
public class ProtobufEncoder extends MessageToByteEncoder<ProtoMsg.Message> {
​
   @Override
   protected void encode(ChannelHandlerContext ctx, ProtoMsg.Message msg, ByteBuf out)
         throws Exception {
      byte[] bytes = msg.toByteArray();// 将对象转换为byte
      int length = bytes.length;// 读取消息的长度
​
      ByteBuf buf = Unpooled.buffer(2 + length);
      buf.writeShort(length);// 先将消息长度写入,也就是消息头
      buf.writeBytes(bytes);// 消息体中包含咱们要发送的数据
      out.writeBytes(buf);
​
   }

}

实战第五步 解码器

与编码器的操做相反,去掉头部的两个字节,而后转换成 ProtoMsg.Message 消息

/**
 * 解码器
 *
 */
public class ProtobufDecoder extends ByteToMessageDecoder {
   @Override
   protected void decode(ChannelHandlerContext ctx, ByteBuf in,
         List<Object> out) throws Exception {
      // 标记一下当前的readIndex的位置
      in.markReaderIndex();
      // 判断包头长度
      if (in.readableBytes() < 2) {// 不够包头
         return;
      }
​
      // 读取传送过来的消息的长度。
      int length = in.readUnsignedShort();
​
      // 长度若是小于0
      if (length < 0) {// 非法数据,关闭链接
         ctx.close();
      }
​
      if (length > in.readableBytes()) {// 读到的消息体长度若是小于传送过来的消息长度
         // 重置读取位置
         in.resetReaderIndex();
         return;
      }
​
      ByteBuf frame = Unpooled.buffer(length);
      in.readBytes(frame);
      try {
         byte[] inByte = frame.array();
​
         // 字节转成对象
         ProtoMsg.Message msg = ProtoMsg.Message.parseFrom(inByte);

         if (msg != null) {
            // 获取业务消息头
            out.add(msg);
         }
      } catch (Exception e) {
         LOG.info(ctx.channel().remoteAddress() + ",decode failed.", e);
      }
​
   }
}

实战第六步 解码器

将编码器和解码器,加入pipeline中,代码以下:

// 设置通道初始化
bootstrap.handler(
        new ChannelInitializer<SocketChannel>()
        {
            public void initChannel(SocketChannel ch) throws Exception
            {
                ch.pipeline().addLast(new ProtobufDecoder());
                ch.pipeline().addLast(new ProtobufEncoder());
                ch.pipeline().addLast(chatClientHandler);
​
            }
        }
);

这一块,很简单。

写在最后

终于大功告成。

为了方便你们理解 netty 和 protobuf 整合的过程, 实例进行了裁剪,仅仅剩下了 上面这块很是很重要的部分。

若是须要真正的理解上面的内容,建议你们必定要去跑实例。


疯狂创客圈 实战计划
  • Netty 亿级流量 高并发 IM后台 开源项目实战

  • Netty 源码、原理、JAVA NIO 原理

  • Java 面试题 一网打尽

  • 疯狂创客圈 【 博客园 总入口 】

相关文章
相关标签/搜索