前文开了高并发学习的头,文末说了将会选择NIO、RPC相关资料作进一步学习,因此本文开始学习NIO知识。html
IO知识回顾java
在学习NIO前,有必要先回顾一下IO的一些知识。 算法
IO中的流编程
Java程序经过流(Stream)来完成输入输出。流是生产或者消费信息的抽象,流经过Java的输入输出与物理设备链接,尽管与之相连的物理设备不尽相同,可是全部的流的行为都是同样的,因此相同的输入输出类的功能和方法适用于全部的外部设备。这意味着一个输入流能够抽象多种类型的输入,好比文件、键盘或者网络套接字等,一样的,一个输出流也能够输出到控制台、文件或者相连的网络。segmentfault
流的分类数组
从功能上能够将流分为输入流和输出流。输入和输出是相对于程序来讲的,程序在使用数据时所扮演的角色有两个:一个是源,一个是目的。若程序是数据的源,对外输出数据,咱们就称这个数据流相对于程序来讲是输出流,若程序是数据的目的地,咱们就称这个数据流相对于程序来讲是输入流。缓存
从结构上能够将流分为字节流和字符流,字节流以字节为处理单位,字符流以字符为处理单位。安全
从角色上能够将流分为节点流和过滤流。从特定的地方读写的流叫作节点流,如磁盘或者一块内存区域,而过滤流则以节点流做为输入或者输出,过滤流是使用一个已经存在的输入流或者输出流链接来建立的。 服务器
字节流的输入流和输出流的基础是InputStream和OutputStream,字节流的输入输出操做由这两个类的子类实现。字符流是Java1.1后新增的以字符为单位进行输入和输出的流,字符流的输入输出的基础是Reader和Writer。网络
字节流(byte stream)为处理字节的输入和输出提供了方便的方法,好比使用字节流读取或者写入二进制的数据。字符流(character stream)为字符的输入和输出提供了方便,采用了统一的编码标准,于是能够国际化。值得注意的,在计算机的底层实现中,全部的输入输出其实都是以字节的形式进行的,字符流只是为字符的处理提供了具体的方法。
输入流
读数据的逻辑为:
open a stream
while more information
read information
close the stream
忽略掉异常处理,相关的代码实现大体以下:
InputStream input = new FileInputStream("c:\\data\\input-text.txt"); int data = input.read(); while(data != -1) { //do something with data... doSomethingWithData(data); data = input.read(); } input.close();
输出流
写数据的逻辑为:
open a stream
while more information
write information
close the stream
忽略掉异常处理,相关的代码实现大体以下:
OutputStream output = new FileOutputStream("c:\\data\\output-text.txt"); while(hasMoreData()) { int data = getMoreData(); output.write(data); } output.close();
输入流的类层次
输出流的类层次
过滤流
在InputStream、OutputStream类的子类中,FilterInputStream和FilterOutputStream过滤流又派生出DataInputStream和DataOutputStream数据输入输出流等子类。
IO流的连接
Input Stream Chain:从外部文件往程序中写入数据,因此第一步须要构造输入流,同时也是节点流,FileInputStream,为了使这个流具有缓冲的特性,须要从节点流转成过滤流,BufferedInputStream,仅仅有缓冲特性可能还不能知足平常需求,还须要有读取基本数据类型的特性,能够基于现有的过滤流再转成其余的过滤流,DataInputStream,此时即可以方便的从文件中读取数据;
Output Stream Chain:往外部文件中写出数据,首先对于外部文件来讲,是FileOutputStream,为了使这个流具有缓冲的特性,须要从节点流转成过滤流,BufferedOutputStream,仅仅有缓冲特性可能还不能知足平常需求,还须要有写出基本数据类型的特性,能够基于现有的过滤流再转成其余的过滤流,DataOutputStream,此时即可以方便的从写各类数据类型;
Reader的类层次
Writer的类层次
至此,大概回顾了一下IO的部分基础知识。
IO与装饰模式
回到IO流的连接中,Input Stream Chain的通常代码是这样写的:
InputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream("c:\\data\\input-text.txt")))
Output Stream Chain的通常代码是这样写的:
OutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("c:\\data\\output-text.txt")))
实际上,这种一个流与另外一个流首尾相接,造成一个流管道的实现机制,其实就是装饰模式的一种应用。
装饰模式的套路
抽象构件角色(Component):给出一个抽象接口,以规范准备接收附加责任的对象
具体构件角色(ConcreteComponent):定义一个将要接收附加责任的类
装饰角色(Decorator):持有一个构件(Component)对象引用,并定义一个与抽象构建接口一致的接口
具体装饰角色(ConcreteDecorator):负责给构件对象贴上附加的责任
装饰模式的代码实现
让咱们来看下代码:
public interface Component { void doSomething(); } public class ConcreteComponent implements Component{ @Override public void doSomething() { System.out.println("功能A"); } } public class Decorator implements Component{//重点1 定义抽象构件接口一致的接口 private Component component;//重点2 持有构件对象的引用 public Decorator(Component component) { this.component = component; } @Override public void doSomething() { component.doSomething(); } }
最后是具体装饰角色代码:
public class ConcreteDecorator1 extends Decorator{ public ConcreteDecorator1(Component component) { super(component); } @Override public void doSomething() { super.doSomething(); this.doAnotherThing(); } public void doAnotherThing() { System.out.println("功能B"); } } public class ConcreteDecorator2 extends Decorator{ public ConcreteDecorator2(Component component) { super(component); } @Override public void doSomething() { super.doSomething(); this.doAnotherThing(); } public void doAnotherThing() { System.out.println("功能C"); } }
对于客户端来讲只须要以下简单的代码,便可完成对构件对象ConcreteComponent的装饰:
Component component = new ConcreteDecorator1(new ConcreteDecorator2(new ConcreteComponent())); component.doSomething();
IO中对应的装饰模式解释
DataInputStream、BufferedInputStream的角色就像上述的ConcreteDecorator1和ConcreteDecorator2,FilterInputStream相似Decorator,InputStream就是Component。
在JDK的源码中:
public class FilterInputStream extends InputStream { protected volatile InputStream in; protected FilterInputStream(InputStream in) { this.in = in;} public int read() throws IOException { return in.read();}
再看下Decorator:
public class Decorator implements Component{//重点1 定义抽象构件接口一致的接口 private Component component;//重点2 持有构件对象的引用 public Decorator(Component component) { this.component = component; } @Override public void doSomething() { component.doSomething(); } }
至此,咱们能够知道IO是如何体现的装饰模式。
为何须要NIO
IO主要面向流数据,常常为了处理个别字节或字符,就要执行好几个对象层的方法调用。这种面向对象的处理方法,将不一样的 IO 对象组合到一块儿,提供了高度的灵活性(IO里面的装饰模式),但须要处理大量数据时,却可能对执行效率形成致命伤害。IO的终极目标是效率,而高效的IO每每又没法与对象造成一一对应的关系。高效的 IO 每每意味着您要选择从A到B的最短路径,而执行大量IO 操做时,复杂性毁了执行效率。传统Java平台上的IO抽象工做良好,适应用途普遍。可是当移动大量数据时,这些IO类可伸缩性不强,也没有提供当今大多数操做系统广泛具有的经常使用IO功能,如文件锁定、非块IO、就绪性选择和内存映射。这些特性对实现可伸缩性是相当重要的,对保持与非 Java 应用程序的正常交互也能够说是必不可少的,尤为是在企业应用层面,而传统的Java IO 机制却没有模拟这些通用IO服务。Java规范请求#51(JSR 51, https://jcp.org/en/jsr/detail?id=51)包含了对高速、可伸缩 I/O 特性的详尽描述,借助这一特性,底层操做系统的IO性能能够获得更好发挥。 JSR 51 的实现,其结果就是新增类组合到一块儿,构成了 java.nio 及其子包,以及 java.util.regex 软件包,同时现存软件包也相应做了几处修改。JCP 网站详细介绍了 JSR 的运做流程,以及 NIO 从最初的提议到最终实现并发布的演进历程。随着 Merlin(Jdk1.4) 的发布,操做系统强大的IO特性终于能够借助 Java 提供的工具获得充分发挥。论及IO性能,Java不再逊于任何一款编程语言。
到这里,咱们知道Java NIO的目的是为了提升效率,充分利用操做系统提供的IO特性,因此为了应对更多的处理请求,咱们须要新的IO模型(NIO)。
NIO的核心组件
这一节,咱们将开始学习NIO。
上文中曾提到,NIO的三个核心组件:Selector、Channel和Buffer。用一张图来抽象这三者之间的关系。
在没有Java NIO以前,传统的IO对于网络链接的处理,一般会采用Thread Per Task,即一线程一链接的模式,这种模式在中小量业务处理时基本上是能知足要求的,可是随着链接数不断增长,所建立的线程会不断占用内存空间,同时大量的线程也会带来频繁的上下文切换,CPU都用来操做上下文切换了,必然影响实际的业务处理。有了Java NIO以后,就能够花少许的线程来处理大量的链接,在上图中,Selector是对Linux下的select/poll/epoll的包装,Channel是可IO操做的硬件、文件、socket、其余程序组件的包装,咱们能够把Channel当作是网络链接,网络链接上主要有connect、accept、read和write等事件,Selector负责监听这些事件的发生,一旦某个Channel上发生了某个事件,Thread切换到该Channel进行事件处理。类比到操做系统层面,若是是select/poll方式,应用进程对每一个socket(Channel)的文件描述符顺序扫描,查看是否就绪,阻塞在select(Selector)上,若是就绪,调用recvfrom,若是是epoll方式,就不是顺序扫描,而是提供回调函数,当文件描述符就绪时,直接调用回调函数,进一步提升效率。图中还有一个组件就是Buffer,它实际上就是一块内存,底层是基于数组来实现的,通常和Channel成对出现,数据的读写都是经过Buffer来实现的。
接下来,依次来看下Selector、Channel和Buffer。
Selector模块
Selector
Selector是一个多路传输的SelectableChannel对象。
Selector能够经过调用自身的open方法来进行建立,在open方法里面是经过系统默认的selector provider来建立Selector的。固然也能够经过调用openSelector来自定义一个Selector。一个Selector将会一直保持open的状态直到调用close方法。
一个可选择的Channel对象注册到Selector的行为是经过SelectionKey对象来体现的。Selector维护了三种SelectionKey的集合:
key set包含了当前的注册到Selector的Channel对应的全部的keys,这些keys能够经过keys()返回;
selected-key set的每一个成员都是相关的通道被选择器(在前一个选择操做
中)判断为已经准备好的,而且包含于键的 interest 集合中的操做。这个集合经过 selectedKeys()方法返回。selected-key set是key set的子集;
cancelled-key set是一个被取消的Channel可是还未被注销的key的集合,这个集合没法直接访问,cancelled-key set也是key set的子集。
对于一个刚建立的Selector,上述三个集合默认都是空的。
经过调用Channel的register方法,一个新的key将会被添加到Selector的key set中。在执行selection操做时,cancelled keys将会从key set中移除,key set自己是不能被直接修改的。
无论是直接关闭Channel,仍是调用SelectionKey的close方法,都会有一个key被添加到cancelled-key set中。在下一个selection操做中,取消一个key的动做都会致使其对应的Channel被注销掉,同时这个key也会从Selector的key set中移除。
在执行selection操做时,keys会被添加到selected-key set中,经过Set的remove方法或者是Iterator的remove方法,key能够直接从selected-key set中移除,除此之外,没有其余的方法能够达到这样的效果。特别须要强调的是, 移除操做不是selection的反作用。key也不能直接添加到selected-key set中。
Selection
在每一次Selection操做中,keys有可能从selected-key set、key set或者cancelled-key set中增长或者删除。Selection操做是经过select()、select(long)、selectNow()方法来执行的,通常包含以下三步:
一、cancelled-key set中的每个key均可以从它所属的key set中移除,同时它所属的Channel也会注销,这一步将会使cancelled-key set为空;
二、底层操做系统开始被查询以更新剩余的Channel通道的就绪状态来执行这个key感兴趣的事件,对于一个至少有一种这样操做的Channel来讲,下面2个动做将会被执行:
2.1 若是Channel的key不在selected-key set中,而后key会被增长到selected-key set中,同时它的就绪操做会被修改出来准确的标记出哪个Channel已完成准备工做,任意以前的ready set的就绪信息将会被丢弃;
2.2 若是Channel的key在selected-key set中,同时它的就绪操做会被修改出来准确的标记出哪个Channel已完成准备工做,任意以前的ready set的就绪信息将会被保留,换句话来讲,这个底层操做系统的ready set将会按位写入到当前的key的ready set。
若是全部的key set里面的key一开始就没有interest set,那么无论是selected-key set仍是它对应的就绪操做都不会被更新。
三、若是执行步骤2时有新的key加入到cancelled-key,则按步骤1进行处理。
这三个方法的本质区别就是选择操做是否阻塞等待一个或多个通道准备就绪,或者等待了多长时间。
Concurrency
选择器自己能够安全地供多个并发线程使用。可是,它们的key set不是。
在执行选择操做时,选择器在 Selector 对象上进行同步,而后是key set,最后是selected-key set,按照这样的顺序。cancelled-key set也在选择过程的的第 1步和第 3 步之间保持同步。
在进行选择操做时,对选择器的interest sets所作的更改对该操做没有影响,他们将在下一个选择操做中看到。
选择器的一个或多个键集中的键的存在并不表示该键有效或其通道已打开。若是其余线程有可能取消键或关闭通道,则应用程序代码应谨慎同步并在必要时检查这些条件。
线程会阻塞在select()或者select(long)方法上,若是其余线程想中断这个阻塞,能够经过以下三种方式:
经过调用选择器的wakeup方法;
经过调用选择器的close方法;
经过调用被阻塞线程的interrupt方法,在这种状况下,将设置其中断状态,并调用选择器的wakeup方法。
close方法以与选择操做相同的顺序在选择器和全部三个键集上同步。
一般,选择器的key和selected-key不能安全地供多个并发线程使用。若是此类线程能够直接修改这些集合之一,则应经过在集合自己上进行同步来控制访问。 这些集合的迭代器方法返回的迭代器是快速失败的:若是在建立迭代器以后修改了集合,则除了经过调用迭代器本身的remove方法以外,其余任何方式都会抛出java.util.ConcurrentModificationException。
以下为Selector提供的全部方法:
SelectionKey
表示SelectableChannel向Selector的注册的令牌。
每次将Channel注册到选择器中时,都会建立一个选择键。这个键一直有效,直到经过调用其cancel方法,关闭其通道或关闭其选择器将其取消。取消键不会当即将其从选择器中删除,而是将其添加到选择器的“取消键”集合中,以便在下一次选择操做期间将其删除。能够经过调用isValid方法来测试这个键是否有效性。
选择键包含两个表示为整数值的操做集。操做集的每一个位表示键的通道支持的可选操做的类别。
兴趣集肯定下一次调用选择器的选择方法之一时,将测试那些操做类别是否准备就绪。使用建立键时给定的值来初始化兴趣集,之后能够经过interestOps(int)方法对其进行更改。
准备集标识键的选择器已检测到键的通道已准备就绪的操做类别。 建立key时,准备集将初始化为零。它可能稍后会在选择操做期间由选择器更新,但没法直接更新。
选择键的就绪集指示其通道已为某个操做类别作好了提示,但不是保证,此类类别中的操做能够由线程执行而不会致使线程阻塞。准备工做极可能在选择操做完成后当即准确。外部事件和在相应通道上调用的I/O操做可能会使它不许确。
此类定义了全部已知的操做集位,可是精确地给定通道支持哪些位取决于通道的类型。SelectableChannel的每一个子类都定义一个validOps()方法,该方法返回一个集合,该集合仅标识通道支持的那些操做。尝试设置或测试键通道不支持的操做集位将致使run-time exception。
一般有必要将一些特定于应用程序的数据与选择键相关联,例如,一个对象表明一个更高级别协议的状态并处理就绪通知,以实现该协议。所以,选择键支持将单个任意对象附加到键上。能够经过attach方法附加对象,而后再经过attach方法检索对象。
选择键可安全用于多个并发线程。一般,读取和写入兴趣集的操做将与选择器的某些操做同步。确切地说,如何执行此同步取决于实现方式:在低性能的实现方式中,若是选择操做已在进行中,则兴趣组的读写可能会无限期地阻塞;在高性能实现中,读取或写入兴趣集可能会短暂阻塞(若是有的话)。不管如何,选择操做将始终使用该操做开始时当前的兴趣设置值。
以下为SelectionKey提供的全部方法:
Channel模块
Channel用来表示诸如硬件设备、文件、网络套接字或程序组件之类的实体的开放链接,该实体可以执行一个或多个不一样的I/O操做(例如读取或写入)。I/O 能够分为广义的两大类别:File I/O和Stream I/O。那么相应地有两种类型的通道,它们是文件(file)通道和套接字(socket)通道。文件通道有一个FileChannel类,而套接字则有三个socket通道类:SocketChannel、 ServerSocketChannel和DatagramChannel。通道能够以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操做要么当即完成,要么返回一个结果代表未进行任何操做。只有面向流的(stream-oriented)的通道,如 SocketChannel、ServerSocketChannel才能使用非阻塞模式。SocketChannel、ServerSocketChannel从 SelectableChannel引伸而来。从 SelectableChannel 引伸而来的类能够和支持有条件的选择(readiness selectio)的选择器(Selector)一块儿使用。将非阻塞I/O 和选择器组合起来就可使用多路复用 I/O(multiplexed I/O),也就是前面提到的select/poll/epoll。因为FileChannel不是从SelectableChannel类引伸而来,因此FileChannel,也就是文件IO,是没法使用非阻塞模型的。
FileChannel
用于读取、写入、映射和操做文件的通道。
文件通道是能够链接到文件的SeekableByteChannel。它在文件中具备当前位置,支持查询和修改。文件自己包含一个可变长度的字节序列,能够读取和写入这些字节,而且能够查询其当前大小。当写入字节超过当前大小时,文件大小会增长; 文件的大小在被截断时会减少。该文件可能还具备一些关联的元数据,例如访问权限、内容类型和最后修改时间等,此类未定义用于元数据访问的方法。
除了熟悉的字节读取、写入和关闭操做以外,此类还定义了如下特定于文件的操做:
能够以不影响通道当前位置的方式在文件中的绝对位置读取或写入字节;
文件的区域能够直接映射到内存中。对于大文件,这一般比调用传统的读取或写入方法要有效得多;
对文件所作的更新可能会被强制发送到基础存储设备,以确保在系统崩溃时不会丢失数据;
字节能够从文件传输到其余通道,反之亦然,能够经过操做系统进行优化,将字节快速传输到文件系统缓存或从文件系统缓存快速传输;
文件的区域可能被锁定,以防止其余程序访问;
文件通道能够安全地供多个并发线程使用。如Channel接口所指定的,close方法能够随时调用。在任何给定时间,可能仅在进行涉及通道位置或能够更改其文件大小的一项操做。当第一个此类操做仍在进行时,尝试启动第二个此类操做的尝试将被阻止,直到第一个操做完成。其余操做,尤为是采起明确立场的操做,能够同时进行。它们是否实际上执行取决于底层实现。
此类的实例提供的文件视图保证与同一程序中其余实例提供的相同文件的其余视图一致。可是,因为底层操做系统执行的缓存和网络文件系统协议引发的延迟,此类实例提供的视图可能与其余并发运行的程序所见的视图一致,也可能不一致。 无论这些其余程序的编写语言是什么,以及它们是在同一台计算机上仍是在其余计算机上运行,都是如此。任何此类不一致的确切性质都取决于底层操做系统如何实现。
经过调用此类定义的open方法来建立文件通道。也能够经过调用后续类的getChannel方法从现有的FileInputStream,FileOutputStream或RandomAccessFile对象得到文件通道,该方法返回链接到相同基础文件的文件通道。若是文件通道是从现有流或随机访问文件得到的,则文件通道的状态与其getChannel方法返回该通道的对象的状态紧密相连。不管是显式更改通道位置,仍是经过读取或写入字节来更改通道位置,都会更改原始对象的文件位置,反之亦然。经过文件通道更改文件的长度将更改经过原始对象看到的长度,反之亦然。经过写入字节来更改文件的内容将更改原始对象看到的内容,反之亦然。
在各个点上,此类都指定须要一个“可读取”、“可写入”或“可读取和写入”的实例。经过FileInputStream实例的getChannel方法得到的通道将打开以供读取。经过FileOutputStream实例的getChannel方法得到的通道将打开以进行写入。最后,若是实例是使用模式“ r”建立的,则经过RandomAccessFile实例的getChannel方法得到的通道将打开以供读取;若是实例是使用模式“ rw”建立的,则将打开以进行读写。打开的用于写入的文件通道可能处于附加模式,例如,若是它是从经过调用FileOutputStream(File,boolean)构造函数并为第二个参数传递true建立的文件输出流中得到的。在这种模式下,每次调用相对写入操做都会先将位置前进到文件末尾,而后再写入请求的数据。位置的提高和数据的写入是否在单个原子操做中完成取决于操做系统的具体实现。
SocketChannel
一个可选择的Channel,用于面向流的链接socket。
经过调用此类的open方法来建立套接字通道。没法为任意现有的套接字建立通道。新建的套接字通道一打开,是处于还没有链接的状态的。尝试在未链接的通道上调用I/O操做将致使引起NotYetConnectedException。套接字通道能够经过调用其connect方法进行链接,链接后,套接字通道将保持链接状态,直到关闭为止。套接字通道是否已链接能够经过调用其isConnected方法来肯定。
套接字通道支持非阻塞链接,建立一个套接字通道,并能够经过connect方法启动创建到远程套接字的连接的过程,而后由finishConnect方法完成。能够经过调用isConnectionPending方法来肯定链接操做是否正在进行。
套接字通道支持异步关闭,这相似于Channel类中指定的异步关闭操做。若是套接字的输入端被一个线程关闭,而另外一个线程在套接字通道的读取操做中被阻塞,则阻塞线程中的读取操做将完成而不会读取任何字节,而且将返回-1。若是套接字的输出端被一个线程关闭,而另外一个线程在套接字通道的写操做中被阻塞,则被阻塞的线程将收到AsynchronousCloseException。
套接字选项是使用setOption方法配置的。套接字通道支持如下选项:
选项名称 描述
SO_SNDBUF 套接字发送缓冲区的大小
SO_RCVBUF 套接字接收缓冲区的大小
SO_KEEPALIVE 保持链接活跃
SO_REUSEADDR 重复使用地址
SO_LINGER 若是有数据,则在关闭时徘徊(仅在阻塞模式下配置)
TCP_NODELAY 禁用Nagle算法
也能够支持其余(特定于实现的)选项。
套接字通道能够安全地供多个并发线程使用。它们支持并发读取和写入,尽管在任何给定时间最多能够读取一个线程,而且最多能够写入一个线程。connect和finishConnect方法彼此相互同步,而且在这些方法之一的调用正在进行时尝试启动读取或写入操做将被阻止,直到该调用完成为止。
ServerSocketChannel
一个可选择的Channel,用于面向流的监听socket。
经过调用此类的open方法能够建立服务器套接字通道。没法为任意现有的ServerSocket建立通道。新建立的服务器套接字通道一打开,是处于还没有绑定的状态的。尝试调用未绑定的服务器套接字通道的accept方法将致使引起NotYetBoundException。能够经过调用此类定义的bind方法之一来绑定服务器套接字通道。
套接字选项是使用setOption方法配置的。服务器套接字通道支持如下选项:
选项名称 描述
SO_RCVBUF 套接字接收缓冲区的大小
SO_REUSEADDR 重复使用地址
也能够支持其余(特定于实现的)选项。
服务器套接字通道可安全用于多个并发线程。
Buffer模块
Buffer
一个特定原始类型数据的容器。
缓冲区是特定原始类型元素的线性有限序列。除了其内容以外,缓冲区的基本属性还包括capacity、limit和position:
缓冲区的capacity是它包含的元素数量。缓冲区的capacity永远不会为负,也不会改变。
缓冲区的limit是不该读取或写入的第一个元素的索引。缓冲区的limit永远不会为负,也永远不会大于缓冲区的capacity。
缓冲区的position是下一个要读取或写入的元素的索引。缓冲区的position永远不会为负,也不会大于limit。
对于每一个非布尔基本类型,此类都有一个子类,即IntBuffer、ShortBuffer、LongBuffer、CharBuffer、ByteBuffer、DoubleBuffer和FloatBuffer。
Transferring data
此类的每一个子类定义了get和put操做的两类:
相对操做从当前位置开始读取或写入一个或多个元素,而后将该位置增长所传送元素的数量。若是请求的传输超出limit,则相对的get操做将引起BufferUnderflowException,而相对的put操做将引起BufferOverflowException; 不管哪一种状况,都不会传输数据。
绝对运算采用显式元素索引,而且不影响位置。若是index参数超出limit,则绝对的get和put操做将引起IndexOutOfBoundsException。
固然,也能够经过始终相对于当前位置的通道的I/O操做将数据移入或移出缓冲区。
Marking and resetting
缓冲区的mark标记是在调用reset方法时将其position重置的索引。mark并不是老是定义的,可是定义时,它永远不会为负,也永远不会大于position。若是定义了mark,则在将position或limit调整为小于mark的值时将mark标记丢弃。若是未定义mark,则调用reset方法将引起InvalidMarkException。
Invariants
对于mark,position,limit和capacity,如下不变式成立:
0 <=mark<= position <=limit<=capacity
新建立的缓冲区始终具备零位置和未定义的标记。 初始时limit能够为零,也能够是其余一些值,具体取决于缓冲区的类型及其构造方式。新分配的缓冲区的每一个元素都初始化为零。
Clearing, flipping, and rewinding
除了访问position,limit和capacity以及mark和reset的方法以外,此类还定义了如下对缓冲区的操做:
clear使缓冲区为新的通道读取或相对put操做序列作好准备:将limit设置为capacity,并将位置position为零。
flip使缓冲区为新的通道写入或相对get操做序列作好准备:将limit设置为当前position,而后将position设置为零。
rewind使缓冲区准备好从新读取它已经包含的数据:保留limit不变,并将position设置为零。
Read-only buffers
每一个缓冲区都是可读的,但并不是每一个缓冲区都是可写的。每一个缓冲区类的变异方法都指定为可选操做,当对只读缓冲区调用时,该方法将引起ReadOnlyBufferException。只读缓冲区不容许更改其内容,但其mark,positoin和limit是可变的。缓冲区是否为只读能够经过调用isReadOnly方法来肯定。
Thread safety
缓冲区不能安全用于多个并发线程。若是一个缓冲区将由多个线程使用,则应经过适当的同步来控制对该缓冲区的访问。
Invocation chaining
此类中没有其余要返回值的方法被指定为返回在其上调用它们的缓冲区。这使得方法调用能够连接在一块儿,例如,语句序列:
b.flip();
b.position(23);
b.limit(42);
能够用一个更紧凑的语句代替
b.flip().position(23).limit(42);
基于NIO实现一个简单的聊天程序
上述总结了NIO的基础知识,知道了NIO能够处理文件IO和流IO(网络IO),NIO最大的魅力仍是在于网络IO的处理,接下来将经过NIO实现一个简单的聊天程序来继续了解Java的NIO,这个简单的聊天程序是一个服务端多个客户端,客户端相互之间能够实现数据通讯。
服务端:
public class NioServer { //经过Map来记录客户端链接信息 private static Map<String,SocketChannel> clientMap = new HashMap<String,SocketChannel>(); public static void main(String[] args) throws Exception { //建立ServerSocketChannel 用来监听端口 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //配置为非阻塞 serverSocketChannel.configureBlocking(false); //获取服务端的socket ServerSocket serverSocket = serverSocketChannel.socket(); //监听8899端口 serverSocket.bind(new InetSocketAddress(8899)); //建立Selector Selector selector = Selector.open(); //serverSocketChannel注册到selector 初始时关注客户端的链接事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { try { //阻塞 关注感兴趣的事件 selector.select(); //获取关注事件的SelectionKey集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //根据不一样的事件作不一样的处理 selectionKeys.forEach(selectionKey -> { final SocketChannel client; try { //链接创建起来以后 开始监听客户端的读写事件 if (selectionKey.isAcceptable()) { //如何监听客户端读写事件 首先须要将客户端链接注册到selector //如何获取客户端创建的通道 能够经过selectionKey.channel() //前面只注册了ServerSocketChannel 因此进入这个分支的通道一定是ServerSocketChannel ServerSocketChannel server = (ServerSocketChannel)selectionKey.channel(); //获取到真实的客户端 client = server.accept(); client.configureBlocking(false); //客户端链接注册到selector client.register(selector,SelectionKey.OP_READ); //selector已经注册上ServerSocketChannel(关注链接)和SocketChannel(关注读写) //UUID表明客户端标识 此处为业务信息 String key = "[" + UUID.randomUUID().toString() + "]"; clientMap.put(key,client); }else if (selectionKey.isReadable()) { //处理客户端写过来的数据 对于服务端是可读数据 此处一定是SocketChannel client = (SocketChannel)selectionKey.channel(); //Channel不能读写数据 必须经过Buffer来读写数据 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //服务端读数据到Buffer int count = client.read(byteBuffer); if(count > 0) { //读写转换 byteBuffer.flip(); //写数据到其余客户端 Charset charset = Charset.forName("utf-8"); String receiveMessage = String.valueOf(charset.decode(byteBuffer).array()); System.out.println("client:" + client + receiveMessage); String sendKey = null; for(Map.Entry<String,SocketChannel> entry: clientMap.entrySet()) { if(client == entry.getValue()) { //拿到发送者的UUID 用于模拟客户端的聊天发送信息 sendKey = entry.getKey(); break; } } //给全部的客户端发送信息 for(Map.Entry<String,SocketChannel> entry: clientMap.entrySet()) { //拿到全部创建链接的客户端对象 SocketChannel value = entry.getValue(); ByteBuffer writeBuffer = ByteBuffer.allocate(1024); //这个put操做是Buffer的读操做 writeBuffer.put((sendKey + ":" + receiveMessage).getBytes()); //write以前须要读写转换 writeBuffer.flip(); //写出去 value.write(writeBuffer); } } } }catch (Exception ex) { ex.printStackTrace(); } }); //处理完成该key后 必须删除 不然会重复处理报错 selectionKeys.clear(); }catch (Exception e) { e.printStackTrace(); } } } }
客户端:
public class NioClient { public static void main(String[] args) throws Exception { //建立SocketChannel 用来请求端口 SocketChannel socketChannel = SocketChannel.open(); //配置为非阻塞 socketChannel.configureBlocking(false); //建立Selector Selector selector = Selector.open(); //socketChannel注册到selector 初始时关注向服务端创建链接的事件 socketChannel.register(selector,SelectionKey.OP_CONNECT); //向远程发起链接 socketChannel.connect(new InetSocketAddress("127.0.0.1",8899)); while (true) { //阻塞 关注感兴趣的事件 selector.select(); //获取关注事件的SelectionKey集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //根据不一样的事件作不一样的处理 for(SelectionKey selectionKey : selectionKeys) { final SocketChannel channel; if(selectionKey.isConnectable()) { //与服务端创建好链接 获取通道 channel = (SocketChannel)selectionKey.channel(); //客户端与服务端是否正处于链接中 if(channel.isConnectionPending()) { //完成链接的创建 channel.finishConnect(); //发送链接创建的信息 ByteBuffer writeBuffer = ByteBuffer.allocate(1024); //读入 writeBuffer.put((LocalDateTime.now() + "链接成功").getBytes()); writeBuffer.flip(); //写出 channel.write(writeBuffer); //TCP双向通道创建 //键盘做为标准输入 避免主线程的阻塞 新起线程来作处理 ExecutorService service = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory()); service.submit(() -> { while (true) { writeBuffer.clear(); //IO操做 InputStreamReader inputStreamReader = new InputStreamReader(System.in); BufferedReader reader = new BufferedReader(inputStreamReader); String readLine = reader.readLine(); //读入 writeBuffer.put(readLine.getBytes()); writeBuffer.flip(); //写出 channel.write(writeBuffer); } }); } //客户端也须要监听服务端的写出信息 因此须要关注READ事件 channel.register(selector,SelectionKey.OP_READ); }else if(selectionKey.isReadable()) { //从服务端读取事件 channel = (SocketChannel)selectionKey.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int count = channel.read(readBuffer); if(count > 0) { readBuffer.flip(); Charset charset = Charset.forName("utf-8"); String receiveMessage = String.valueOf(charset.decode(readBuffer).array()); System.out.println("client:" + receiveMessage); } } //处理完成该key后 必须删除 不然会重复处理报错 selectionKeys.clear(); } } } }
演示效果:
最后咱们来总结一下:
一、IO面向流,NIO面向缓冲区,流只能单向传输,而缓冲区能够双向传输,双向传输的模型除了吞吐量获得增长外,这个模型也更接近操做系统和网络的底层;
二、对于网络IO,Selector和Channel组合在一块儿,实现了IO多路复用,这样少许的线程也能处理大量的链接,适用于应对高并发大流量的场景;而对于文件IO,就谈不上IO多路复用,可是FileChannel经过提供transferTo、transferFrom方法来减小底层拷贝的次数也能大幅提高文件IO的性能;
三、Buffer缓冲区用来存储数据,除了没有布尔类型外,其余基础数据类型和Java里面的基础类型是同样的,Buffer的核心属性是position、limit和capacity,读写数据时是这几个变量在不断翻转变化,可是其实这个设计并不优雅,Netty的ByteBuf提供读写索引分离的方式使实现更加优雅;
四、NIO的编程模式总结:
将Socket通道注册到Selector中,监听感兴趣的事件;
当感兴趣的事件就绪时,则会进去咱们处理的方法进行处理;
每处理完一次就绪事件,删除该选择键(由于咱们已经处理完了)。
参考资料:
http://ifeve.com/java-nio-all/
https://segmentfault.com/a/1190000014932357?utm_source=tag-newest
https://www.zhihu.com/question/29005375?sort=created
部分图片截图自某学习视频,若有侵权请告知。
原文出处:https://www.cnblogs.com/iou123lg/p/12497586.html