写点什么

netty 系列之:netty 中常用的对象编码解码器

作者:程序那些事
  • 2022 年 4 月 24 日
  • 本文字数:6574 字

    阅读完需:约 22 分钟

netty系列之:netty中常用的对象编码解码器

简介

我们在程序中除了使用常用的字符串进行数据传递之外,使用最多的还是 JAVA 对象。在 JDK 中,对象如果需要在网络中传输,必须实现 Serializable 接口,表示这个对象是可以被序列化的。这样就可以调用 JDK 自身的对象对象方法,进行对象的读写。


那么在 netty 中进行对象的传递可不可以直接使用 JDK 的对象序列化方法呢?如果不能的话,又应该怎么处理呢?


今天带大家来看看 netty 中提供的对象编码器。

什么是序列化

序列化就是将 java 对象按照一定的顺序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的 java 对象。


所以序列化的目的就是为了传输对象,对于一些复杂的对象,我们可以使用第三方的优秀框架,比如 Thrift,Protocol Buffer 等,使用起来非常的方便。


JDK 本身也提供了序列化的功能。要让一个对象可序列化,则可以实现 java.io.Serializable 接口。


java.io.Serializable 是从 JDK1.1 开始就有的接口,它实际上是一个 marker interface,因为 java.io.Serializable 并没有需要实现的接口。继承 java.io.Serializable 就表明这个 class 对象是可以被序列化的。


@Data@AllArgsConstructorpublic class CustUser implements java.io.Serializable{    private static final long serialVersionUID = -178469307574906636L;    private String name;    private String address;}
复制代码


上面我们定义了一个 CustUser 可序列化对象。这个对象有两个属性:name 和 address。


接下看下怎么序列化和反序列化:


public void testCusUser() throws IOException, ClassNotFoundException {        CustUser custUserA=new CustUser("jack","www.flydean.com");        CustUser custUserB=new CustUser("mark","www.flydean.com");
try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); objectOutputStream.writeObject(custUserB); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUser custUser1 = (CustUser) objectInputStream.readObject(); CustUser custUser2 = (CustUser) objectInputStream.readObject(); log.info("{}",custUser1); log.info("{}",custUser2); } }
复制代码


上面的例子中,我们实例化了两个 CustUser 对象,并使用 objectOutputStream 将对象写入文件中,最后使用 ObjectInputStream 从文件中读取对象。


上面是最基本的使用。需要注意的是 CustUser class 中有一个 serialVersionUID 字段。


serialVersionUID 是序列化对象的唯一标记,如果 class 中定义的 serialVersionUID 和序列化存储中的 serialVersionUID 一致,则表明这两个对象是一个对象,我们可以将存储的对象反序列化。


如果我们没有显示的定义 serialVersionUID,则 JVM 会自动根据 class 中的字段,方法等信息生成。很多时候我在看代码的时候,发现很多人都将 serialVersionUID 设置为 1L,这样做是不对的,因为他们没有理解 serialVersionUID 的真正含义。

重构序列化对象

假如我们有一个序列化的对象正在使用了,但是突然我们发现这个对象好像少了一个字段,要把他加上去,可不可以加呢?加上去之后原序列化过的对象能不能转换成这个新的对象呢?


答案是肯定的,前提是两个版本的 serialVersionUID 必须一样。新加的字段在反序列化之后是空值。

序列化不是加密

有很多同学在使用序列化的过程中可能会这样想,序列化已经将对象变成了二进制文件,是不是说该对象已经被加密了呢?


这其实是序列化的一个误区,序列化并不是加密,因为即使你序列化了,还是能从序列化之后的数据中知道你的类的结构。比如在 RMI 远程调用的环境中,即使是 class 中的 private 字段也是可以从 stream 流中解析出来的。


如果我们想在序列化的时候对某些字段进行加密操作该怎么办呢?


这时候可以考虑在序列化对象中添加 writeObject 和 readObject 方法:


private String name;    private String address;    private int age;
private void writeObject(ObjectOutputStream stream) throws IOException { //给age加密 age = age + 2; log.info("age is {}", age); stream.defaultWriteObject(); }
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); log.info("age is {}", age); //给age解密 age = age - 2; }
复制代码


上面的例子中,我们为 CustUser 添加了一个 age 对象,并在 writeObject 中对 age 进行了加密(加 2),在 readObject 中对 age 进行了解密(减 2)。


注意,writeObject 和 readObject 都是 private void 的方法。他们的调用是通过反射来实现的。

使用真正的加密

上面的例子, 我们只是对 age 字段进行了加密,如果我们想对整个对象进行加密有没有什么好的处理办法呢?


JDK 为我们提供了 javax.crypto.SealedObject 和 java.security.SignedObject 来作为对序列化对象的封装。从而将整个序列化对象进行了加密。


还是举个例子:


public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {        CustUser custUserA=new CustUser("jack","www.flydean.com");        Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");        Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");        SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES");        IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes());        enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);        deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv);        SealedObject sealedObject= new SealedObject(custUserA, enCipher);
try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(sealedObject); }
try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); SealedObject custUser1 = (SealedObject) objectInputStream.readObject(); CustUser custUserV2= (CustUser) custUser1.getObject(deCipher); log.info("{}",custUserV2); } }
复制代码


上面的例子中,我们构建了一个 SealedObject 对象和相应的加密解密算法。


SealedObject 就像是一个代理,我们写入和读取的都是这个代理的加密对象。从而保证了在数据传输过程中的安全性。

使用代理

上面的 SealedObject 实际上就是一种代理,考虑这样一种情况,如果 class 中的字段比较多,而这些字段都可以从其中的某一个字段中自动生成,那么我们其实并不需要序列化所有的字段,我们只把那一个字段序列化就可以了,其他的字段可以从该字段衍生得到。


在这个案例中,我们就需要用到序列化对象的代理功能。


首先,序列化对象需要实现 writeReplace 方法,表示替换成真正想要写入的对象:


public class CustUserV3 implements java.io.Serializable{
private String name; private String address;
private Object writeReplace() throws java.io.ObjectStreamException { log.info("writeReplace {}",this); return new CustUserV3Proxy(this); }}
复制代码


然后在 Proxy 对象中,需要实现 readResolve 方法,用于从系列化过的数据中重构序列化对象。如下所示:


public class CustUserV3Proxy implements java.io.Serializable{
private String data;
public CustUserV3Proxy(CustUserV3 custUserV3){ data =custUserV3.getName()+ "," + custUserV3.getAddress(); }
private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); CustUserV3 result = new CustUserV3(pieces[0], pieces[1]); log.info("readResolve {}",result); return result; }}
复制代码


我们看下怎么使用:


public void testCusUserV3() throws IOException, ClassNotFoundException {        CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com");
try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); }
try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject(); log.info("{}",custUser1); } }
复制代码


注意,我们写入和读出的都是 CustUserV3 对象。

Serializable 和 Externalizable 的区别

最后我们讲下 Externalizable 和 Serializable 的区别。Externalizable 继承自 Serializable,它需要实现两个方法:


 void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
复制代码


什么时候需要用到 writeExternal 和 readExternal 呢?


使用 Serializable,Java 会自动为类的对象和字段进行对象序列化,可能会占用更多空间。而 Externalizable 则完全需要我们自己来控制如何写/读,比较麻烦,但是如果考虑性能的话,则可以使用 Externalizable。


另外 Serializable 进行反序列化不需要执行构造函数。而 Externalizable 需要执行构造函数构造出对象,然后调用 readExternal 方法来填充对象。所以 Externalizable 的对象需要一个无参的构造函数。

netty 中对象的传输

在上面的序列化一节中,我们已经知道了对于定义好的 JAVA 对象,我们可以通过使用 ObjectOutputStream 和 ObjectInputStream 来实现对象的读写工作,那么在 netty 中是否也可以使用同样的方式来进行对象的读写呢?


很遗憾的是,在 netty 中并不能直接使用 JDK 中的对象读写方法,我们需要对其进行改造。


这是因为我们需要一个通用的对象编码和解码器,如果使用 ObjectOutputStream 和 ObjectInputStream,因为不同对象的结构是不一样的,所以我们在读取对象的时候需要知道读取数据的对象类型才能进行完美的转换。


而在 netty 中我们需要的是一种更加通用的编码解码器,那么应该怎么做呢?


还记得之前我们在讲解通用的 frame decoder 中讲过的 LengthFieldBasedFrameDecoder? 通过在真实的数据前面加上数据的长度,从而达到根据数据长度进行 frame 区分的目的。


netty 中提供的编码解码器名字叫做 ObjectEncoder 和 ObjectDecoder,先来看下他们的定义:


public class ObjectEncoder extends MessageToByteEncoder<Serializable> {
复制代码


public class ObjectDecoder extends LengthFieldBasedFrameDecoder {
复制代码


可以看到 ObjectEncoder 继承自 MessageToByteEncoder,其中的泛型是 Serializable,表示 encoder 是从可序列化的对象 encode 成为 ByteBuf。


而 ObjectDecoder 正如上面我们所说的继承自 LengthFieldBasedFrameDecoder,所以可以通过一个长度字段来区分实际要读取对象的长度。


接下来我们详细了解一下这两个类是如何工作的。

ObjectEncoder

先来看 ObjectEncoder 是如何将一个对象序列化成为 ByteBuf 的。


根据 LengthFieldBasedFrameDecoder 的定义,我们需要一个数组来保存真实数据的长度,这里使用的是一个 4 字节的 byte 数组叫做 LENGTH_PLACEHOLDER,如下所示:


private static final byte[] LENGTH_PLACEHOLDER = new byte[4];
复制代码


我们看下它的 encode 方法的实现:


    protected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception {        int startIdx = out.writerIndex();
ByteBufOutputStream bout = new ByteBufOutputStream(out); ObjectOutputStream oout = null; try { bout.write(LENGTH_PLACEHOLDER); oout = new CompactObjectOutputStream(bout); oout.writeObject(msg); oout.flush(); } finally { if (oout != null) { oout.close(); } else { bout.close(); } } int endIdx = out.writerIndex(); out.setInt(startIdx, endIdx - startIdx - 4); }
复制代码


这里首先创建了一个 ByteBufOutputStream,然后向这个 Stream 中写入 4 字节的长度字段,接着将 ByteBufOutputStream 封装到 CompactObjectOutputStream 中。


CompactObjectOutputStream 是 ObjectOutputStream 的子类,它重写了 writeStreamHeader 和 writeClassDescriptor 两个方法。


CompactObjectOutputStream 将最终的数据 msg 写入流中,一个 encode 的过程就差不多完成了。


为什么说差不多完成了呢?因为长度字段还是空的。


在最开始的时候,我们只是写入了一个长度的 placeholder,这个 placeholder 是空的,并没有任何数据,这个数据是在最后一步 out.setInt 中写入的:


out.setInt(startIdx, endIdx - startIdx - 4);
复制代码


这种实现也给了我们一种思路,在我们还不知道消息的真实长度的时候,如果希望在消息之前写入消息的长度,可以先占个位置,等消息全部读取完毕,知道真实的长度之后,再替换数据。


到此,对象数据已经全部编码完毕,接下来我们看一下如何从编码过后的数据中读取对象。

ObjectDecoder

之前说过了 ObjectDecoder 继承自 LengthFieldBasedFrameDecoder,它的 decode 方法是这样的:


    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {        ByteBuf frame = (ByteBuf) super.decode(ctx, in);        if (frame == null) {            return null;        }
ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver); try { return ois.readObject(); } finally { ois.close(); } }
复制代码


首先调用 LengthFieldBasedFrameDecoder 的 decode 方法,根据对象的长度,读取到真实的对象数据放到 ByteBuf 中。


然后通过自定义的 CompactObjectInputStream 从 ByteBuf 中读取到真实的对象,并返回。


CompactObjectInputStream 继承自 ObjectInputStream,是和 CompactObjectOutputStream 相反的操作。

ObjectEncoderOutputStream 和 ObjectDecoderInputStream

ObjectEncoder 和 ObjectDecoder 是对象和 ByteBuf 之间的转换,netty 还提供了和 ObjectEncoder,ObjectDecoder 兼容的 ObjectEncoderOutputStream 和 ObjectDecoderInputStream,这两个类可以从 stream 中对对象编码和解码,并且和 ObjectEncoder,ObjectDecoder 完全兼容的。

总结

以上就是 netty 中提供的对象编码和解码器,大家如果希望在 netty 中传递对象,那么 netty 提供的这两个编码解码器是最好的选择。


本文已收录于 http://www.flydean.com/14-8-netty-codec-object/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 刚刚阅读数: 2
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:netty中常用的对象编码解码器_Java_程序那些事_InfoQ写作社区