Netty学习二:Java IO与序列化
1 Java IO
1.1 Java IO
1.1.1 IO
IO,即输入(Input)输出(Output)的简写,是描述计算机软硬件对二进制数据的传输、读写等操作的统称。
按照软硬件可分为:
- 磁盘IO
- 内存IO
- 网络IO
按照处理的方式可分为:
- 同步IO
- 非阻塞IO
- 异步IO
按照数据类型可分为:
- 字节流
- 字符流
随着软硬件技术的飞速发展,IO性能也有了很大的发展,但IO还是影响现代计算机系统性能最重要的因素之一
- 磁盘技术还严重影响读写性能
- 网络传输还存在很大的延迟
- 数据库的IO已经成为计算机应用系统的主要瓶颈
1.1.2 IO相关指标
1.1.2.1 IOPS
IOPS,IO系统每秒所执行IO操作的次数,是一个重要的用来衡量系统IO能力的一个参数。对于单个磁盘组成的IO系统来说,计算它的IOPS不是一件很难的事情,只要我们知道了系统完成一次IO所需要的时间的话我们就能推算出系统IOPS来。
磁盘IOPS的计算:
IO Time = 寻址时间 + 60s/转速/2 + IO块大小/传输速率
IOPS = 1/IO Time = 1/(寻址时间 + 60s/转速/2 + IO块大小/传输速率)
不同转速的磁盘IO:
- 3600转:1000/(5ms + 60000/3600/2 + 4K/40MB)=75
- 7200转:1000/(5ms + 60000/7200/2 + 4K/40MB)=108
1.1.2.2 IO响应时间
IO响应时间也被称为IO延时(IO Latency),IO响应时间就是从操作系统内核发出的一个读或者写的IO命令到操作系统内核接收到IO回应的时间,注意不要和单个IO时间混淆了,单个IO时间仅仅指的是IO操作在磁盘内部处理的时间,而IO响应时间还要包括IO操作在IO等待队列中所花费的等待时间。
- 随着系统实际IOPS越接近理论的最大值,IO的响应时间会成非线性的增长,越是接近最大值,响应时间就变得越大
1.1.2.3 吞吐量
吞吐量是指单位时间内传输的数据量的总和.
一个系统吞吐量通常由QPS(TPS)、并发数两个因素决定,每套系统这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降。
QPS(TPS)=并发数/平均响应时间
1.1.3 Java的IO
IO是包括Java在内所有编程语言最重要的特性和模块之一,因为不管是读写文件,分配回收内存和网络通信都离不开IO。
同时IO也是计算机系统最主要的性能瓶颈和问题之一,特别是在分布式系统中IO问题更显得突出。
Java最开始只支持BIO,到了JDK1.4开始支持NIO,在JDK7中支持NIO2.0(AIO)
JavaIO按照数据类型分为:
- 字节流:InputStream/OutputStream
- 字符流:Writer/Reader
1.2 BIO
BIO即Blocking IO,同步并且阻塞。
在BIO中,用户线程发起一个IO操作以后,必须等待IO操作的完成,只有当真正的IO完成以后,用户线程才能继续操作。
如上图,用户发起一个请求到服务器,服务器接收后分配一个处理线程来进行处理,同时用户线程阻塞以等待处理线程处理完成后,返回数据到用户线程,用户线程继续往下执行。
BIO模型的问题:
- 处理线程执行速度影响用户线程的性能
- 用户线程与处理线程一对一的模式,随着用户线程的增多,处理线程也将持续增加
1.3 伪异步IO
为了解决BIO模型中线程一对一的问题,通过伪异步IO进行处理。
如上图,服务器接收到用户线程的请求后,通过后端的线程池分配线程进行处理。由于线程池的大小可以设置,因此可以限制服务端的资源使用。
伪异步IO实际上只是对一对一线程模型进行改进,没有解决同步阻塞的问题
1.4 NIO
NIO即Non-Blocking IO,是通过非阻塞的方式实现IO的技术,Java在1.4后提供该技术的支持。
1.4.1 Java NIO
- 用户线程将请求发送到服务端后,如果服务端不能马上准备好数据,则即可返回,用户线程继续执行其他操作;
- 用户线程主动询问服务端是否准备好数据,如果服务端准备好数据,则将数据返回给用户线程,用户线程继续处理。
1.4.2 Buffer、Channel和Selector
1.4.2.1 缓冲区Buffer
在Java NIO中,所有的数据操作都时在缓冲区中完成的。在读取数据时,它直接读缓冲区的数据;写入数据时,也是将数据写入缓存去。
缓冲区实际上是一个数组,包括:
- 字节缓冲区:ByteBuffer
- 字符缓冲区:CharBuffer
- 短整型缓冲区:ShortBuffer
- 整形缓冲区:IntBuffer
- 长整形缓冲区:LongBuffer
- 浮点缓冲区:FloatBuffer
- 双精度浮点型缓冲区:DoubleBuffer
一个 Buffer 主要由 position、limit、capacity 三个变量来控制读写的过程。这三个变量在读和写时分别代表的含义如下:
- position:当前写入/读取的单位数据的数量
- limit:代表最多能写入/读取多少单位的数据量
- capacity:Buffer的容量
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据.
1.4.2.2 通道Channel
Channel是一个全双工的双向通道,可以通过它读取和写入数据。Channel总是从Buffer读取数据或者向Buffer写入数据.
Java Channel包括:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
1.4.2.3 多路复用器Selector
Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从 SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤:
- 向Selector对象注册感兴趣的事件
- 从Selector中获取感兴趣的事件
- 根据不同的事件进行相应的处理
1.4.3 服务端处理
1. 打开ServerSocketChannel,用于监听客户端连接
2. 绑定监听端口,设置连接为非阻塞模式
3. 创建Reactor线程,创建多路复用器并启动线程
4. 将SeverSocketChannel注册Reactor线程的多路复用器Selector上,监听ACCEPT事件
5. 多路复用器在线程run方法的无限循环体内轮询准备就绪的Key
6. 多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
7. 设置客户端链路为非阻塞模式
8. 将新接入的客户端连接到Reactor线程的多路复用器上,监听读操作,用来读取客户端发送的网络消息
9. 异步读取客户端请求消息到缓冲区
10. 对Buffer进行编辑码,将解码成功的消息封装成Task,投递到业务线程池,进行业务逻辑编排
11. 将POJO对象编码为Buffer,调用channel的异步write接口,将消息异步发送给客户端
1.4.4 客户端处理
1. 打开SocketChannel,绑定本地地址和端口
2. 设置SocketChannel为非阻塞模式,设置TCP参数
3. 异步连接服务端
4. 判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果没有连接成功返回false
5. 向Reactor线程的多路复用器注册OP_CONNECT状态位
6. 创建Reactor线程,创建多路复用器并且启动线程
7. 多路复用器在线程run方法的无限循环体内轮询准备就绪的Key
8. 接收connect事件进行处理
9. 判断连接结果,如果连接成功,注册读事件到多路复用器
10. 注册读事件到多路复用器
11. 异步读客户端请求消息到缓冲区
12. 对Buffer进行编解码,将解码成功的消息封装成Task,投递到业务线程池,进行业务逻辑编排
13. 将POJO对象编码为Buffer,调用channel的异步write接口,将消息异步发送给客户端
1.5 AIO
在JAVA NIO中用户线程会主动的询问数据是否准备完成,不是真正的异步
Java AIO即Java NIO2.0,是在JDK1.7中引入的新概念,并提供了异步文件通道和异步套接字通道的实现。
NIO2.0的异步套接字通道是真正的异步非阻塞IO,它对应UNIX网络编程中的事件驱动IO,它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。
1.6 比较
- BIO
同步阻塞IO,性能低,编程较容器,适用于连接数目比较小且固定的架构
- NIO
同步非阻塞IO,性能高,编程较复杂,适用于连接数目多且连接比较短(轻操作)的架构
- AIO
异步非阻塞IO,性能高,编程较复杂,使用于连接数目多且连接比较长(重操作)的架构
2 序列化
2.1 序列化与反序列化
序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
序列化使其他代码可以查看或修改那些不序列化便无法访问的对象实例数据。
什么情况下需要序列化:
- 当你想把的内存中的对象保存到一个文件中或者数据库中时候
- 当你想用套接字在网络上传送对象的时候
- 当你想通过RMI传输对象的时候
2.2 Java序列化
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
2.2.1 Serializable
实现Serializable接口的Java类表示可以被Java默认序列化或者其他第三方序列化工具序列化,但有些第三方序列化工具序列化类时不需要该标识。
2.2.2 serialVersionUID
如果没有设置这个值,你在序列化一个对象之后,改动了该类的字段或者方法名之类的,那如果你再反序列化想取出之前的那个对象时就可能会抛出异常,因为你改动了类中间的信息,serialVersionUID是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,当修改后的类去反序列化的时候发现该类的serialVersionUID值和之前保存在问价中的serialVersionUID值不一致,所以就会抛出异常。而显示的设置serialVersionUID值就可以保证版本的兼容性,如果你在类中写上了这个值,就算类变动了,它反序列化的时候也能和文件中的原值匹配上。而新增的值则会设置成null,删除的值则不会显示。
2.5.3 注意事项
-
序列化只能保存对象的非静态成员交量,不能保存任何的成员方法和静态的成员变量,而且序列化保存的只是变量的值,对于变量的任何修饰符都不能保存。
-
对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的。例如一个Thread对象或一个FileInputStream对象 ,对于这些字段,我们必须用transient关键字标明,否则编译器将报措。
-
当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化。
2.3 序列化实现
-
Netty 序列化
Netty通过ObjectEncoder和ObjectDecoder对对象进行序列化编解码以便在网络中传输数据
-
Dubbo 序列化
Dubbo序列化是阿里在Dubbo框架中用于对象序列化的技术
-
Hessian 序列化
Hessian是一种跨语言的高效二进制序列化方式