Java NIO之Java中的IO分类

时间:2024-01-25 17:55:34

前言 

前面两篇文章(Java NIO之理解I/O模型(一)Java NIO之理解I/O模型(二))介绍了,IO的机制,以及几种IO模型的内容,还有涉及到的设计模式。这次要写一些更贴近实际一些的内容了,终于要说到了Java中的各种IO了。我也是边学边理解,有写的不对的地方,欢迎小伙伴们指出和补充。

Java中的IO分类

BIO

BIO是指 Blocking IO 在JDK1.0的时候就引入了,直到JDK1.4一直都是Java中唯一的IO方式。它的主要实现方式就是,一个线程执行一个请求,如果请求数据量较大线程就会一直占用着,又或者请求什么也不做,也是会占用一个线程的,这样当客户端请求数量变多时,服务端线程数也跟着变多,最终就会导致服务端CPU崩溃的。当然可以使用线程池来减小CPU的压力,但是毕竟线程池也不是从本质上解决问题(长链接,线程池大小,以及拒绝策略等等因素都要考虑)。

无论是网络连接还是本地读取输出操作都是这种方式。具体的例子我就不举了(毕竟BIO不是本次的重点)。

NIO

Java中的NIO其实就是使用的多路I/O复用模型,前面的文章已经介绍过原理了,但是在理解Java的NIO之前,还是先介绍几个Java NIO的基础概念:Channel(通道),Buffer(缓冲区),Selector(选择器)。

Channel(通道)

Channel可以理解为,互通的管道,和Java的IO中的各种Stream(InputStream、OutputStream等等)一个等级,只不过Channel是双向的,而Stream是单向的。通道的作用是将数据移入或移出道各种I/O源,即可读又可写。

在Java中Channel类的层次结构相当复杂,有多个接口和许多可选操作。不过,常用的也就几个。

FileChannel

DatagramChannel

SocketChannel

ServerSocketChannel

FileChannel可以对文件进行读和写,DatagramChannel可以以UDP的协议来进行数据读写,SocketChannel以TCP的协议来对网络两端进行读写,ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写。

Buffer(缓冲区)

Buffer是一个高效的数据容器,在NIO中所有的数据操作都必须经过缓冲区,这点是和BIO不同的,BIO是直接将数据写到Stream对象中的。因为Stream对象的设计是按顺序一个字节一个字节的传送数据。虽然出于性能考虑,也可以传递字节数组,但是基本概念都是一个字节一个字节的传递数据。通道与之不同之处在于,通道会传送缓冲区的数据块,而且通道的基本概念就是按照一个数据块一个数据块的去读和写。所以也可以将缓冲区理解为一个字节数组,专门用来存储以及准备好出入通道的字节。

如下图:

 

如上图所示,无论是客户端发送和接收数据,还是服务端接收和相应数据,都是从缓冲区中进行数据操作的。

在Java中除了boolean外,所有的基本数据类型都有特定的Buffer子类:

ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。网络程序几乎只会使用ByteBuffer,但程序偶尔也会使用其他类型来取代ByteBuffer。

除了数据列表外,每个缓冲区都记录了信息的4个关键部分。无论是何种类型,都有相同的方法来获取和设置这些值:

  • 位置(position)

缓冲区中将读取或写入的下一个位置。这个位置从0开始计,最大值等于缓冲区的大小。可以用下面两个方法获取和设置。

public final int position();
public final Buffer position(int newPosition);
  • 容量(capacity)

缓冲区可以保存的最大数目。容量值在创建缓冲区时设置,此后不能改变。

可以用以下方法读取:

public final int capacity();
  • 限度(limit)

缓冲区中可访问数据的末尾位置。只要不改变限度,就无法读/写超过这个位置的数据,即使缓冲区有更大的容量也没有用。限度可以用下面两个放获取和设置。

public final int limit();
public final Buffer limit(int newLimit);
  • 标记(mark)

缓冲区中客户端指定的索引。通过调用mark()可以将标记设置为当前位置。调用reset()可以将当前位置设置为所标识的位置。

public final Buffer mark() ;
public final Buffer reset();

如果将位置设置为低于现有标记,则丢弃这个标记。

与读取InputStream不同,读取缓冲区实际上不会以任何方式改变缓冲区中的数据。只可能向前或向后设置位置,从而可以从缓冲区中某个特定位置开始读取。类似的程序可以调整限度,从而控制将要读取的数据的末尾。只有容量是固定的。

Selector(选择器)

Selector是Java NIO中最重要的一部分,Selector的作用就是用单线程来轮询处理注册的Channel,一旦哪个Channel的数据准备就绪了,就可以进行处理了。

如下图:

使用Selector,要先向Selector中注册Channel, 然后调用它的select()方法,这个方法会一直阻塞到某个注册的Channel中的事件准备就绪。一旦select()方法返回,线程就可以处理这些事件了,比如新的连接进入,数据接收等。

Selector类并没有注册新通道的方法,register()方法是在SelectableChannel类中声明。SelectableChannel类是实现自Channel接口的,它支持将Channel注册到Selector中。

SelectableChannel提供了两个注册Channel的方法:

public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;
public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException;

第一个参数代表通道要向哪个选择器注册。第二个参数是SelectionKey类中的一个命名常量,标识所注册的操作。

SelectionKey定义了4个命名位常量,用户选择操作类型:

SelectionKey.OP_READ;
SelectionKey.OP_WRITE;
SelectionKey.OP_CONNECT;
SelectionKey.OP_ACCEPT;

当一个通道需要在同一个选择器中关注多个操作,只需要用户位“或”操作符组合这些常量即可。

channel.register(selector,SelectionKey.OP_READ | SelectionKey.OP_WRITE);

第三个参数是可选的,代表键的附件。这个参数通常用户存储链接状态,例如:如果要实现一个Web服务器,可能要附加一个FileInputStream或FileChannel,这个流或通道连接到服务器提供给客户端的本地文件。

例子:

复制文件的读写操作,可以用来举例说明NIO的一个大致过程。

public static void copyFileByNIO(String src,String dst) throws IOException {

        //声明源文件和目标文件
        RandomAccessFile aFile = new RandomAccessFile(src, "rw");
        RandomAccessFile bFile = new RandomAccessFile(dst, "rw");

        //获得传输通道channel
        FileChannel inChannel = aFile.getChannel();
        FileChannel outChannel = bFile.getChannel();
        //获得容器buffer
        ByteBuffer buffer= ByteBuffer.allocate(1024);
        while(true){
            //判断是否读完文件
            int eof =inChannel.read(buffer);
            if(eof==-1){
                break;
            }
            //重设一下buffer的position=0,limit=position
            buffer.flip();
            //开始写
            outChannel.write(buffer);
            //写完要重置buffer,重设position=0,limit=capacity
            buffer.clear();
        }
        inChannel.close();
        outChannel.close();
        aFile.close();
        bFile.close();
    } 

AIO

AIO这次就不介绍了,我后续要单独的拿出一整篇来介绍AIO。