netty 默认支持protobuf 的封装与解码,如果通信双方都使用netty则没有什么障碍,但如果客户端是其它语言(C#)则需要自己仿写与netty一致的方式(解码+封装),提前是必须很了解netty是如何进行封装与解码的。这里主要通过读源码主要类ProtobufVarint32FrameDecoder(解码)+ProtobufVarint32LengthFieldPrepender(封装) 来解析其原理与实现。

文章来源http://www.cnblogs.com/tankaixiong

一,支持protobuf 协议的默认实现

//配置服务端NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() { @Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufDecoder(
SubscribeReqProto.SubscribeReq.getDefaultInstance()))
.addLast(new ProtobufVarint32LengthFieldPrepender())
.addLast(new ProtobufEncoder())
.addLast(new SubReqServerHandler());
} });
//绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync(); }finally{
//退出时释放资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}

以上是提供的默认实现。关键在于ProtobufVarint32FrameDecoder,ProtobufVarint32LengthFieldPrepender类。

二,ProtobufVarint32LengthFieldPrepender 编码类

An encoder that prepends the the Google Protocol Buffers 128 Varints integer length field.

* BEFORE DECODE (300 bytes)       AFTER DECODE (302 bytes)
* +---------------+ +--------+---------------+
* | Protobuf Data |-------------->| Length | Protobuf Data |
* | (300 bytes) | | 0xAC02 | (300 bytes) |
* +---------------+ +--------+---------------+

从类的说明来看, proto 消息格式如:Length + Protobuf Data (消息头+消息数据) 方式,这里特别需要注意的是头长使用的是varints方式不是int ,消息头描述消息数据体的长度。为了更减少传输量,消息头采用的是varint 格式。

什么是varint?

文章来源http://www.cnblogs.com/tankaixiongVarint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。 Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,会用两个字节。

更多可参见我上篇文章

最大的区别是消息头它不是固定长度(常见是的使用INT 4个字节固定长度),Varint它用一个或多个字节来表示一个数字决定它不是固定长度!

ProtobufVarint32LengthFieldPrepender 类的主要方法如下:

@Override
protected void encode(
ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
int bodyLen = msg.readableBytes();
int headerLen = CodedOutputStream.computeRawVarint32Size(bodyLen);
out.ensureWritable(headerLen + bodyLen); CodedOutputStream headerOut =
CodedOutputStream.newInstance(new ByteBufOutputStream(out), headerLen);
headerOut.writeRawVarint32(bodyLen);
headerOut.flush(); out.writeBytes(msg, msg.readerIndex(), bodyLen);
}

CodedOutputStream 主要是针对与varints相关操作类。 先看是如何写消息头的,得到bodyLen 消息体长度然后调用computeRawVarint32Size()计算需要多少个字节,

public static int computeRawVarint32Size(final int value) {
if ((value & (0xffffffff << 7)) == 0) return 1;
if ((value & (0xffffffff << 14)) == 0) return 2;
if ((value & (0xffffffff << 21)) == 0) return 3;
if ((value & (0xffffffff << 28)) == 0) return 4;
return 5;
}

0xffffffff << 7 二进制表示11111111111111111111111110000000 ,当与value &计算=0则表示value最大只会是000000000000000000000001111111,一个字节足以。

通过&运算得出使用多少个字节就可以表示当前数字。左移7位是与Varint定义相关,第一位需要保留给标识(1表示后续的 byte 也是该数字的一部分,0则结束)。要表示 int 32位 和多加的每个字节第一个标识位,多出了4位,所以就最大会有5个字节。

得到了varints值,然后如何写入out? 再看关键方法writeRawVarint32()。

public void writeRawVarint32(int value) throws IOException {
while (true) {
//0x7F为127
if ((value & ~0x7F) == 0) {//是否小于127,小于则一个字节就可以表示了
writeRawByte(value);
return;
} else {
writeRawByte((value & 0x7F) | 0x80);//因不于小127,加一高位标识
value >>>= 7;//右移7位,再递归
}
}
}
/** Write a single byte. */
public void writeRawByte(final byte value) throws IOException {
if (position == limit) {
refreshBuffer();
} buffer[position++] = value;
} private void refreshBuffer() throws IOException {
if (output == null) {
// We're writing to a single buffer.
throw new OutOfSpaceException();
} // Since we have an output stream, this is our buffer
// and buffer offset == 0
output.write(buffer, 0, position);
position = 0;
}

byte 的取值(-128~127) , 0x7F为127 , 0x80为128

循环取后7位,如果小于127则结束,不小于第一位加标识位1。 因为循环右移所以,实际位置颠倒了,解码时需要倒过来再拼接。

消息头因为是varint32可变字节,所以比较复杂些,消息体简单直接writeBytes即可。

二,ProtobufVarint32FrameDecoder 解码类

同样对应CodedOutputStream有CodedInputStream类,操作解码时的varints。

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
in.markReaderIndex();
final byte[] buf = new byte[5];
for (int i = 0; i < buf.length; i ++) {
if (!in.isReadable()) {
in.resetReaderIndex();
return;
} buf[i] = in.readByte();
if (buf[i] >= 0) {
int length = CodedInputStream.newInstance(buf, 0, i + 1).readRawVarint32();
if (length < 0) {
throw new CorruptedFrameException("negative length: " + length);
} if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
} else {
out.add(in.readBytes(length));
return;
}
}
} // Couldn't find the byte whose MSB is off.
throw new CorruptedFrameException("length wider than 32-bit");
}

前面说明了最大长度为5个字节所以这里声明了5个长度的字节来读取消息头。

buf[i] >= 0 这里为什么是>0然后就可以解码了呢?

还是这句话:varints第一位表示后续的byte是否是该数字的一部分!

如果字节第一位为1则表示后续还有字节是表示消息头,当这个字节的第一位为1则这个字节肯定是负数(字节最高位表示正负),大于等于0表示描述消息体长度的数字已经读完了。

然后调用readRawVarint32() 还原成int ,与之前 writeRawVarint32()反其道而行。

public int readRawVarint32() throws IOException {
byte tmp = readRawByte();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7f;
if ((tmp = readRawByte()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = readRawByte()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = readRawByte()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
result |= (tmp = readRawByte()) << 28;
if (tmp < 0) {
// Discard upper 32 bits.
for (int i = 0; i < 5; i++) {
if (readRawByte() >= 0) {
return result;
}
}
throw InvalidProtocolBufferException.malformedVarint();
}
}
}
}
return result;
}

取第N字节左移7*N位或|第一个字节拼接,实现了倒序拼接,最后得到了消息体长度。然后根据得到的消息体长度读取数据,如果消息体长度不够则回滚到markReaderIndex,等待数据。

四,总结

文章来源http://www.cnblogs.com/tankaixiong本文主要详细介绍了netty 对 protobuf 协议的解码与包装。重点在消息头 varint32的 算法表示上进行了说明。了解了varint32在协议中的实现,方便应用在其语言对接。

netty 对 protobuf 协议的解码与包装探究(2)的更多相关文章

  1. netty系列之:在netty中使用protobuf协议

    目录 简介 定义protobuf 定义handler 设置ChannelPipeline 构建client和server端并运行 总结 简介 netty中有很多适配不同协议的编码工具,对于流行的goo ...

  2. SuperSocket与Netty之实现protobuf协议,包括服务端和客户端

    今天准备给大家介绍一个c#服务器框架(SuperSocket)和一个c#客户端框架(SuperSocket.ClientEngine).这两个框架的作者是园区里面的江大渔. 首先感谢他的无私开源贡献. ...

  3. 【MINA】用protobuf做编解码协议

    SOCKET协议 支持java serial 与 AMF3的混合协议,目前没有基于xml 与 json的实现. 协议说明: * 9个字节协议头+协议体. * * 协议头1-4字节表示协议长度 =协议体 ...

  4. Netty学习——Netty和Protobuf的整合(二)

    Netty学习——Netty和Protobuf的整合(二) 这程序是有瑕疵的,解码器那里不通用,耦合性太强,有两个很明显的问题,但是要怎么解决呢?如:再加一个内部类型 Person2,之前的代码就不能 ...

  5. protobuf 协议浅析

    目录 Protobuf 协议浅析 1. Protobuf 介绍 1.1 Protobuf 基本概念 1.2 Protobuf 的优点 1.3 Protobuf, JSON, XML 的区别 2. Pr ...

  6. 使用Go语言+Protobuf协议完成一个多人聊天室

    软件环境:Goland Github地址 一.目的 之前用纯逻辑垒完了一个可登入登出的在线多人聊天室(代码仓库地址),这次学习了Protobuf协议,于是想试着更新下聊天室的版本. 主要目的是为了掌握 ...

  7. 自定义兼容多种Protobuf协议的编解码器

    <从零开始搭建游戏服务器>自定义兼容多种Protobuf协议的编解码器 直接在protobuf序列化数据的前面,加上一个自定义的协议头,协议头里包含序列数据的长度和对应的数据类型,在数据解 ...

  8. Netty学习篇⑤--编、解码

    前言 学习Netty也有一段时间了,Netty作为一个高性能的异步框架,很多RPC框架也运用到了Netty中的知识,在rpc框架中丰富的数据协议及编解码可以让使用者更加青睐: Netty支持丰富的编解 ...

  9. Netty学习——Netty和Protobuf的整合(一)

    Netty学习——Netty和Protobuf的整合 Protobuf作为序列化的工具,将序列化后的数据,通过Netty来进行在网络上的传输 1.将proto文件里的java包的位置修改一下,然后再执 ...

随机推荐

  1. Linux系统的/proc目录

    1. /proc目录 Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构.改变内核设置的机 制.proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间.它 ...

  2. CodeForces 652B z-sort

    先对序列排个序. 例如:1 2 3 4 5 6 7 我们把序列分成两半,前一半是1 2 3 4,后一半是5 6 7 然后,我们从前一半取最小的一个,再从后一半取最小的一个..一直操作下去就能构造出答案 ...

  3. Linux下mysql数据库的命令

    连接数据库命令:mysql -u 用户名 -p 密码 要求你输入要连接数据库的用户名和密码.用户名默认root密码不方便输入时,可以只输入:mysql -u 用户名 -p 然后回车,此时提示你输入密码 ...

  4. UVa11555 - Aspen Avenue

    今晚CF GYM A题,神坑.. 原题: Aspen Avenue ``Phew, that was the last one!'' exclaimed the garden helper Tim a ...

  5. Nginx + Apache 反向代理

    反向代理负载均衡 使用代理服务器可以将请求转发给内部的Web服务器,使用这种加速模式显然可以提升静态网页的访问速度.因此也可以考虑使用这种技术,让代理服务器将请求均匀转发给多台内部Web服务器之一上, ...

  6. mysql root密码

    方法1: 用SET PASSWORD命令 首先登录MySQL. 格式:mysql> set password for 用户名@localhost = password('新密码'); 例子:my ...

  7. ucos任务优先级从64到256,任务就绪表的改变

    Ucos在任务调度中经常使用的技术为任务就绪表,在之前的文章中使用的例子是低于64个优先级的任务就绪表查找方法,现在ucos将任务扩展到256优先级之后,任务就绪表的查找也做了一定的修改,今天来讲讲 ...

  8. Oracle行转列的3种方法

    测试表为A3 , 有5个字段:ID1, ID2, ID3, ID4, ID5 测试数据如下: ID1 ID2 ID3 ID4 ID5 1 2 3 4 5 11 22 33 44 55 111 222 ...

  9. 路由器、交换机学习之IP地址、使用网络掩码划分子网

    局域网子网划分 对于C类IP地址来说(192.168.1.X,其中前面的192.168.1为网络号,后面的X为主机号,这样的网络中可以有254台主机,其中.0为局域网地址,.255为广播地址)进行子网 ...

  10. UVa 124 - Following Orders

    题目大意:给一个变量列表和变量的大小关系,输出所有的满足约束的序列. 构建为有向图,然后就是拓扑排序,使用回溯输出所有的结果. #include <cstdio> #include < ...