Netty编码器机制
- 编码和解码的基本介绍
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o52Anr6n-1623035864972)(pic\Netty2\Netty编码解码.png)]
- codec(编码器)的组成部分有两个:decoder(解码器)和encoder(编码器)。encoder负责把业务数据缓存字节码数据,decoder负责把字节码数据转换成业务数据
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
- Netty本身的编码解码的机制和问题分析
- Netty自身提供了一些codec(编解码器)
- Netty提供的编码器
- StringEncoder,对字符串数据进行编码
- ObjectEncoder,对Java对象进行编码
- …
- Netty提供的解码器
- StringDecoder,对字符串数据进行解码
- ObjectDeocoder,对Java对象进行编码
- …
- Netty本身自带的ObjectDecoder和ObjectEncoder可以用实现POJO对象或业务对象的编码和解码,底层使用的仍然是Java序列化计数,而Java序列化技术本身效率就不高,存在如下问题
- 无法跨语言
- 序列化后体积太大,是二进制编码的5倍多
- 序列化性能太低
- 解决方法,Google的ProtoBuf
ProtoBuf机制
-
ProtoBuf基本介绍和使用示意图
-
ProtoBuf是Google发布的开源项目,全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化。或者说序列化,它很适合做数据存储或RPC(remote procedure call)【远程过程调用】数据交换格式
-
Protobuf是以message的方式管理数据的
-
支持跨平台,跨语言,即【客户端和服务器端可以是不同的语言编写的】(支持目前绝大多数语言,例如C++,C#,Java,Python等)
-
高性能,高可靠性
-
使用protobuf编译期能自动生成代码,Protobu是将类的定义使用.proto文件进行描述。说明,在idea中编写.proto文件时,会自动提示下载.proto编写插件,可以让语法高亮
-
然后通过proto.exe编译期根据.proto自动生成.java文件
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QF4akQrL-1623035864975)(pic\Netty2\Protobuf编码解码.png)]
-
Netty中ProtoBuf步骤
-
在Maven项目引入Protobuf坐标,下载相关jar包
-
<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.6.1</version> </dependency>
-
编写proto文件Student.proto
-
使用proto.exe编译Student.proto文件,生成相应的StudentPOJO.java类
-
-
ProtoBuf实例
-
要求
- 客户端发送一个Student Pojo对象到服务器(通过ProtoBuf编码)
- 服务端能接受Student POJO对象,并显示信息(通过ProtoBuf解码)
-
代码
-
Student.proto
-
syntax="proto3";//版本 option java_outer_classname = "StudentPOJO"; message Student{ int32 id =1 ; string name = 2; }
-
-
StudentPOJO.java
-
MyNettyClientHandler
-
package com.jl.java.web.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.CharsetUtil; /** * @author jiangl * @version 1.0 * @date 2021/5/23 10:57 */ public class MyNettyClientHandler extends ChannelInboundHandlerAdapter { /** * 当通道就绪时就会触发该方法 * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //发送一个Student对象到服务器 StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(1).setName("dsfadfa").build(); ctx.writeAndFlush(student); } /** * 当通道有读取事件时,会触发 * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println("服务器回复的消息:"+buf.toString(CharsetUtil.UTF_8)); System.out.println("服务器的地址:"+ctx.channel().remoteAddress()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
-
-
MyNettyServerHandler
-
public class MyNettyServerHandler2 extends SimpleChannelInboundHandler<StudentPOJO.Student> { /** * 读取数据实际(可以读取客户端发送的消息) * @param ctx 上下文对象,含有管道pipeline ,通道channel * @param msg 客户端发送的数据,默认Object类型 * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, StudentPOJO.Student msg) throws Exception { System.out.println("客户端发送的数据:id="+msg.getId()+",name="+msg.getName()); } }
-
-
Netty编解码器和handler调用机制
-
基本说明
- netty的组件设计:Netty的主要组件有Channel、EventLoop、ChannelFuture、ChannelHandler、ChannelPipline等
- ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),开发者可以接受入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的
- ChannelPipeline提供了ChannelHandler链的容器,以客户端应用程序为例,如果事件的运动方法是从客户端到服务器端的,则称这些事件为出站,即客户端发送给故武器的数据会通过pipeline中的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgP3VAcn-1623035864976)(pic\Netty2\ChannelPipeline.png)]
- 入站,一定是读取管道中的数据进行解码
- 出站,一定是把编码后的数据写入管道中
- socket和channel通道的相对位置来说明出站和入站
- 数据从channel通道流向socket内存就是入站
- 数据从socket内存流向channel通道就是出站
- 以服务器和客户端为例
- 服务器接收消息就是入站
- 服务器发送消息就是出站
- 客户端接收消息就是入站
- 客户端发送消息就是出站
-
编码解码器
-
当Netty发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节码转换为另一种格式(比如Java对象);如果是出站消息,就会被编码成字节码
-
Netty提供了一系列实用的编解码器,他们都实现了ChannelInboundHandler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站(ChannelPipeline中的ChannelInboundHandler)为例,对于每个从入站Channel读取的消息,channelRead方法都会被调用。随后,它将调用由解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
-
解码器ByteToMessageDecoder
- 关系图
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XKSCxcl5-1623035864978)(D:\personal\文档\文档\pic\Netty2\ByteToMessageDecoder.png)]
- 由于不可能直到远程节点是否会一次性发送一个完整的信息,tcp有可能出现粘包和拆包的问题,ByteToMessageDecoder会对入站数据进行缓冲,直到它准备好被处理。
- 关系图
-
一个关于ByteToMessageDecoder实例分析
-
package com.jl.java.web.codec;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import java.util.List;/** * @author jiangl * @version 1.0 * @date 2021/5/27 14:08 */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是否具有足够的数据
-
-
Netty的Handler调用机制实例
-
案例要求
-
使用自定义的编码器和解码器来说明Netty的handler调用机制
- 客户端发送long(8个字节)->服务器
- 服务器发送long(8个字节) ->客户端
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Pvn3uA6-1623035864980)(pic\Netty2\Handler调用机制.png)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bvI0ulWJ-1623035864982)(pic\Netty2\客户端服务端Handler过程.png)]
-
代码
-
InBoundServerDecoder
-
package com.jl.java.web.inoutbound;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.handler.codec.ByteToMessageDecoder;import java.util.List;/** * 服务端入站解码器 * @author jiangl * @version 1.0 * @date 2021/5/27 14:47 */public class InBoundServerDecoder extends ByteToMessageDecoder { /** * decode 会根据接收的数据,被调用多次,直到确定没有新的元素被添加list, * 或者ByteBuf没有更多的可读字节为止 * 如果list out不为空,就会得到list的内容传递给下一个 * ChannelInboundHandler处理,下一个ChannelInboundHandler的方法也会被调用多次,这次数和前一个一样 * @param ctx 上下文对象 * @param in 入站的Bytebuf * @param out list集合,将解码后的数据传给下一个handler * @throws Exception */ @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { System.out.println("InBoundServerDecoder decode被调用"); //因为long 8个字节,需要判断有8个字节,才能读取一个long if(in.readableBytes() >= 8){ out.add(in.readLong()); } }}
-
-
OutBoundClientEncoder
-
package com.jl.java.web.inoutbound;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import io.netty.handler.codec.MessageToByteEncoder;import java.util.List;/** * @author jiangl * @version 1.0 * @date 2021/5/27 15:07 */public class OutBoundClientEncoder extends MessageToByteEncoder<Long> { @Override protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception { System.out.println("OutBoundClientEncoder encode被调用"); System.out.println("客户端准备发送 msg="+msg); out.writeLong(msg); }}
-
-
-
结论
- 不论解码器handler还是编码器handler,其接收的消息类型必须与待处理的消息类型一致,否则这个handler不会被执行
- 因为在编解码器的Handler中,会判断自己可以处理的数据类型和传入的数据类型是否一致,如果一致则这个handler会调用自己的逻辑处理数据,如果不一致则直接将数据传入给下一个handler处理数据
- 在解码器进行数据解码时,需要判断缓存(Bytebuf)的数据是否足够,否则接收到的结果会和期望的结果不一致
- 不论解码器handler还是编码器handler,其接收的消息类型必须与待处理的消息类型一致,否则这个handler不会被执行
-
解码器-ReplayingDecoder
-
public abstract class ReplayingDecoder
extends ByteToMessageDecoder -
ReplayingDecoder扩展了ByteToMessageDecoder类,使用ReplayingDecoder,开发者不必调用readableBytes()方法。参数S指定了用户状态管理的类型,其中void代表不需要状态管理
-
应用实例
-
使用ReplayingDecoder编写解码器,对前面的案例进行简化
-
代码
-
package com.jl.java.web.inoutbound;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ReplayingDecoder;import java.util.List;/** * @author jiangl * @version 1.0 * @date 2021/5/27 21:21 */public class MyByteToLongDecoder extends ReplayingDecoder<Void> { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { System.out.println("MyByteToLongDecoder 被调用"); //在ReplayingDecoder不需要判断数据是否足够读取,内部会处理判断 out.add(in.readLong()); }}
-
-
-
ReplayingDecoder使用方便,但是有局限性
- 并不是所有ByteBuf操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException。
- ReplayingDecoder在某些情况下可能稍慢于ByteToMessageDecoder,例如而网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢
其他编码器
- 其他编码器
- LineBaseFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据
- DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符
- HttpObjectDecoder:一个HTTP数据的解码器
- LengthFieldBasedFrameDecoder:通过制定长度来标识整包消息,这样就可以自动的处理黏包和半包消息