热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

序列化和反序列化学习

Java领域的对象如何传输基于socket进行对象传输socket通信的代码的例子user.javapublicclassUser{privateStringname;pu

Java 领域的对象如何传输

基于 socket 进行对象传输

socket 通信的代码的例子

user.java

public class User {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}
}

SocketServer.java 服务端

public class SocketServer {public static void main(String[] args) throws IOException {ServerSocket serverSocket = null;BufferedReader in = null;try {serverSocket = new ServerSocket(8081);Socket socket = serverSocket.accept();ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());User user = (User) objectInputStream.readObject();System.out.println(user);} catch (Exception e) {e.printStackTrace();} finally {if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}if (serverSocket != null) {serverSocket.close();}}}
}

SocketClient.java 客户端

public class SocketClient {public static void main(String[] args) {Socket socket = null;ObjectOutputStream out = null;try {socket = new Socket("127.0.0.1", 8081);User user = new User();out = new ObjectOutputStream(socket.getOutputStream());out.writeObject(user);} catch (IOException e) {e.printStackTrace();} finally {if (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}if (socket != null) {try {socket.close();} catch (IOException e) {e.printStackTrace();}}}}
}

运行结果
这段代码运行以后,能够实现 Java 对象的正常传输吗? 很显然,会报错
在这里插入图片描述
如何解决报错的问题呢?
对 User 这个对象实现一个 Serializable 接口,再次运行就可以看到对象能够正常传输了

public class User implements Serializable {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}
}

序列化的意义
我们发现对 User 这个类增加一个 Serializable,就可以解决 Java 对象的网络传输问题。

Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在 JVM 停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java 对象序列化就能够帮助我们实现该功能

简单来说
序列化是把对象的状态信息转化为可存储传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化
反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程成为对象的反序列化


序列化的高阶认识

简单认识一下 Java 原生序列化

前面的代码中演示了,如何通过 JDK 提供了 Java 对象的序列化方式实现对象序列化传输,主 要通过输出流 java.io.ObjectOutputStream 和对象输入流 java.io.ObjectInputStream 来实现。


  • java.io.ObjectOutputStream:表示对象输出流 , 它的 writeObject(Object obj)方法可以对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。
  • java.io.ObjectInputStream:表示对象输入流 ,它的 readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回

需要注意的是,被序列化的对象需要实现 java.io.Serializable 接口


serialVersionUID 的作用
身份证的作用,唯一标识一个类

在 IDEA 中通过如下设置可以生成 serializeid
在这里插入图片描述
字面意思上是序列化的版本号,凡是实现 Serializable 接口的类都有一个表示序列化版本标识符的静态变量

演示步骤


  1. 先将 user 对象序列化到文件中
  2. 然后修改 user 对象,增加 serialVersionUID 字段
  3. 然后通过反序列化来把对象提取出来
  4. 演示预期结果:提示无法反序列化

结论
Java 的序列化机制是通过判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是 InvalidCastException。

从结果可以看出,文件流中的 class 和 classpath 中的 class,也就是修改过后的 class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。从错误结果来看,如果没有为指定的 class 配置 serialVersionUID,那么 java 编译器会自动给这个 class 进行一个摘要算法, 类似于指纹算法,只要这个文件有任何改动,得到的 UID 就会截然不同的,可以保证在这么多类中,这个编号是唯一的。所以,由于没有显指定 serialVersionUID,编译器又为我们生成了一个 UID,当然和前面保存在文件中的那个不会一样了,于是就出现了 2 个序列化版本号不一致的错误。因此,只要我们自己指定了 serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用。


tips: serialVersionUID 有两种显示的生成方式:

  • 一是默认的 1L,比如:private static final long serialVersionUID = 1L;
  • 二是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段。当实现 java.io.Serializable 接口的类没有显式地定义一个 serialVersionUID 变量时候,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。


Transient 关键字

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

绕开 transient 机制的办法(了解)
略:


Java 序列化的一些简单总结

  1. Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
  2. 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
  3. 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进
    行序列化(实现深度克隆)
  4. 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
  5. 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和
    readObject

分布式架构下常见序列化技术

初步了解了 Java 序列化的知识以后,我们又得回到分布式架构中,了解序列化的发展过程

序列化的发展
随着分布式架构、微服务架构的普及。服务与服务之间的通信成了最基本的需求。这个时候, 我们不仅需要考虑通信的性能,也需要考虑到语言多元化问题 所以,对于序列化来说,如何去提升序列化性能以及解决跨语言问题,就成了一个重点考虑的问题。

由于 Java 本身提供的序列化机制存在两个问题


  1. 序列化的数据比较大,传输效率低
  2. 其他语言无法识别和对接

以至于在后来的很长一段时间,基于 XML 格式编码的对象序列化机制成为了主流,一方面解 决了多语言兼容问题,另一方面比二进制的序列化方式更容易理解。以至于基于 XML 的 SOAP 协议及对应的 WebService 框架在很长一段时间内成为各个主流开发语言的必备的技术。 再到后来,基于 JSON 的简单文本格式编码的 HTTP REST 接口又基本上取代了复杂的 Web Service 接口,成为分布式架构中远程通信的首要选择。但是 JSON 序列化存储占用的空间大、性能低等问题,同时移动客户端应用需要更高效的传输数据来提升用户体验。在这种情况下与语言无关并且高效的二进制编码协议就成为了大家追求的热点技术之一。首先诞生的一个开源的二进制序列化框架-MessagePack。它比 google 的 Protocol Buffers 出现得还要早。


各种序列化技术简介


  • XML 序列化框架介绍
    XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大, 而且效率不高,适用于对性能不高,并且 QPS 较低的企业级内部系统之间的数据交换的场景, 同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 Webservice,就是采用 XML 格式对数据进行序列化的。XML 序列化/反序列化的实现方式有很多,熟知的方式有 XStream 和 Java 自带的 XML 序列化和反序列化两种

  • JSON 序列化框架
    JSON(Javascript Object Notation)是一种轻量级的数据交换格式,相对于 XML 来说,JSON 的字节流更小,而且可读性也非常好。现在 JSON 数据格式在企业运用是最普遍的
    JSON 序列化常用的开源工具有很多

    1. Jackson (https://github.com/FasterXML/jackson)
    2. 阿里开源的 FastJson (https://github.com/alibaba/fastjon)
    3. Google 的 GSON (https://github.com/google/gson)

    这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,但是 Jackson、 GSON 的稳定性要比 Fastjson 好。而 fastjson 的优势在于提供的 api 非常容易使用

  • Hessian 序列化框架
    Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说, Hessian 具有更好的性能和易用性,而且支持多种不同的语言。实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构, 性能更高

  • Avro 序列化
    Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动 态语言可以方便地处理 Avro 数据。

  • kyro 序列化框架
    Kryo 是一种非常成熟的序列化实现,已经在 Hive、Storm中使用得比较广泛,不过它不能跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的 hessian2

  • Protobuf 序列化框架
    Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件, Protobuf 是一个纯粹的表示层协议,可以和各种传输层协议一起使用。
    Protobuf 使用比较广泛,主要是空间开销小(比java序列化方式占用空间小10倍)和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中
    但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要用到的话必须要去投入成本在这个技术的学习中。
    protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的 proto 文件,如果某个类发生修改,还得重新生成该类对应的 proto 文件。


Protobuf 序列化的原理

那么接下来着重分析一下 protobuf 的序列化原理,前面说过它的优势是空间开销小,性能也相对较好。它里面用到的一些算法还是值得我们去学习的

protobuf 的基本应用,使用 protobuf 开发的一般步骤是


  1. 配置开发环境,安装 protocol compiler 代码编译器
  2. 编写.proto 文件,定义序列化对象的数据结构
  3. 基于编写的.proto 文件,使用 protocol compiler 编译器生成对应的序列化/反序列化工具类
  4. 基于自动生成的代码,编写自己的序列化应用

Protobuf 案例演示
下载 protobuf 工具 https://github.com/google/protobuf/releases
编写 proto 文件。找到 protoc-3.5.1-win32.zip

syntax="proto2"; //string / bytes / bool / int32(4 个字节) /int64/float/double enum 枚举类 message 自定义类
package com.gupaoedu.serial;
option java_package = "com.gupaoedu.serial";
option java_outer_classname="UserProtos";
message User {
required string name=1; required int32 age=2;
}/*
修饰符
required 表示必填字段
optional 表示可选字段
repeated 可重复,表示集合 1,2,3,4 需要在当前范围内是唯一的
表示顺序
*/

生成实体类

【.\protoc.exe --java_out=./ ./user.proto】

实现序列化

<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>3.7.0</version>
</dependency>

UserProtos.User user&#61;UserProtos.User.newBuilder().setName("Mic") .setAge(18).build();
ByteString bytes&#61;user.toByteString();
System.out.println(bytes);
UserProtos.User nUser&#61;UserProtos.User.parseFrom(bytes); System.out.println(nUser);

protobuf 序列化原理

我们可以把序列化以后的数据打印出来看看结果

public static void main(String[] args) {UserProtos.User user &#61; UserProtos.User.newBuilder().setAge(300).setName("Mic").build();byte[] bytes &#61; user.toByteArray();for (byte bt : bytes) {System.out.print(bt &#43; " ");}
}

输出结果


10 3 77 105 99 16 -84 2


我们可以看到&#xff0c;序列化出来的数字基本看不懂&#xff0c;但是序列化以后的数据确实很小。
正常来说&#xff0c;要达到最小的序列化结果&#xff0c;一定会用到压缩的技术&#xff0c;而 protobuf 里面用到了两种压缩算法&#xff0c;一种是 varint&#xff0c;另一种是 zigzag
具体做法是基于一定的规则&#xff0c;对字节做一些处理。这里不细研究了。

总结
Protocol Buffer 的性能好&#xff0c;主要体现在序列化后的数据体积小 & 序列化速度快&#xff0c;最终使得传输效率高&#xff0c;其原因如下:
序列化速度快的原因:
a. 编码 / 解码 方式简单(只需要简单的数学运算 &#61; 位移等等)
b. 采用 Protocol Buffer 自身的框架代码和编译器共同完成序列化后的数据量体积小(即数据压缩效果好)的原因:
a. 采用了独特的编码方式&#xff0c;如 Varint、Zigzag 编码方式等等
b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑


序列化技术的选型

技术层面


  1. 序列化空间开销&#xff0c;也就是序列化产生的结果大小&#xff0c;这个影响到传输的性能
  2. 序列化过程中消耗的时长&#xff0c;序列化消耗时间过长影响到业务的响应时间
  3. 序列化协议是否支持跨平台&#xff0c;跨语言。因为现在的架构更加灵活&#xff0c;如果存在异构系统通信
    需求&#xff0c;那么这个是必须要考虑的
  4. 可扩展性/兼容性&#xff0c;在实际业务开发中&#xff0c;系统往往需要随着需求的快速迭代来实现快速更新&#xff0c;这就要求我们采用的序列化协议基于良好的可扩展性/兼容性&#xff0c;比如在现有的序列化数据结构中新增一个业务字段&#xff0c;不会影响到现有的服务
  5. 技术的流行程度&#xff0c;越流行的技术意味着使用的公司多&#xff0c;那么很多坑都已经淌过并且得到了
    解决&#xff0c;技术解决方案也相对成熟
  6. 学习难度和易用性

选型建议


  1. 对性能要求不高的场景&#xff0c;可以采用基于 XML 的 SOAP 协议
  2. 对性能和间接性有比较高要求的场景&#xff0c;那么 Hessian、Protobuf、Thrift、Avro 都可以。
  3. 基于前后端分离&#xff0c;或者独立的对外的 api 服务&#xff0c;选用 JSON 是比较好的&#xff0c;对于调试、可读
    性都很不错
  4. Avro 设计理念偏于动态类型语言&#xff0c;那么这类的场景使用 Avro 是可以的

推荐阅读
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
  • 属性类 `Properties` 是 `Hashtable` 类的子类,用于存储键值对形式的数据。该类在 Java 中广泛应用于配置文件的读取与写入,支持字符串类型的键和值。通过 `Properties` 类,开发者可以方便地进行配置信息的管理,确保应用程序的灵活性和可维护性。此外,`Properties` 类还提供了加载和保存属性文件的方法,使其在实际开发中具有较高的实用价值。 ... [详细]
  • 本文探讨了如何利用Java代码获取当前本地操作系统中正在运行的进程列表及其详细信息。通过引入必要的包和类,开发者可以轻松地实现这一功能,为系统监控和管理提供有力支持。示例代码展示了具体实现方法,适用于需要了解系统进程状态的开发人员。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • 深入解析 Synchronized 锁的升级机制及其在并发编程中的应用
    深入解析 Synchronized 锁的升级机制及其在并发编程中的应用 ... [详细]
author-avatar
绿茶2602921445
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有