NIO
NIO 从 JDK1.4 才开始有:JDK1.7 推出 NIO 2.0
在JDK1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈
因此,在很长一段时间里,大型的高性能服务端应用程序都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。
当并发访问量增大、响应时间延迟增大之后,采用Java BIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和延时,极大地增加了企业的成本
并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口)客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
BIO通信模型:
在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。
NIO,即New I/O,这是官方叫法,因为它相对于之前的I/O类库是新增的。但是,由于之前老的I/O类库是阻塞I/O,New I/O类库的目标就是要让Java支持非阻塞I/O,所以,更多的人喜欢称之为非阻塞I/O(Non-block I/O),由于非阻塞I/O更能够体现NIO的特点,所以这里使用的NIO都是指非阻塞I/O
一、NIO的概念;
基于通道,面向缓冲区的非阻塞IO
二、IO 和 NIO 的比较:
1、IO是面向流的,NIO是(基于通道)面向缓冲区的:IO将数据直接写出到流中或从流中直接读取数据,NIO中所有的数据必须通过缓冲区来处理,缓冲区可以是双向的。
2、IO以流(逐字节)的方式处理数据,NIO以块的方式处理数据。
3、IO是阻塞的,NIO是非阻塞的:
三、传统的IO流:
1、一个线程调用read()或write()方法时,该线程被阻塞,直到有一些数据被读取或写入。
2、网络通信进行IO操作时,由于线程会被阻塞,因此,服务器必须为每个客户端提供一个独立的线程来处理IO操作。
3、当服务器需要处理大量客户端时,性能急剧下降。
四、NIO:
1、当线程从某通道进行读写数据时,若没有数据可用,此时,该线程可以处理其它的任务。
2、线程通常将非阻塞IO的空闲时间用在其它通道上的IO操作
3、因此,一个线程可以管理多个输入和输出通道。
五、核心API:
1、缓冲区:java.nio.Buffer
1、缓冲区从两个方面来提高I/O的效率:
1、减少实际的物理读写次数
2、缓冲区所占的内存空间一直在被复用,减少了动态分配内存及GC的次数。
2、缓冲区是一个数组,java.nio.ByteBuffer是最常见的缓冲区。
1、BtyeBuffer提供了两个创建字节缓冲区的方法:
2、allocate(int capacity):将缓冲区建立在JVM的内存中。
3、allocateDirect(int capacity):将缓冲区建立在物理内存中,创建的缓冲区称为直接缓冲区,可以进一步提高I/O的速度。
直接缓冲区:
1、分配直接缓冲区的开销比较大,因此只有在大型、持久的缓冲区(易受操作系统的本机I/O影响)中才会使用直接缓冲区。
2、直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。
3、直接字节缓冲区还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建
非直接缓冲区:
物理磁盘 --- read() ---> 内核地址空间 --- copy ---> 用户地址空间(JVM) --- read() ---> 应用程序
直接缓冲区:
物理磁盘 --- read() ---> 操作系统的物理内存(映射文件) --- read() ---> 应用程序
Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。
Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer,是NIO中数据读或者写的中转地
在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:
ByteBuffer
IntBuffer
CharBuffer
LongBuffer
DoubleBuffer
FloatBuffer
ShortBuffer
MappedByteBuffer
其中最常用的就是:ByteBuffer
Buffer中有四个非常重要的属性:
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity:作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position:当你写数据到Buffer中时,position表示当前的位置。初始的position值为0。当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
一句话概括:capacity就是buffer的容量,position和limit之间就是可读和可写的区域范围, mark就是一个标记位置,位于position和limit之间,主要用于重复读取
Buffer中有八个非常有趣的方法:
flip():写模式转换成读模式,在读模式下,可以读取之前写入到buffer的所有数据。 limit = position; position = 0; mark = -1;
clear():清空buffer ,准备再次被写入 (position变成0,limit变成capacity)
rewind():将position重置为0,一般用于重复读, position = 0; mark = -1;
compact(): 将未读取的数据拷贝到 buffer 的头部位,新写入的数据将放到缓冲区未读数据的后面。
mark():标记一个位置
reset():重置到该位置
ramainning():求出剩下的没有读取的数据
capacity():求出buffer的容量
两个buffer判断是否相等:
有相同的类型(byte、char、int等)。
Buffer中剩余的byte、char等的个数相等。
Buffer中所有剩余的byte、char等都相同。
2、通道:Channel
Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读、写或者同时读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。
Channel 用于向 buffer 提供数据或者读取 buffer 数据,buffer 对象的唯一接口。
常用的Channel:
FileChannel :实现从文件读,或者向文件写
SocketChanel :通过TCP协议向网络的两端读写数据
ServerSocketChannel :监听客户端发起的TCP链接,如果有链接请求,则为每个TCP连接创建一个新的SocketChannel来进行数据读写
DatagramChannel :通过UDP协议向网络的两端读写数据
1、概念:通道表示打开到IO设备(文件、套接字)的连接。
2、说明:
1、NIO中通道的作用和IO中流的作用相似,都是用来传输数据的。
2、NIO中所有的数据都是通过缓冲区来处理的,通道就是用来连接缓冲区和数据源的:
3、流与通道的比较:
1、IO中的流是单向的,而NIO中的通道是双向的。
4、Channel的实现类:
本地IO:
FileChannel:用于读取、写入、映射和操作文件的通道。适用于对本地文件的操作。
网络IO:
TCP:
SocketChannel:通过TCP读写网络中的数据。
ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。
UDP:
DatagramChannel:通过UDP读写网络中的数据通道。
5、获取通道的方法:
方法一:对支持通道的对象调用getChannel()方法,支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
Socket
ServerSocket
DatagramSocket
方法二:
Files.newByteChannel()
使用Files类的静态方法newByteChannel() 获取字节通道。
或者通过通道的静态方法 open() 打开并返回指定通道
3、多路复用器Selector
多路复用器Selector是Java NIO编程的基础,熟练地掌握Selector对于掌握NIO编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接、接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
Selector.select()
Selector.epoll()
使用NIO完成网络通信的三个要点:
1、通道(Channel):负责连接
java.nio.channels.Channel 接口:
|--SelectableChannel
|--SocketChannel
|--ServerSocketChannel
|--DatagramChannel
|--Pipe.SinkChannel
|--Pipe.SourceChannel
2、缓冲区(Buffer):负责数据的存取
3、选择器(Selector):是SelectableChannel的多路复用器,用于监控SelectableChannel的IO状况。
说明:
1、非阻塞模式是针对网络IO而言的。
2、FileChannel不能切换成非阻塞模式。