编码与解码
基本介绍
- 编写网络应用数据时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码。
- codec(编解码器)的组成部分有两个:decoder(解码器),encoder(编码器)。encoder负责把业务数据转成字节码数据,decoder负责把字节码数据转成业务数据。
编码与解码器
- 当Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。
- Netty提供一系列实用的编解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler
解码器- ByteToMessageDecoder
关系继承图
由于不可能知道远程节点 是否会一次性发送一个完整的信,TCP有可能出现粘包和拆包问题,这个类会对入站数据进行缓冲,直到它准备好被处理。
一个关于ByteToMessageDecoder实例分析
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}
说明:
这个例子,每次入站从ByteBuf中读取4字节,将其解码为一个int,然后将它添加到下一个List中。当没有更多元素可以被添加到该List中时,它的内容将会被发送给下一个ChannelInboundHandler。int在被添加到List中时,会被自动装箱为Integer。在调用readInt()方法前必须验证所输入的ByteBuf是否具有足够的数据
解码器-ReplayingDecoder
-
public abstract class ReplayingDecoder
extends ByteToMessageDecoder -
ReplayingDecoder扩展了ByteToMessageDecoder类,使用这个类,我们不必调用readableBytes()方法。参数S指定了用户状态管理的类型,其中Void代表不需要状态管理
-
ReplayingDecoder使用方便,但它也有一些局限性:
- 并不是所有的 ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个 UnsupportedOperationException。
- ReplayingDecoder 在某些情况下可能稍慢于 ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢
其他编码器
-
LineBasedFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。
-
DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。
-
HttpObjectDecoder:一个HTTP数据的解码器
-
LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。
netty本身编码与解码的机制和问题分析
-
Netty 自身提供了一些 codec(编解码器)
-
Netty 提供的编码器
StringEncoder,对字符串数据进行编码
ObjectEncoder,对 Java 对象进行编码
… -
Netty 提供的解码器
StringDecoder, 对字符串数据进行解码
ObjectDecoder,对 Java 对象进行解码
… -
Netty 本身自带的 ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是 Java 序列化技术 , 而Java 序列化技术本身效率就不高,存在如下问题
无法跨语言
序列化后的体积太大,是二进制编码的 5 倍多。
序列化性能太低
=> 引出 新的解决方案 [Google 的 Protobuf]
Protobuf
Protobuf是什么
我们先来看看官方文档给出的定义和描述:
- protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
- Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
- 你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
Protobuf入门实例
编写程序,使用Protobuf完成如下功能
- 客户端可以发送一个Student PoJo对象到服务器(通过Protobuf编码)
- 服务器能够接收Studeng PoJo对象,并显示信息(通过Protobuf解码)
代码示例如下,具体代码解释看注释
- 编写Student.proto文件,用proto语法编写
syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message 管理数据
message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, 他是真正发送的POJO对象
int32 id = 1; // Student 类中有 一个属性 名字为 id 类型为int32(protobuf类型) 1表示属性序号,不是值
string name = 2;
}
- 使用protoc.exe编译Student.proto文件 :
protoc.exe --java_out=. Student.proto
生成StudentPOJO.java文件 - 编写客户端程序
public class MyNettyClient {
public static void main(String[] args) throws InterruptedException {
// 客户端只需要创建一个线程组
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
// 创建引导程序
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new MyClientInitializer());
System.out.println("客户端启动完毕。。。");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
//////////////////////
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 加入ProtoBufEncoder编码器
pipeline.addLast(new ProtobufEncoder());
// 加入自己的业务处理handler
pipeline.addLast(new MyClientHandler());
}
}
///////////////////////
public class MyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道就绪就会调用此方法
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端 channelActive 方法被调用");
// 发送一个Student对象到服务器
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(5).setName("张三").build();
ctx.writeAndFlush(student);
}
/**
* 当通道有读取事件时就会触发此方法
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到服务器发来消息" + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 有异常就会调用此方法
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel();
}
}
- 编写服务器程序
public class MyNettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建2个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建引导程序
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
System.out.println("服务器启动完毕。。。");
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
///////////////////////////////
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 加入protobuf解码器 指定对哪种对象进行解码
pipeline.addLast(new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
// 加入业务处理Handler
pipeline.addLast(new MyServerHandler());
}
}
/////////////////////////////////////
public class MyServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {
/**
* 当通道有读取事件时就会调用此方法
* @param channelHandlerContext
* @param student
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, StudentPOJO.Student student) throws Exception {
System.out.println("收到客户端消息:学生的id为"+ student.getId() + "学生姓名是:" + student.getName());
}
/**
* 当数据读取完毕后 会调用 此方法
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 给客户端回复一个消息
ctx.writeAndFlush(Unpooled.copiedBuffer("客户端,你好", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
先启动服务器,再启动客户端,运行结果如下:
服务器启动完毕。。。
收到客户端消息:学生的id为5学生姓名是:张三
客户端启动完毕。。。
客户端 channelActive 方法被调用
收到服务器发来消息客户端,你好
从结果可以看出服务器正确的输出了客户端发来的Student对象,先是通过客户端的ProtobufEncoder
对Student进行编码,然后再通过服务器端的ProtobufDecoder
进行解码,服务器就正确输出了Student对象,很重要的是先要编写Student.proto文件,然后再protoc.exe编译Student.proto文件 生成StudentPOJO.java文件。