上篇文章当中,我们讲到了半包和粘包,半包和粘包我们也叫封帧,封帧就是解决我们的半包和粘包的问题。
上边我们讲到的整个类都是以Decoder结尾的类,Decoder是解码器,那么编码器是Encoder,这是编码器。编码器在Netty当中是单独的一个章节。
一:编解码器概念
编解码器是处理数据的,Netty把网络通信的核心内容都封装好了,我们需要处理什么呢?我们需要处理通信中所要传递的数据处理,数据处理就是编解码器。这个是我们必须要掌握的。
1:什么是编解码
编解码肯定涉及到网络通信,网络通信涉及到两端,客户端和服务端。编解码器是什么呢?站在Java的体系当中(其他的体系是同样的一回事,我们熟悉Java,按Java来讲。)实际上在客户端我们是基于面向对象的思想来编程的,实际上使用的是一个一个的Java对象,也就是一个一个的Java类型。最终呢这些数据进行通信的数据也是要封装成Java对象的,然而我们网络通信的时候到底认不认可这些Java对象呢?显然是不认可的,到了Socket,甚至是网卡这种非常底的层次当中,他们认为一切都是二进制,真正通信的是这些字节。对于网络来讲,通信的是字节,所以只有把Java对象转换为字节,最终才能通过网络这种形式发送给服务器端,这个时候就需要把我们的Java的对象转换为二进制。
对于我们的服务端来讲,网络给我们的服务端是什么?是二进制,所以对于我们服务端来讲我们首先要做的就是将这些二进制转换为Java对象。站在客户端和服务器端的角度来讲都有这样的一个过程,且这两个过程是互逆的–Java对象和二进制之间的互转。那我们Java对象转换为二进制,就是编码;将二进制数据转换为Java对象就是解码。
这个过程在我们的Java网路通信过程当中是非常重要的且不可或缺的。作为广义的编解码来讲,是有两部分组成的。第一部分就是我们所说的Encode,另外一部分就是我们所属的Decode。在Netty的体系当中我们的编解码是怎么体现的呢?
二:Netty中的编解码
1:Netty当中编解码体现
在Netty当中的编解码的体现,实际上跟我们广义上的编解码是完全一样,就是Java对象和二进制之间的互转。
2:Netty当中编解码承担者
在Netty当中承担编解码的工作是谁做的呢?-- 操作我们数据的核心 Handler,而且是ChannelHandler。ByteToMessageCodec
public abstract class ByteToMessageCodec<I> extends ChannelDuplexHandler(支持编解码) 一个用于对字节到消息进行即时编码/解码的编解码器,反之亦然。 这可以被认为是{ByteToMessageDecoder}和{ MessageToByteEncoder}的组合。 请注意,{ByteToMessageCodec}的子类一定不要注释{@MessageEncoder} 注解的子类,要用{@link @Sharable}来注释。
ByteToMessageCodec的父类是ChannelDuplexHandler,顶级父类是ChannelHandler说明了这个类本质上是Handler,实际开发当中的编码器和解码器就是这个类的子类。抽象类无法实例化,我们无法创建他的对象,他的子类有很多。
我们做的编解码器分别是上边这两个类的子类。ByteToMessageCodec是上边这两个类的组合,在编程过程中我们使用这两个类。
3:编解码器使用过程中两个核心内容
1):序列化协议
序列化协议又叫做(编码格式),指的就是在网络通信过程中传输数据的格式。最时髦的说法就叫做序列化协议(编码)或者叫反序列化协议(解码)
使用什么样的格式进数据的网络的传输,数据的格式有两大门类:第一种是二进制的门类也叫字节门类,在应用层开发过程中网络传输时事Byte二进制数据,可读性很差,好处就是传输过程使用Byte二进制序列化协议的传输效率是很高的。另外一种我们在传输的过程中使用字符的这种协议,这种方式可读性很好但是传输效率很低。
传输的协议是可以根据我们的需求在若干个门类当中进行选择的,完全取决于我们自己的设计。
A:常见的序列号协议
a:Java的序列化
Java的序列化可以把Java的对象编程二进制存储在文件中,或者是把二进制通过网络进行传输,返序列化就是从文件中或者网络中还原内容到对象,这就是Java的序列化和反序列化协议。
1:我们想用Java的序列化,我们必须有一个要求,这个要求必须让我们的类实现一个Serializable接口,这是一个标识性接口
2:然后我们通过ObjectOutputStream和ObjectInputStream进行序列化和反序列化。实现Java对象和二进制数据之间的转换,这个二进制既可以存储到文件中,也可以传输到网络中。同样也可以从文件或者网络中去取。
3:serialVersionUID 序列化的唯一标识,而且我们还要把他声明成private static final long这个版本号,如果我在一个类当中不写这个值的话,Java在序列化当中会默认创建一个序列化标识,一般就是这个对象的hashCode,一个对象的HashCode不同对象的HashCode是唯一的。那为什么我们还要默认显示声明这个东西呢?反序列化 的时候我们需要读取这个类的内容还需要反显这个对象中的数据,为什么要有这个值而且不能用这个默认的呢,反序列化的时候会对这个版本做比对,如果序列化之后类被改了那么版本就对不上了,反序列化就会失败。HashCode是根据所有属性参与构造出来的一个值。所以,改动后就版本就不一致,反序列化就失败了。为了解决这个问题,我们就是需要显示的将这个Hash值指定出来,我们这个UID是一致的。这样的话,如果我们类中加了属性,但是我们的版本号没变,这样的话Javav比对版本号还是一致的,就可以反序列化成功,只不过新加的属性值是没有的。
所以,我们基于以上说法我们开发过程中需要有几个好的开发习惯,实现序列化接口,提供默认的序列化标识ID,显示提供无参构造方法,防止有人加了有参构造方法之后,反射的时候会报错。
我们遵从了这种设计方式之后,就可以在我们的程序当中通过Java的序列化和反序列化来充当我们的网络通信的序列化协议了。客户端用的Java服务端用的也是Java,网络传输也是基于二进制的,Java的对象基于ObjectOutputStream序列化为二进制通过网络传输给服务端,服务端接收到之后基于ObjectInputStream将二进制数据反序列化为Java对象,来继续使用即可了。
Java序列化的问题:
1:Java序列化和反序列化的方式是无法跨语言的。比如我们客户端是基于Java写的,服务端是基于Python写的。实际上只要尊徐网络传输的规则基于ip和端口建立了连接,就可以进行通信的。只不过如果使用Java序列化协议,Java对象二进制转换之后,通过网络传输给Python是无法反序列化的。
2:可读性查。网络传输过程中是二进制的,可读性很差。
3:Java序列化后的数据大小太大了。所谓的Java序列化就是将Java对象转换为字节,Nio当中的ByteBuffer也可以将Java对象转换为字节,前者是后者的5倍。网络传输过程中,数据量太大,传输效率就低
4:Java序列化操作的时间也是ByteBuffer的5倍。
标识性接口:起到的就是一个标示性作用,接口中无方法让我们实现。就是一个Tag Interface,其他的标识性接口还有Spring的ThrowsAdvice
b:XML
处理繁琐
c:JSON
现在整个通信过程中主流通信协议就是JSON。
1:可读性好
2:JSON这种字符串形式进行序列化,数据量比较大,大于二进制的内容。
我们在应用层开发过程中,如果我们走Http协议的话,我们一定配的是JSON
d:msgpack
数据格式来讲类似于JSON,传输数据过程中类似于二进制传输,传输效率很高。数据体量很小。传输过程中把JSON当中的{},""都给去了,所以数据量更小。这是二进制的,并且支持多语言。
bson是mongodb的二进制数据。
e:protobuf
Google提供的二进制传输协议。支持多种编程语言,有自己的编译器,把数据格式编译程中间语言。就可以跨语言解析了。相对来讲二进制体量更小,传输更快,可读性比较查。
Hadoop基于此作为序列化协议。
Dubbo默认的序列化方式是hession,也可以选protobuf
以上用的较多的就是JSON和protobuf
序列化协议仅仅解决的一个网络传输过程中的一个数据格式的问题。不管是用普通的序列化,还是JSON,是不是这个数据怎么进行转换的,就是我们的编解码做的了。
序列化协议都是针对于应用层的角度,往网络传输的时候都是二进制,这里说的是应用层写出的数据是Byte还是字符串。站在写程序的角度,我们可以分为往外写的是字符串还是二进制。字符串这种形式转换的速度慢,传输的数据量较大。
字节一个字节就是8位,字节就是二进制。JSON最终也要转换为二进制,字符串也是如此,字符串转换为的字节的时候也要将他的特殊字符和格式的一些东西也要转换为字节,所以他会大一点。直接字节就是直接通过IO流写出字节流转换为二进制,非直接字节就是将数据线转化为字符流再通过字节流转换为字节。
Netty是传输层,我们写的数据是应用层(Http协议)。Netty传输层对应的协议是TCP协议。
2):具体的编解码器
我们的序列化协议指定之后,就可以选择我们具体的编解码器了。编解码器具体使用者两个过程当中。
在Netty的体系当中就是两个类的子类型,不论我们的序列化协议选择的是什么,我们都可以通过,Netty当中的两个类,去进行编解码。
ByteToMessageDecoder
MessageToByteEncoder
4:Netty常见的编解码器
1):Netty中两个编解码体系
在Netty的体系当中,编解码器是有两大流派的,第一个流派是ByteToMessageDecoder和MessageToByteEncoder,另外的一个体系就是MessageToMessageDecoder和MessageToMessageEncoder。
2):Netty当中两个编解码体系的区别
1:两种编解码器ByteToMessageDecoder和MessageToByteEncoder更加的底层。我们以解码为例,ByteToMessageDecoder中的decode方法。MessageToMessageDecoder当中decoder方法是有一个泛型的,这里有泛型就以为着作为这个类的子类也是要带着这个泛型的。我们这个泛型既可以之指定,通过这一点我们可以得到一个结论,Byte这种是更为底层的,因为只能操作ByteBuffer这种类型。MessageToMessageDecoder层面较高。
任意类型做转换说明这个类型说明已经被转换过一次了,说明他肯定不是底层。
MessageToMessageDecoder解码器的decode方法:
protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
ByteToMessageDecoder解码器的decode方法:
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
这里用到的是什么设计模式?并不是适配器模式,这里用到的是模板设计模式。
2:public class StringDecoder extends MessageToMessageDecoder{}将泛型制定成了ByteBuf从这个角度来分析,StringDecoder在设计过程当中这个Decode方法和ByteToMessageDecoder当中的decode方法功能是一样的,因为指定了泛型是ByteBuf,这里边肯定有别的区别。这个区别就是Byte体系才会解决封帧(半包粘包)的问题,Message体系不解决封帧的问题,所以,我们使用我们使用MessageToMessageDecoder解码器的话,就必须在这之前使用我们的上节课讲到的解决半包和粘包问题的解码器,这里也能体现出他的层次高因为前边确实有人帮他处理。这是他的第二个特点。所有的MessageToMessage门类都比必须加,我们要尽量少用这玩意。
3):StringDecoder和StringEncoder
编解码器站在Netty的体系当中,我们就是这两个类型。我们使用的编解码器都是上述这两个类的子类。Netty当中我们已经见过了StringDecoder和StringEncoder这是Netty为我们提供的第一组编解码器,能进行字符串的转换。注意:StringDecoder的父类:StringDecoder
StringDecoder的父类并不是ByteToMessageDecoder,在Netty的体系当中,编解码器是有两大流派的,第一个流派是ByteToMessageDecoder和MessageToByteEncoder,另外的一个体系就是MessageToMessageDecoder和MessageToMessageEncoder。
4):封帧相关的编解码器
FixLengthFrameDecoder
LineBasedFrameDecoder
LengthFieldBasedFrameDecoder
5):Java相关的编解码器
ObjectEncoder和ObjectDecoder。如果我们想要在Netty体系当中去使用Java序列化作为序列化协议的话,我们就不需要手动去调用ObjectOutputSream和ObjectInputStream来进行序列化和反序列化了,直接通过这二者即可。
ObjectDecoder是LengthFieldBasedFrameDecoder的子类,会默认帮我们解决封帧的问题,所以不会有半包和粘包的问题,当我们日后在后续进行编解码的问题的时候,首先需要考虑的问题就是是否有半包和粘包。作为Netty的客户端。
我们通过channel.writeAndFlush将user对象写出到服务端的时候,是通过Java序列化将对象转换为二进制数据,然后讲数据通过网络传输给服务端,这个时候不会有半包和粘包的问题。服务端这块该怎么做,直接通过ObejctDecoder是需要一个参数的ClassResolvers.cacheDisabled(null);因为我们已经前边经过了解码,所以我们这边拿到的就直接就是User对象,我们经过强转,然后通过转换服务端就能够使用这个对象了。
客户端代码:
public class MyNettyClient { private static final Logger log = LoggerFactory.getLogger(MyNettyClient.class); public static void main(String[] args) throws InterruptedException, JsonProcessingException { log.debug("myNettyClientStarter------"); EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); bootstrap.channel(NioSocketChannel.class); Bootstrap group = bootstrap.group(eventLoopGroup);//32 ---> 1 IO操作 31线程 bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new LoggingHandler()); //ch.pipeline().addLast(new ObjectEncoder()); } }); Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel(); User user = new User(1, "sunshuai"); //user ---> ObjectEncoder user java序列化 二级制 ---> 服务端 ObjectMapper objectMapper = new ObjectMapper(); String userJSON = objectMapper.writer().writeValueAsString(user); ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); buffer.writeCharSequence(userJSON, Charset.defaultCharset()); channel.writeAndFlush(buffer); } }
服务端代码:
public class MyNettyServer { private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class); public static void main(String[] args) { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.group(new NioEventLoopGroup()); //接受socket缓冲区大小 等同于 滑动窗口的初始值 65535 //serverBootstrap.option(ChannelOption.SO_RCVBUF, 100); //netty创建ByteBuf时 执行大小 默认1024 child ScoketChannel相关 serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16)); serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() { @Override // protected void initChannel(NioSocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null))); pipeline.addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { User user = (User)msg; log.debug("{}",user); super.channelRead(ctx, msg); } }); pipeline.addLast(new LoggingHandler()); } }); // serverBootstrap.bind(8000); } }
user对象:
package com.suns.netty08; import java.io.Serializable; public class User implements Serializable { private Integer id; private String name; public User(Integer id, String name) { this.id = id; this.name = name; } public User() { } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + '}'; } public void setName(String name) { this.name = name; } }
这就是Java的序列化的编解码体系,底层使用的还是Java的Serializable机制。这个编解码器首先是Netty提供的,Java在做序列化的时候,对于Java序列化做了一系列的优化,优化后的数据量会变小。
6):JSON相关的编解码器
Netty来讲没有提供JSON的编码器,只提供了Netty的解码器。JSON的解码器叫做JsonObjectDecoder,为什么不需要JSON的编码器,因为JSON的本质上就是字符串,我直接输出字符串就好了,管你怎么编码的?根本没必要给JSON字符串搞一个编码器。
我们编码JSON的时候需要一个JSON的库,gson,jackson,fastjson,jsonlib都是比较常规的。
Mac和Linux的编码默认都是UTF-8的所以这个时候使用默认编码是没有问题的,但是我们在这里还是要手动指定一下编码集。
JsonObjectDecoder是用来解码的,他实际上将JSON数据转换为了ByteBuf,而并不是直接将JSON字符串直接放到了User对象当中,而不是将数据转换到了ByteBuf当中。JsonObjectDecoder所谓的解码只不过是解了码之后放到了ByteBuf当中了,那么这个感觉并没有做什么事,我们直接把他注释掉仿佛也没啥事。所以,JSON的解码器到底是干什么的呢?JSON解码器真正的目的,针对于JSON数据做封帧的。
我们故意调小咱们服务端接收数据的长度,这个情况下一定会发生半包和粘包的问题。我们如果不加这个JSO的解码器就会发生两次接收(分两次接收去处理,这里我们绝不允许,就相当于拿着半截报文去处理业务去了),发生半包的情况。
日后传JSON数据一定要注意这个问题,使用这个做封帧,如果我们不用这个做封帧是不是可以,可以的,但是如果不用这个做封帧的话,我们需要使用头体的那个进行封帧即可,只不过需要加头信息,数据量增大。
客户端代码:
public class MyNettyClient1 { private static final Logger log = LoggerFactory.getLogger(MyNettyClient1.class); public static void main(String[] args) throws InterruptedException { log.debug("myNettyClientStarter------"); EventLoopGroup eventLoopGroup = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); bootstrap.channel(NioSocketChannel.class); Bootstrap group = bootstrap.group(eventLoopGroup);//32 ---> 1 IO操作 31线程 bootstrap.handler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new LoggingHandler()); ch.pipeline().addLast(new ObjectEncoder()); } }); Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel(); User user = new User(1, "sunshuai"); //gson jackson jsonlib channel.writeAndFlush(user); } }
服务端代码:
public class MyNettyServer1 { private static final Logger log = LoggerFactory.getLogger(MyNettyServer1.class); public static void main(String[] args) { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.channel(NioServerSocketChannel.class); serverBootstrap.group(new NioEventLoopGroup()); //接受socket缓冲区大小 等同于 滑动窗口的初始值 65535 //serverBootstrap.option(ChannelOption.SO_RCVBUF, 100); //netty创建ByteBuf时 执行大小 默认1024 child ScoketChannel相关 serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16)); serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() { @Override // protected void initChannel(NioSocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //JsonObjectDecoder JSON解码 ---> json--java 错误认真 // JSON解码 ---> ByteBuf // 解决JSON的封帧 pipeline.addLast(new JsonObjectDecoder()); //JSON解码器 干什么? pipeline.addLast(new ChannelInboundHandlerAdapter() { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //User user = (User)msg; //log.debug("{}",user); ByteBuf byteBuf = (ByteBuf) msg; String userJSON = byteBuf.toString(Charset.defaultCharset()); ObjectMapper objectMapper = new ObjectMapper(); User user = objectMapper.readValue(userJSON, User.class); log.debug("{}", userJSON); log.debug("user object is {} ", user); super.channelRead(ctx, msg); } }); pipeline.addLast(new LoggingHandler()); } }); // serverBootstrap.bind(8000); } }
编解码器就是一种特殊的Handler,他值解决一个问题,只解决一个Byte到我们的Message之间的转换,Byte到Message是解码
像StringDecoder和StringEncoder这种东西我们在是实际开发过程中用的很少,因为这里有坑,我们需要时刻记得去给他进行封帧。






