乐趣区

关于java: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
@AllArgsConstructor
public 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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

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

退出移动版