Java I/O系统从流结构上可分为字节流(以字节为处理单位或称面向字节)和字符流(以字符为处理单位或称面向字符)。
字节流的输人流和输出流基础是InputStream和OutputStream这两个抽象类,字节流的输人输出操作由这两个类的子类实现。字符流是Java1.1版后新增加的以字符为单位进行输人输出处理的流,字符流输入输出的基础是抽象类Reader和Writer。但是最底层都是字节流,字符流的出现有助于我们对文件的读取,例如按行读取之类的。
流又分为节点流和过滤流: 节点流:从特定的地方读写的流类,例如:磁盘或一块内存区域或者键盘的输入。 过滤流:使用节点流作为输人或输出。过滤流是使用一个已经存在的输入流或输出流(可以是节点流也可以是过滤流)连接创建的。 如下图所示: 以数据写入读取为例,我们可以以以下方式构建流的链: 以下是字符流的输入输出的继承关系:
以上就是传统的JAVA I/O系统。
java io的核心概念是流,面向流的编程。 java nio中有3个核心概念:Selector、Channel、Buffer,面向块编程。
他们的关系如下图所示,每个Channel里又有Buffer。Selector可以选择连接到哪个Channel,从而从中读取写入数据。我们的数据交换通过Buffer来进行,所以可以使用Buffer来读也可以写。所有数据都是通过Buffer来进行的。我们也可以单独使用Channle和Buffer而不使用Selector,如下面的文件读取示例,关于Selector和Channle还有Buffer的结合使用,在讲Selector的时候会给出例子。流是单向的,而Channel是双向的。此外,Java中的8中原生数据类型除了boolean都有对应的Buffer类型,IntBuffer、LongBuffer等等。 下面是使用NIO的一个例子:
package com.chester.netty.nio; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class NioTest1 { public static void main(String[] args) throws Exception{ FileInputStream fileInputStream = new FileInputStream("Hello.txt"); FileChannel fileChannel = fileInputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(10); fileChannel.read(byteBuffer); byteBuffer.flip(); while(byteBuffer.remaining() > 0){ byte b = byteBuffer.get(); System.out.println("Character: " + (char)b); } fileInputStream.close(); } }Buffer有三个重要的状态属性:position、limit、capacity;
位置(position):当前缓冲区(Buffer)的位置,将从该位置往后读或写数据。 容量(capacity):缓冲区的总容量上限(不会改变)。 上限(limit):缓冲区的实际容量大小。
我们结合下图来理解: 首先,在写模式中。 position最开始指向0的位置,limit和capacity都指向最后一个位置,然后每写一个position就向后移动一个。limit就像字面意思一样,最多写到limit指向的位置。
我们调用 byteBuffer.flip();转换到读模式(也就是 limit = position; position = 0;)
写模式中各个位置如上图所示,position指向0的位置,limit指向之前写到的最大的位置,capacity不变。每读一个position就向后移动一个,limit是最多能读到的位置。
如果查看NIO的Buffer API文档,你会发现Buffer的很多操作都是围绕这三个值来进行的。比如上面的NIO例子代码中有:byteBuffer.remaining(),它就是返回当前位置和限制之间的元素数, 即limit减position的值;byteBuffer.get()会增加一个position的值。
更多内容可以查看JAVA API文档。查看Buffer的源代码也是一个不错的选择,例如你可以查看到在Buffer中的flip方法:
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }代码中很直白的告诉里你将会对上面所说的值做哪些操作。
现在我们关注一下Buffer申请内存的代码。即:ByteBuffer.allocate(10)。实际上分析它还是比较容易的,我们直接在IDEA中按住Ctrl点击这方法来到实现:
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); }再点击HeapByteBuffer
HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); /* hb = new byte[cap]; offset = 0; */ }点击super,注意这里的new byte[cap]参数。
ByteBuffer(int mark, int pos, int lim, int cap, // package-private byte[] hb, int offset) { super(mark, pos, lim, cap); this.hb = hb; this.offset = offset; }最后可以看到,this.hb = hb,结合new byte[cap]参数,也就是直接在堆上申请一块内存,作为Buffer对象具体存放数据的空间。实际上也比较符合HeapByteBuffer这个名字。在ByteBuffer中还有一个申请内存来存放数据的方法:allocateDirect;要理解这个方法,就必须对JVM的内存模型有一些了解,我们知道JVM中有一个堆外内存的概念。ByteBuffer中使用allocateDirect申请内存就是在堆外内存中申请的。在使用allocateDirect申请的Buffer对象中有一个address变量指向这个堆外内存的地址。
为什么不直接使用堆上的内存呢?实际上是出于对效率的考虑,在Java堆上的内存,操作系统不会直接使用(注意是不会而不是不能),而是在操作系统内存中又申请一块空间。JAVA堆上的数据还要拷贝到操作系统申请的空间上,操作系统再和IO设备进行交互,而使用allocateDirect,将省略这样一次拷贝,也就是0拷贝的概念。
这里做一个扩展,为什么操作系统不直接使用JAVA堆上的数据来和IO设备交互呢?这里就需要对GC有一个理解了,GC的过程中会改变对象的内存位置。而在和IO设备交互的时候显然这个地址不能改变,JVM就必须保证不会进行GC,和IO设备交互是比较耗费时间的,而长时间不进行GC的话必然导致堆的溢出,所以我们可以先拷贝到操作系统的内存中,而后这块内存再和IO设备交互。因为拷贝比直接和IO设备交互要快很多,而短时间的停止GC是可以接受的。
下面说说一个常用的概念。
内存映射文件内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问。这种解决办法能大大简化修改文件的代码。直接修改内存数据,就会修改文件数据,而不用像通常一样需要文件读写,加快了文件访问的速度。内存映射文件的实现是操作系统来维护的。在Nio中可以使用ByteBuffer的子类MappedByteBuffer来进行相应的操作,具体做法可以参考:https://blog.csdn.net/akon_vm/article/details/7429245
之前我们的Channel只有一个Buffer,但是实际上我们可以将一个Channel中的数据写到多个Buffer中(Scattering散开),或者将多个Buffer中的数据写到一个Channel中(Gathering聚合)。
在从Channel中读数据写到多个Buffer中的时候,先写满第一个Buffer。多个Buffer中的数据写到Channel中的时候,也是先写完第一个Buffer。具体的操作可以查看下面的实例代码。
import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; /** * Scattering:在将数据写入到buffer中时,可以采用buffer数组,依次写入,一个buffer满了就写下一个。 * Gatering:在将数据读出到buffer中时,可以采用buffer数组,依次读入,一个buffer满了就读下一个。 */ /** * 使用方式:打开cmd telnet locakhost 8899 * 连接后输入字符串,在控制台会输出每个Buffer信息。 */ public class NioTestServer { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); InetSocketAddress address=new InetSocketAddress(8899); serverSocketChannel.socket().bind(address); int messageLength=2+3+5; ByteBuffer[] byteBuffers=new ByteBuffer[3]; byteBuffers[0]=ByteBuffer.allocate(2); byteBuffers[1]=ByteBuffer.allocate(3); byteBuffers[2]=ByteBuffer.allocate(5); SocketChannel socketChannel=serverSocketChannel.accept(); while (true){ int byteRead=0; //接受客户端写入的的字符串 while(byteRead<messageLength){ long r=socketChannel.read(byteBuffers); byteRead+=r; System.out.println("byteRead:"+byteRead); //通过流打印每个Buffer的position和limit信息 Arrays.asList(byteBuffers).stream(). map(buffer -> "position:"+ buffer.position() +",limit:"+buffer.limit()). forEach(System.out::println); } //将所有buffer都flip。 Arrays.asList(byteBuffers). forEach(buffer -> { buffer.flip(); }); //将数据读出回显到客户端 long byteWrite=0; while (byteWrite < messageLength) { long r=socketChannel.write(byteBuffers); byteWrite+=r; } //将所有buffer都clear Arrays.asList(byteBuffers). forEach(buffer -> { buffer.clear(); }); System.out.println("byteRead:"+byteRead+",byteWrite:"+byteWrite+",messageLength:"+messageLength); } } }在普通的server操作中:
..... while(ture){ Socket socket = serverSocket.accept();//阻塞直到有客户端连接过来 socket.getInputSteam(); ..... ..... ..... } ..... ..... .....如果有多个客户端,这样显然是不合适的,因为无法即时响应客户端。改进如下:
..... while(true){ Socket socket = serverSocket.accept();//阻塞直到有客户端连接过来 new Thread(socket); } ..... ..... .....但是对于多个客户端连接的话,服务器肯定会起多个线程,这对服务器来说压力肯定是非常大的,而且频繁的线程间切换也会损耗性能,NIO的出现解决了这个问题,一个线程可以处理多个客户端事件,具体的工作我们可以再交给具体的任务线程。Selector的具体操作可以查看这里;下面是一个例子:
import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class NioTest4Server { public static void main(String[] args) throws IOException { int [] ports=new int[5]; ports[0]=5000; ports[1]=5001; ports[2]=5002; ports[3]=5003; ports[4]=5004; //一般创建selector的方法 Selector selector=Selector.open(); //for循环用来将多个端口地址和通道绑定 for (int i=0;i<5;i++){ //打开ServerSocketChannel通道 ServerSocketChannel serverSocketChannel=ServerSocketChannel.open(); //通道必须配置成非阻塞状态,FileChannel是无法配置成非阻塞状态的,所以它不能使用下面的访问方式。 serverSocketChannel.configureBlocking(false); //通过ServerSocketChannel的socket()方法获得serverSocket对象。 ServerSocket serverSocket=serverSocketChannel.socket(); //将每一个serverSocket和端口号绑定 InetSocketAddress address=new InetSocketAddress(ports[i]); serverSocket.bind(address); // 将channel注册到selector上,只对感兴趣的事件监听 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); /** * 通道触发了一个事件意思是该事件已经就绪。四种事件,用四种key表示。 * SelectionKey.OP_CONNECT:某个channel成功连接到另一个服务器称为“连接就绪”。 * SelectionKey.OP_ACCEPT:一个server socket channel准备好接收新进入的连接称为“接收就绪”。 * SelectionKey.OP_READ:一个有数据可读的通道可以说是“读就绪”。 * SelectionKey.OP_WRITE:等待写数据的通道可以说是“写就绪”。 */ System.out.println("监听端口为:"+ports[i]); } /** * 一个Selector对象会包含3种类型的SelectionKey集合: * * all-keys:当前所有向Selector注册的SelectionKey的集合,Selector的keys()方法返回该集合 * * 当register()方法执行时,新建一个SelectioKey,并把它加入Selector的all-keys集合中。 * * selected-keys:相关事件已经被Selector捕获的SelectionKey的集合,Selector的selectedKeys()方法返回该集合 * * 在执行Selector的select()方法时,如果与SelectionKey相关的事件发生了, * 这个SelectionKey就被加入到selected-keys集合中,程序直接调用selected-keys集合的remove()方法, * 或者调用它的iterator的remove()方法,都可以从selected-keys集合中删除一个SelectionKey对象。 * * cancelled-keys:已经被取消的SelectionKey的集合,Selector没有提供访问这种集合的方法 * * 如果关闭了与SelectionKey对象关联的Channel对象,或者调用了SelectionKey对象的cancel方法, * 这个SelectionKey对象就会被加入到cancelled-keys集合中,表示这个SelectionKey对象已经被取消。 */ while(true){ //阻塞,直到有事件发送 int keyNumbers=selector.select(); System.out.println("返回key的数量:"+ keyNumbers); //获得所有SelectionKey,因为同一时间可能连接多个channel,从而产生多个SelectionKey Set<SelectionKey> selectionKeys=selector.selectedKeys(); //迭代所有已经获得的Selectedkey Iterator<SelectionKey> iterator= selectionKeys.iterator(); //迭代selectionKeys while (iterator.hasNext()){ SelectionKey selectionKey=iterator.next(); if (selectionKey.isAcceptable()){ //通过key来获得发送事件的通道 ServerSocketChannel serverSocketChannel= (ServerSocketChannel) selectionKey.channel(); //如果客户端连接,获得客户端channel SocketChannel socketChannel=serverSocketChannel.accept(); socketChannel.configureBlocking(false); //通过selector来监听读事件 socketChannel.register(selector, SelectionKey.OP_READ); //如果不移除这个key,他还存在在Selectedkey集合中,那么下次迭代他还存在 iterator.remove(); System.out.println("获取客户端连接:"+socketChannel); } else if (selectionKey.isReadable()){ SocketChannel socketChannel= (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer=ByteBuffer.allocate(512); while(true){ byteBuffer.clear(); int read= socketChannel.read(byteBuffer); if (read<=0){ break; } byteBuffer.flip(); socketChannel.write(byteBuffer); } Charset charset = Charset.forName("utf-8"); String massage = String.valueOf(charset.decode(byteBuffer).array()); System.out.println("读取:"+massage+",来自于"+socketChannel); iterator.remove(); } } } } }以上就是JAVA NIO系统主要内容的讲解了,Selector是NIO中最重要的内容,一定要好好理解。
