网络编解码
网络编程为什么需要编解码
我们都知道Java时们面向对象的语言,但是在网络编程中,像Java中那样的“对象”是无法进行传输的,因为在Socket 中,传输的只能是字节流。那么这便表示我们必须要将对象转换成可以在网络间进行传输的字节流。平常我们的编程中有使用过toString()这样的方法,然后再获取字节流。这样是可以将对象转换成字节流,但是经过网络传输后,接收方接收到字节流后,仍然需要将字节流还原成原始数据对象,那么也就需要类似于fromString()方法进行转换。由此我们便知道,数据的编解码是为了传输数据。
常见编解码方式
我们Java程序员都知道我们的平常开发中常常会使用到Java的序列化和反序列化来进行编码,当然也是用过一些其他的编码方式,那么这里便简单罗列一下主流的编解码方式。
1、Java序列化/反序列化
在Java开发中,为了方便存储和传输,我们常常使用的就是JDK知道的序列化机制。这样的机制使用过实现Serializable接口,来标记当前类是具有序列化的特性的。在Java中是使用ObjectOutputStream中的writeObject()来对对象进行序列化,使用ObjectInputStream中的readObject()方法来还原对象。但是这样的方式所编译的信息的可读性是很差的。
2、XML
XML的中文名称是可扩展标记语言,这种方式在之前的Web Service编程中是常使用过的一种方式,但是由于技术的更迭,现今已经很少看到只用这种方式了。
3、JSON
JSON(JavaScript Object Notation)是一种轻量的数据传递的方式,它完全采用独立于编程语言的文本格式来存储和展示数据,当前,这种格式使用的非常广泛,其主要原因无非是简洁、清晰的层次结构,便于阅读且易于机器的解析和生成,在网络传输中的效率也是很高的。
4、msgpack
msgpack是一种基于二进制、高效的对象序列化方式,msgpack也可以像JSON那样,在多重编程语言之间交换结构对象,相比于JSON而言,它还更轻巧、快速。但是msgpack和Java的序列化编码后一样,都会出现乱码,可读性比较差。
5、Marshalling
Marshalling是JBoss开发的Java对象序列化包,它是对JDK自带的系列化框架进行了优化,且能与Serializable接口保持兼容。和前面的XML、JSON不一样的是,Marshalling是Java专用的,但是和JDK自带的又不太一样,Marshalling具有定制性、可定义Stream头等特性。
6、Protobuf
ProtoBuf是有谷歌开发的一种灵活高效的数据序列化协议,相比XML、JSON而言,Protobuf文件的更小,且更快捷。Protobuf也支持多语言的编程,也自带了编译器,可以自动生成Java、Python、C++等不同编程语言的代码,但是所生成代码的可读性差,且内容冗余。
编码选择考量
- 数据编码后的空间大小
- 编解码速度是否需要高效
- 是否追求可读性
- 是否支持多语言
Netty 编解码器
前面我们对网络中为什么需要编解码和一些主流的编解码方式等进行了简单的说明,那么现在就将开始进行Netty中编解码的内容阐述了,在开始前,先来对编解码等相关词汇来进行简单的解释,如下:
- 编码(Encode):俗称序列化,是将对象序列化成字节数组,方便网络间的传输和数据持久化,或者其他用途。
- 解码(Decode):俗称反序列化,是将网络、内存中的读取到的字节数组还原成原始的数据对象,便于后续的使用
Netty中提供了提供了强大的编解码功能,这会让我们自定义编解码器会很便易,在Netty中编解码器实现了ChannelHandlerAdapter,这是一个特殊的ChannelHandler。在netty中ChannelInboundHandler负责输入数据的处理,ChannelOutboundHandler负责输出数据的处理。那么我们来看下他们的类图,如下:
解码器(Decoder)
在netty中,解码器主要是由ByteToMessageDecoder和MessageToMessageDecoder两个类提供,他们都继承了ChannelInboundHandlerAdapter适配类,我们来看一下他们的类图,如下:
有上图可知,有三个抽象解码器:ByteToMessageDecoder、ReplayingDecoder、MessageToMessageDecoder,他们的作用分别是:
- ByteToMessageDecoder:用于将字节转换为消息对象,需要检查缓冲区是否有足够的字节。
- ReplayingDecoder:继承了 ByteToMessageDecoder,但是不需要检查缓冲区是否有足够的字节,不过 ReplayingDecoder 速度要慢于ByteToMessageDecoder, 同时不支持所有的ByteBuf。
- MessageToMessageDecoder:将一个完整报文信息的Java对象转换成另一个Java对象。
1、ByteToMessageDecoder
ByteToMessageDecoder类会通过 ChannelInboundHandlerAdapter 以类似流的方式将字节信息从一个 ByteBuf 解码到一个所需要的消息对象类型。其有几个常见的实现类,如下:
- DelimiterBasedFrameDecoder:分隔符解码器,与LineBasedFrameDecoder类似,分隔符可以自己指定。
- FixedLengthFrameDecoder:定长协议解码器,可以指定固定的字节数作一个完整的报文。
- LengthFieldBasedFrameDecoder:长度编码解码器,将报文划分为报文头/报文体,可根据报文头中的Length字段确定报文体的长度,因此报文体的长度是可变的。
- LineBasedFrameDecoder:行分隔符解码器,遇到\n或者\r\n,则认为是一个完整的报文。
- JsonObjectDecoder:JSON格式解码器,当检测到匹配数量的“{” 、“}”或“[”、“]”时,则认为是一个完整的JSON报文对象。
这些实现类,都只是将接收到的二进制数据,解码成包含完整报文信息的ByteBuf实例后,就直接交给了之后的ChannelInboundHandler处理。在ByteToMessageDecoder类中唯一需要实现的抽象方法是io.netty.handler.codec.ByteToMessageDecoder#decode方法,当我们自定义解码器的时候,只需要覆盖这个方法:
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
这个方法中传入了两个参数分别是ChannelHandlerContext和List,它们的作用分别是:
- ChannelHandlerContext:需要解码的二进制数据。
- List:解码后的有效报文数据,将解码后的数据添加到这个集合中。这里用List类型的集合,主要是为了考虑黏包的问题。不过也有发生拆包的可能,当List集合中的数据不足以构成一条有效报文的时候,则不需往集合中添加。
2、MessageToMessageDecoder
ByteToMessageDecoder类是通过将二进制流信息解码成相应的有效对象数据,但是MessageToMessageDecoder则是将一个本身就包含完整报文信息的对象转换成另一个Java对象。在Netty中除了一些公有协议的解码器外,Netty提供的MessageToMessageDecoder实现类较少,主要有如下几个:
- StringDecoder:这个实现类的主要作用是将包含完整报文信息的Bytebuf转换成字符串,可以将其余ByteToMessageDecoder的一些实现类联合使用,以LineBasedFrameDecoder为例,其将二进制数据流按行分割后封装到ByteBuf中。然后在其之后再添加一个StringDecoder,便能将ByteBuf中的数据转换成字符串。
- Base64Decoder:这个类主要是用于进行Base64编码。例如前面所提到的LineBasedFrameDecoder、DelimiterBasedFrameDecoder等ByteToMessageDecoder实现类,是使用特殊字符作为分隔符作为解码的条件。但是如果报文内容中如果本身就包含了分隔符,那么解码就会出错。此时,发送方需要先使用Base64Encoder对报文内容进行Base64编码,然后选择Base64编码包含的64种字符之外的其他特殊字符作为分隔符。在解码时,首先要对特殊字符进行分割,然后再通过Base64Decoder解码来得到原始的二进制字节流。
和ByteToMessageDecoder类类似的,MessageToMessageDecoder也有一个decode方法需要覆盖 ,如下:
protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
3、ReplayingDecoder
ReplayingDecoder是继承自ByteToMessageDecoder类的一个特殊的抽象类,它支持在阻塞 I/O 范式中实现非阻塞解码器。ByteToMessageDecoder解码读取缓冲区的数据之前需要检查缓冲区是否有足够的字节,使用 ReplayingDecoder则无需自己检查,若ByteBuf中有足够的字节,则会正常读取,若没有足够的字节则会停止解码。也正是这样,使得ReplayingDecoder带有一定的局限性。
- 不是所有的操作都被ByteBuf支持,如果调用一个不支持的操作会抛出DecoderException。
- ByteBuf.readableBytes()大部分时间不会返回期望值。
编码器(Decoder)
与ByteToMessageDecoder和MessageToMessageDecoder相对应,Netty提供了对应的编码器实现MessageToByteEncoder和MessageToMessageEncoder, 二者都实现ChannelOutboundHandler接口。 类结构如下:
相对来说,编码器比解码器的实现要更加简单,原因在于解码器除了要按照协议解析数据,还要要处理粘包、拆包问题,而编码器只要将数据转换成协议规定的二进制格式发送即可。
1、MessageToByteEncoder
MessageToByteEncoder是一个可接收泛型的类,泛型参数“I”表示将需要编码的对象的类型,编码的结果是将信息转换成二进制流放入ByteBuf中。子类通过覆写其抽象方法encode来实现编码,如下所示:
protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
从上面代码中可以看到,MessageToByteEncoder的输出对象out是一个ByteBuf实例,我们应该将泛型参数msg包含的信息写入到这个out对象中。
2、MessageToMessageEncoder
MessageToMessageEncoder同样也是一个可接收泛型类,泛型参数“I”表示将需要编码的对象的类型,编码的结果是将信息放到一个List中。子类通过覆写其抽象方法encode,来实现编码,如下所示:
protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
和MessageToByteEncoder不同的,MessageToMessageEncoder编码后的结果放到的out参数中的是一个List。例如,你一次发送2个报文,因此msg参数中实际上包含了2个报文,因此应该解码出两个报文对象放到List中。MessageToMessageEncoder提供的常见子类有如下几个:
- LineEncoder:按行编码,给定一个CharSequence(如String),在其之后添加换行符\n或者\r\n,并封装到ByteBuf进行输出,与LineBasedFrameDecoder相对应。
- Base64Encoder:给定一个ByteBuf,得到对其包含的二进制数据进行Base64编码后的新的ByteBuf进行输出,与Base64Decoder相对应。
- LengthFieldPrepender:给定一个ByteBuf,为其添加报文头Length字段,得到一个新的ByteBuf进行输出。Length字段表示报文长度,与LengthFieldBasedFrameDecoder相对应。
- StringEncoder:给定一个CharSequence(如:StringBuilder、StringBuffer、String等),将其转换成ByteBuf进行输出,与StringDecoder对应。这些MessageToMessageEncoder实现类最终输出的都是ByteBuf,因为最终在网络上传输的都要是二进制数据。
编码解码器(Codec)
编码解码器同时具有编码与解码功能,特点同时实现了ChannelInboundHandler和ChannelOutboundHandler接口,因此在数据输入和输出时都能进行处理。Netty提供提供了一个ChannelDuplexHandler适配器类,编码解码器的抽象基类 ByteToMessageCodec 、MessageToMessageCodec都继承于此类,类结构图如下:
ByteToMessageCodec内部维护了一个ByteToMessageDecoder和一个MessageToByteEncoder实例,可以认为是二者的集合,泛型参数I则是接受的编码类型,代码如下:
public abstract class ByteToMessageCodec<I> extends ChannelDuplexHandler {
private final TypeParameterMatcher outboundMsgMatcher;
private final MessageToByteEncoder<I> encoder;
private final ByteToMessageDecoder decoder = new ByteToMessageDecoder(){…}
// 省略非相关代码
protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
// 省略非相关代码
}
MessageToMessageCodec内部维护了一个MessageToMessageDecoder和一个MessageToMessageEncoder实例,可以认为是二者的集合,泛型参数 INBOUND_IN和OUTBOUND_IN分别表示需要解码和编码的数据类型。代码如下:
public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN> extends ChannelDuplexHandler {
private final MessageToMessageEncoder<Object> encoder = new MessageToMessageEncoder<Object>() {
@Override
public boolean acceptOutboundMessage(Object msg) throws Exception {
return MessageToMessageCodec.this.acceptOutboundMessage(msg);
}
@Override
@SuppressWarnings("unchecked")
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
MessageToMessageCodec.this.encode(ctx, (OUTBOUND_IN) msg, out);
}
};
private final MessageToMessageDecoder<Object> decoder = new MessageToMessageDecoder<Object>() {
@Override
public boolean acceptInboundMessage(Object msg) throws Exception {
return MessageToMessageCodec.this.acceptInboundMessage(msg);
}
@Override
@SuppressWarnings("unchecked")
protected void decode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
MessageToMessageCodec.this.decode(ctx, (INBOUND_IN) msg, out);
}
};
// 省略非相关代码
protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List<Object> out) throws Exception;
protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List<Object> out) throws Exception;
}