Protobuf序列化的字节流数据是不能自描述的,当我们通过socket把数据发送到Client时,Client必须知道发送的是什么类型的数据,才能正确的反序列化它。这严重影响限制了C/S功能的实现,不解决的话信道事实上只能传输一种类型的数据。本文讲解一下我用的解决办法,虽然我觉得应该有官方的实现更合理,即原生支持Protobuf的自描述。

(在金融领域,有一个叫FAST的协议,基本原理和Protobuf相同,并且有更高的压缩率,并且序列化后的字节流是自描述的,可以自动反序列化为对应的模板的数据(模板相当于.proto文件),但是时间效率比protobuf差,大家也可以关注一下。)

解决方案一

首先,介绍另外一种实现,在protobuf官方wiki中描述的一种workaround,通过定义一种用于自描述的类型:

message SelfDescribingMessage {
// Set of .proto files which define the type.
required FileDescriptorSet proto_files = ; // Name of the message type. Must be defined by one of the files in
// proto_files.
required string type_name = ; // The message data.
required bytes message_data = ;
}

(参考:https://developers.google.com/protocol-buffers/docs/techniques#self-description

把实际要传输的类型的字节数组放在message_data字段中,用proto_files和type_name字段来描述它的proto文件和类型。这样,信道上传输的都是SelfDescribingMessage类型,但是其上的负载可以是任何类型的数据。

我没有试过这种方式。我不太愿意使用这种方式的原因是,很显然,这样做需要进行2次序列化和2次反序列化,byte数组也要被创建2次。如果对应时延和性能敏感的系统,这样做不够好。

解决方案二

今天主要要介绍的方案。在protobuf序列化的前面,加上一个自定义的头,这个头包含序列化的长度和它的类型。在解压的时候根据包头来反序列化。

假设socket上要传输2个类型的数据,股票行情信息和期权行情信息:

股票的.proto定义:

syntax = "proto3";

package test.model.protobuf;

option java_package = "test.model.protobuf";

message StockTick {
string stockId = 1;
int price = 2;
}

期权的.proto定义:

syntax = "proto3";

package test.model.protobuf;

option java_package = "test.model.protobuf";

message OptionTick {
string optionId = 1;
string securityId = 2;
int price = 3;
}

netty4官方事实上已经实现了protobuf的编解码的插件,但是只能用于传输单一类型的protobuf序列化。我这里截取一段netty代码,熟悉netty的同学马上就能理解它的作用:

        @Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ProtobufVarint32FrameDecoder());
pipeline.addLast(new ProtobufDecoder(StockTickOuterClass.StockTick.getDefaultInstance()));
pipeline.addLast(new ProtobufVarint32LengthFieldPrepender());
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new CustomProtoServerHandler());
}

看以上代码高亮部分,netty4官方的编解码器必须指定单一的protobuf类型才行。具体每个类的作用:

ProtobufEncoder:用于对Probuf类型序列化。
ProtobufVarint32LengthFieldPrepender:用于在序列化的字节数组前加上一个简单的包头,只包含序列化的字节长度。
ProtobufVarint32FrameDecoder:用于decode前解决半包和粘包问题(利用包头中的包含数组长度来识别半包粘包)
ProtobufDecoder:反序列化指定的Probuf字节数组为protobuf类型。

我们可以参考以上官方的编解码代码,将实现我们客户化的protobuf编解码插件,但是要支持多种不同类型protobuf数据在一个socket上传输:

编码器CustomProtobufEncoder:

import com.google.protobuf.MessageLite;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder; /**
* 参考ProtobufVarint32LengthFieldPrepender 和 ProtobufEncoder
*/
@Sharable
public class CustomProtobufEncoder extends MessageToByteEncoder<MessageLite> { HangqingEncoder hangqingEncoder; public CustomProtobufEncoder(HangqingEncoder hangqingEncoder)
{
this.hangqingEncoder = hangqingEncoder;
} @Override
protected void encode(
ChannelHandlerContext ctx, MessageLite msg, ByteBuf out) throws Exception { byte[] body = msg.toByteArray();
byte[] header = encodeHeader(msg, (short)body.length); out.writeBytes(header);
out.writeBytes(body); return;
} private byte[] encodeHeader(MessageLite msg, short bodyLength) {
byte messageType = 0x0f; if (msg instanceof StockTickOuterClass.StockTick) {
messageType = 0x00;
} else if (msg instanceof OptionTickOuterClass.OptionTick) {
messageType = 0x01;
} byte[] header = new byte[4];
header[0] = (byte) (bodyLength & 0xff);
header[1] = (byte) ((bodyLength >> 8) & 0xff);
header[2] = 0; // 保留字段
header[3] = messageType; return header; }
}

CustomProtobufEncoder序列化传入的protobuf类型,并且为它创建了一个4个字节的包头,格式如下

body长度(low) body长度
(high)
保留字节 类型

其中的encodeHeader方法具体的实现要根据你要传输哪些protobuf类型来修改代码,也可以稍加设计避免使用太多的if…else。

解码器CustomProtobufDecoder:

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import com.google.protobuf.MessageLite; /**
* 参考ProtobufVarint32FrameDecoder 和 ProtobufDecoder
*/ public class CustomProtobufDecoder extends ByteToMessageDecoder { @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
while (in.readableBytes() > 4) { // 如果可读长度小于包头长度,退出。
in.markReaderIndex(); // 获取包头中的body长度
byte low = in.readByte();
byte high = in.readByte();
short s0 = (short) (low & 0xff);
short s1 = (short) (high & 0xff);
s1 <<= 8;
short length = (short) (s0 | s1); // 获取包头中的protobuf类型
in.readByte();
byte dataType = in.readByte(); // 如果可读长度小于body长度,恢复读指针,退出。
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
} // 读取body
ByteBuf bodyByteBuf = in.readBytes(length); byte[] array;
int offset; int readableLen= bodyByteBuf.readableBytes();
if (bodyByteBuf.hasArray()) {
array = bodyByteBuf.array();
offset = bodyByteBuf.arrayOffset() + bodyByteBuf.readerIndex();
} else {
array = new byte[readableLen];
bodyByteBuf.getBytes(bodyByteBuf.readerIndex(), array, 0, readableLen);
offset = 0;
} //反序列化
MessageLite result = decodeBody(dataType, array, offset, readableLen);
out.add(result);
}
} public MessageLite decodeBody(byte dataType, byte[] array, int offset, int length) throws Exception {
if (dataType == 0x00) {
return StockTickOuterClass.StockTick.getDefaultInstance().
getParserForType().parseFrom(array, offset, length); } else if (dataType == 0x01) {
return OptionTickOuterClass.OptionTick.getDefaultInstance().
getParserForType().parseFrom(array, offset, length);
} return null; // or throw exception
}
}

CustomProtobufDecoder实现了2个功能,1)通过包头中的长度信息来解决半包和粘包。 2)把消息body反序列化为对应的protobuf类型(根据包头中的类型信息)。

其中的decodeBody方法具体的实现要根据你要传输哪些protobuf类型来修改代码,也可以稍加设计避免使用太多的if…else。

在Netty服务器上应用编解码器

如何把我们自定义的编解码用于netty Server:

        @Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder",new CustomProtobufDecoder());
pipeline.addLast("encoder",new CustomProtobufEncoder());
pipeline.addLast(new CustomProtoServerHandler());
}

Binhua Liu原创文章,转载请注明原地址http://www.cnblogs.com/Binhua-Liu/p/5577622.html

Protobuf3 + Netty4: 在socket上传输多种类型的protobuf数据的更多相关文章

  1. MVC下form表单一次上传多种类型的图片(每种类型的图片可以上传多张)

    form表单一次上传多种类型的图片(每种类型的图片可以上传多张) controller中的action方法 public ActionResult UploadImage( )        { in ...

  2. Build2016上值得一看的大数据相关Session

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:Build2016开完很久了,现在才来回顾下,就说说那些和大数据相关的Session, ...

  3. 基于TCP协议的项目架构之Socket流传输的实现

    项目背景  某银行的影像平台由于使用时间长,服务器等配置原因,老影像系统满足不了现在日益增长的数据量的需求,所以急需要升级改造.传统的影像平台使用的是Oracle数据库和简单的架构来存储数据(视频.图 ...

  4. 编码占用的字节数 1 byte 8 bit 1 sh 1 bit 中文字符编码 2. 字符与编码在程序中的实现 变长编码 Unicode UTF-8 转换 在网络上传输 保存到磁盘上 bytes

    小结: 1.UNICODE 字符集编码的标准有很多种,比如:UTF-8, UTF-7, UTF-16, UnicodeLittle, UnicodeBig 等: 2 服务器->网页 utf-8 ...

  5. geotrellis使用(十)缓冲区分析以及多种类型要素栅格化

    目录 前言 缓冲区分析 多种类型要素栅格化 总结 参考链接 一.前言        上两篇文章介绍了如何使用Geotrellis进行矢量数据栅格化以及栅格渲染,本文主要介绍栅格化过程中常用到的缓冲区分 ...

  6. ZeroMQ接口函数之 :zmq_recv – 从一个socket上接收一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq_recv zmq_recv(3)        ØMQ Manual - ØMQ/4.1.0 Name zmq_r ...

  7. ZeroMQ接口函数之 :zmq_send – 在一个socket上发送一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-send zmq_send(3)              ØMQ Manual - ØMQ/4.1.0 Name ...

  8. ZeroMQ接口函数之 :zmq_recvmsg – 从一个socket上接收一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-recvmsg zmq_recvmsg(3)         ØMQ Manual - ØMQ/4.1.0 Nam ...

  9. ZeroMQ接口函数之 :zmq_sendmsg – 从一个socket上发送一个消息帧

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq-sendmsg zmq_sendmsg(3)        ØMQ Manual - ØMQ/4.1.0 Name ...

随机推荐

  1. 正经学C#_介绍与其编写基础:《c#入门经典》

    本文所讲内容,均可在<c#入门经典>中可以查询.如有错误,敬请指出.谢谢! C#:全称C Shar.是微软.Net Framework平台下最为主要的客户语言之一.个人理解,c#是微软最为 ...

  2. Linux:krb5

    Kerberos协议主要用于计算机网络的身份鉴别(Authentication), 其特点是用户只需输入一次身份验证信息就可以凭借此验证获得的票据(ticket-granting ticket)访问多 ...

  3. 总结ThinkPHP使用技巧经验分享(二)

    循环输出volist 还有别名 iterate 模版赋值:$User = D('User')$list = $User->findAll()$this->assign('list',$li ...

  4. Pointcut is malformed: Pointcut is not well-formed: expecting 'identifier' at character position 0 ^

    错误提示: 解决方法:指定execution 在执行目标方法之前指定execution 例如: import org.aspectj.lang.annotation.Aspect; import or ...

  5. Windows Phone 四、控件模版

    控件模版的概念 Windows Phone中每一个控件都有一个默认的模版,用于描述控件的内部组成结构和外观样式 相对于原本的样式外观操作,自定义模版的可自定义性更强 最基本的重写控件模版 <Gr ...

  6. rc.local文件

    rc.local用于自启动一些服务. 查看有哪些自启动服务: cat /etc/rc.local

  7. 运用node的文件系统模块批量修改文件名

      如果我们需要大批量修改一个文件中的名称,比如,删除文件名中的副本时,就可以借助node的文件系统模块,快捷快速的完成. 首先建立一个js文件(changeName.js),代码如下: // 引入f ...

  8. Sublime一键预览

    //chrome{ "keys": [ "f12" ], "command": "side_bar_files_open_with ...

  9. .net常见的面试题

    1,asp.net中的页生命周期 答:msdn官网已给出标准答案,这里简述一下:页要经历下表概述的8个阶段.除了页生命周期阶段以外,在请求前后还存在应用程序阶段,但是这些阶段并不特定于页. 而这8个阶 ...

  10. 头疼:为什么chrome不能访问本地文件(带--disable-web-security --allow-file-access-from-files )

    如题,寻求帮助! chrome 带参数启动 --disable-web-security  --allow-file-access-from-files 照理应该可以加载本地文件,找遍google和英 ...