【Netty】「项目实战」(三)序列化算法选型对聊天室可扩展性的影响
前言
本篇博文是《从0到1学习 Netty》中实战系列的第三篇博文,主要内容是围绕不同的序列化算法对聊天室的可扩展性影响展开讨论,并涉及自定义配置、可扩展测试和 BUG 解决等关键方面,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;
序列化算法
在 Netty 中,常用的序列化算法有以下几种:
-
Java 序列化:Java 自带的序列化机制,通过实现
java.io.Serializable
接口来实现对象的序列化和反序列化,使用方便,但性能较差,序列化后的数据较大。 -
JSON 序列化:JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于网络传输和存储。Netty 可以通过集成第三方库(如 Jackson、Gson)来实现对象到 JSON 字符串的序列化和反序列化。
-
Protobuf 序列化:Protobuf(Protocol Buffers)是 Google 开发的一种高效的序列化框架,可以将结构化数据编码为紧凑且高效的二进制格式。Netty 提供了对 Protobuf 的原生支持,可以直接集成并使用。
-
MessagePack 序列化:MessagePack 是一种高效的二进制序列化格式,具有很好的性能和空间效率。Netty 也可以通过集成 MessagePack 相关的库来实现对象的序列化和反序列化。
这些序列化算法各有优缺点,选择合适的序列化算法取决于具体的应用场景和需求。例如,如果需要跨平台的互操作性,可以选择 JSON 或 Protobuf 等通用的序列化方式;如果追求最高的性能和空间效率,可以尝试使用 MessagePack 等紧凑的二进制序列化格式。
需要完整代码的读者请访问博主的 Github:Serializer;
Java 序列化
serialize
方法的步骤如下:
1、先创建一个字节数组输出流 ByteArrayOutputStream
对象 bos
。
ByteArrayOutputStream bos = new ByteArrayOutputStream();
2、再创建一个 ObjectOutputStream
对象 oos
,将 bos
作为参数传递给它的构造函数。ObjectOutputStream
是用于将对象写入流中的类。
ObjectOutputStream oos = new ObjectOutputStream(bos);
3、然后将传入的对象 object
写入到 oos
中,实现对象的序列化。
oos.writeObject(object);
4、最后将 bos
转换为字节数组,并将其作为结果返回。
return bos.toByteArray();
deserialize
方法的步骤如下:
1、先创建一个字节数组输入流 ByteArrayInputStream
对象 bis
,将需要反序列化的字节数组 bytes
作为参数传递给它的构造函数。
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
2、再创建一个 ObjectInputStream
对象 ois
,将 bis
作为参数传递给它的构造函数。ObjectInputStream
是用于从流中读取对象的类。
ObjectInputStream ois = new ObjectInputStream(bis);
3、然后从 ois
中读取对象,并将其强制转换为泛型类型 T
。这里使用泛型 T
来保留原始对象的类型信息。
(T) ois.readObject();
4、将步骤3的反序列化后的对象作为结果返回。
需要注意以下几点:
-
序列化和反序列化方法都使用了泛型
<T>
,使得这两个方法可以用于不同类型的对象。 -
serialize
方法将对象转换为字节数组,而deserialize
方法将字节数组转换回原始对象。 -
序列化过程中,被序列化的对象必须实现
Serializable
接口,否则会抛出NotSerializableException
异常。 -
反序列化过程中,如果传入的字节数组无法正确反序列化为指定类型的对象,会抛出
ClassNotFoundException
异常。
JSON 序列化
引入相关依赖:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
serialize
方法的步骤如下:
1、首先,创建一个新的 Gson
实例,并调用其 toJson
方法将对象转换为 JSON 字符串表示。
String json = new Gson().toJson(object);
2、然后,使用字符串的 getBytes
方法将 JSON 字符串转换为字节数组,并指定字符编码为 UTF-8。
return json.getBytes(StandardCharsets.UTF_8);
3、最后,返回得到的字节数组作为结果。
deserialize
方法的步骤如下:
1、首先,将字节数组通过指定的 UTF-8 字符编码转换为字符串。
String json = new String(bytes, StandardCharsets.UTF_8);
2、然后,使用 Gson 的 fromJson
方法将字符串转换为目标对象的实例,并将其返回作为结果。
return new Gson().fromJson(json, clazz);
Protobuf 序列化
引入相关依赖:
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.7.2</version>
</dependency>
定义了一个私有的 schemaCache
成员变量,用于缓存不同类的 Schema 对象:
private Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();
serialize
方法的步骤如下:
1、首先,获取传入对象的实际类类型,并将其转换为 Class<T>
类型。
Class<T> clazz = (Class<T>) object.getClass();
2、接下来,调用 getSchema()
方法获取该类对应的 Schema<T>
对象。如果缓存中存在相应的 Schema
,则直接使用缓存的对象;否则,通过 RuntimeSchema.getSchema()
创建新的 Schema
对象,并将其加入到 schemaCache
中。
Schema<T> schema = getSchema(clazz);
private <T> Schema<T> getSchema(Class<T> clazz) {
Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
if (Objects.isNull(schema)) {
schema = RuntimeSchema.getSchema(clazz);
if (Objects.nonNull(schema)) {
schemaCache.put(clazz, schema);
}
}
return schema;
}
3、之后创建一个 LinkedBuffer
对象来提供缓冲区,用于存储序列化的数据。
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
4、调用 ProtostuffIOUtil.toByteArray()
方法将对象序列化为字节数组,使用指定的 Schema
和缓冲区进行处理。
bytes = ProtostuffIOUtil.toByteArray(object, schema, buffer);
5、最后,清空缓冲区并返回序列化得到的字节数组。
buffer.clear();
return bytes;
deserialize
方法的步骤如下:
1、首先,通过调用 getSchema()
方法获取对应的 Schema<T>
对象。
Schema<T> schema = getSchema(clazz);
2、然后通过 schema.newMessage()
创建一个新的目标对象。
T object = schema.newMessage();
3、最后调用 ProtostuffIOUtil.mergeFrom()
将字节数组中的数据反序列化到目标对象中,并返回该对象。
ProtostuffIOUtil.mergeFrom(bytes, object, schema);
需要注意以下几点:
-
并发安全性:
schemaCache
是一个ConcurrentHashMap
对象,因此在多线程环境下对其进行读写操作是安全的,这样可以确保在并发访问时不会出现数据竞争或其他线程安全问题。 -
类型转换:在
serialize
方法中,通过(Class<T>) object.getClass()
进行类型转换,将传入对象的实际类类型转换为泛型参数T
所表示的类型。需要确保传入的对象实际类型与泛型参数一致,否则可能会导致编译错误或运行时异常。 -
缓存机制:通过使用
schemaCache
对象对不同类的Schema
进行缓存,可以避免重复创建Schema
对象的开销,并提高序列化和反序列化的性能。但是需要注意,如果系统中存在大量不同类型的对象,可能会导致schemaCache
的大小增长过大,占用较多内存。在此情况下,可以考虑使用 LRU 缓存策略或限制缓存的最大容量。
MessagePack 序列化
引入相关依赖:
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>0.6.12</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.24.0-GA</version>
</dependency>
serialize
方法:
try {
return new MessagePack().write(object);
} catch (IOException e) {
throw new RuntimeException(e);
}
deserialize
方法:
try {
return new MessagePack().read(bytes, clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
自定义配置
为了实现在不同的序列化算法之间进行自由切换,我们需要抽象序列化算法,代码实现如下:
public interface Serializer {
<T> byte[] serialize(T object);
<T> T deserialize(Class<T> clazz, byte[] bytes);
enum Algorithm implements Serializer {
Java {...},
JSON {...},
Protobuf {...},
MessagePack {...}
}
然后再创建一个配置类,它允许我们仅通过更改配置来选择使用哪种序列化算法,代码实现如下:
public static Serializer.Algorithm getSerializerAlgorithm() {
String value = properties.getProperty("serializer.algorithm");
if (value == null) {
return Serializer.Algorithm.Java;
} else {
return Serializer.Algorithm.valueOf(value);
}
}
我们只需要对配置 application.properties
中的参数 serializer.algorithm
进行修改,即可切换序列化算法:
serializer.algorithm=Protobuf
需要完整代码的读者请访问博主的 Github:Serializer,AppConfig;
可扩展测试
接下来,使用 EmbeddedChannel
进行相关测试,代码实现如下:
MessageCodecSerializer CODEC = new MessageCodecSerializer();
LoggingHandler LOGGING = new LoggingHandler();
EmbeddedChannel channel = new EmbeddedChannel(LOGGING, CODEC, LOGGING);
LoginRequestMessage message = new LoginRequestMessage("sidiot", "123456");
channel.writeOutbound(message);
Java
JSON
Protobuf
MessagePack
BUG 解决
非法反射警告
在我们使用 Protobuf
序列化算法和 MessagePack
序列化算法时,会出现如下警告:
其实,这是 JDK 9 引入了一个新特性,即反射不再能够访问非公开成员和不可公开访问的类。在此之前,即使存在访问控制限制,反射仍然可以绕过这些限制进行访问。从 JDK 9 开始,反射也将遵循访问控制规则。
在 JDK 9 中,如果第一次尝试访问非公开成员,会显示警告信息。这是为了提醒开发人员注意访问权限,并且鼓励使用更好的封装实践。通过显示警告信息,开发人员可以意识到他们正在访问非公开成员,并且可以考虑调整代码以符合访问控制规则。
这个改变的目的是增强 Java 的安全性和封装性。通过限制反射对非公开成员的访问,可以减少潜在的安全风险,并促进更好的软件设计实践。开发人员应该遵循访问控制规则,仅在有必要的情况下暴露适当的接口和成员,并避免直接操作非公开成员。
当然,这并不影响程序的运行,如果不想看到这个警告,可以在 VM 选项中,添加下述指令:
--add-opens java.base/java.lang=ALL-UNNAMED
找不到类模板
在使用 MessagePack
序列化算法时,会出现以下报错:
这是因为 MessagePack
找不到类模板,其中一种解决方法就是添加 Message
注解:
另一种解决方法就是使用 register
方法进行注册:
后记
总而言之,选择合适的序列化算法对于其可扩展性起着重要的影响。通过深入研究和不断优化序列化算法选型、自定义配置和可扩展测试,我们可以提升聊天室的性能和稳定性,为用户提供更好的聊天体验。
以上就是 序列化算法选型对聊天室可扩展性的影响 的所有内容了,希望本篇博文对大家有所帮助!
参考:
📝 上篇精讲:「项目实战」(二)提升聊天室的性能,从引入心跳检测机制开始
💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注,创作不易,请多多支持;
👍 公众号:sidiot的技术驿站;
🔥 系列专栏:探索 Netty:源码解析与应用案例分享
转载自:https://juejin.cn/post/7250374485567569979