netty+Protobuf 整合实战
疯狂创客圈 死磕Netty 亿级流量架构系列之12 【博客园 总入口 】
本文说明
本篇是 netty+Protobuf 整合实战的 第一篇,完成一个 基于Netty + Protobuf 实战案例。
要实现高并发、大流量,首先需要高传输效率的协议,Protobuf 是迄今为止最高性能之一的传输格式,我们首先将 Protobuf 和Netty整合起来。
本案例源代码
源代码下载链接: netty+protobuf (整合源代码)
What is Protobuf ?
Why Protobuf ?
Protobuf是由谷歌开源而来,在谷歌内部久经考验。它将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
特点如下:
结构化数据存储格式(XML,JSON等)
高效的编解码性能
语言无关、平台无关、扩展性好
数据交互xml、json、protobuf格式比较
json: 一般的web项目中,最流行的主要还是json。因为浏览器对于json数据支持非常好,有很多内建的函数支持。
xml: 在webservice中应用最为广泛,但是相比于json,它的数据更加冗余,因为需要成对的闭合标签。json使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。
protobuf:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
相对于其它protobuf更具有优势
序列化后体积相比Json和XML很小,适合网络传输
支持跨平台多语言
消息格式升级和兼容性还不错
序列化反序列化速度很快,快于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 加入到环境变量
3、或者,把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代码。 需要用到下面的指令:
例如:
输入完之后,回车即可在目标目录看到已经生成好的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.protobufgroupId> <artifactId>protobuf-javaartifactId> <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 面试题 一网打尽
疯狂创客圈 【 博客园 总入口 】