Java NIO真得就这么简单

Java NIO真得就这么简单

大多数人肯定了解Java IO, 但是对于NIO一般是陌生的,但是Java NIO是一个高频知识点,又不得不学,所以本文通过图文+代码的方式,保姆级别的讲述Java NIO的各个知识点。觉得写得好的,希望点个赞,给个收藏。

Java IO 与 Java NIO的区别

Java IO 与 Java NIO读取文件的差别

普通Java IO

public static void ioReadFile(String fileName) throws Exception {
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(fileName)));
    int len = 0;
    byte [] bytes = new byte[32];
    while((len = bis.read(bytes)) != -1){
        for(int i = 0; i < len; i++) {
        	System.out.print((char) bytes[i]);
        }
    }
    bis.close();
}

Java NIO

public static void nioReadFile(String fileName) throws Exception{
    RandomAccessFile read = new RandomAccessFile(fileName, "r");
    FileChannel channel = read.getChannel();

    ByteBuffer byteBuffer = ByteBuffer.allocate(32);

    while(channel.read(byteBuffer) > 0) {
        byteBuffer.flip();
        while(byteBuffer.hasRemaining()){
            System.out.print((char) byteBuffer.get());
        }
        byteBuffer.clear();
    }
    read.close();
    channel.close();
}

Java IO与Java NIO主要有三个区别

  1. 普通IO是面向流(stream)的处理,而NIO是面向缓冲区(buffer)的处理

    • 面向流

      Java IO面向流意味着每次从流中读取一个或多个字节,直至读取所有字节,字节数据没有被缓存在任何地方,不能前后移动在流中读取数据的位置。

    • 面向缓冲区

      Java NIO面向缓冲区,数据读取到一个稍后处理的缓冲区,需要时可在缓冲区中前后移动。增加了处理过程中的灵活性。

  2. 普通IO是阻塞IO,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。而NIO是非阻塞IO,Java 一个线程从某通道发送请求读取数据,如果无可读取数据,其不会被阻塞,在数据变得可读取之前可以做其他事情

  3. 普通IO不支持selector,而NIO支持selector.(selector下文会详细解释).通过Selector对象,可以实现IO复用,下文会详细讲述。

NIO的三个核心部分

从上面NIO读取文件的代码,可以看出,NIO方式读取文件,首先需要获取一个传输数据的通道channel,之后构建一个承载数据的buffer.打个形象点的比喻就是channel相当于铁路,而buffer相当于货运火车,通过铁路,货运火车可以源源不断将货物,从出发点运抵目的地。其实还有一个selector,构成NIO的三个核心。

channel

channel的读写是双向的,既可以从通道读数据,又可以往通道写数据,而流一般是单向的,比如输入流,输出流,同时channel也支持异步得读写

channel主要有一下四种实现

  • FileChannel 从文件中读取数据
  • DatagramChannel 通过UDP读取网络中的数据
  • SocketChannel 通过TCP读取网络中的数据
  • ServerSockerChannel 监听新进来的TCP连接,类似web服务

buffer

buffer其实本质是一块可以写入数据,也可以读取数据的内存

其工作流程主要分为一下几个步骤

  • 写入数据到buffer
  • 调用flip方法
  • 从buffer中读取数据
  • 调用clear方法或者compact方法

buffer主要有三个属性capacity,position,limit,通过分析其三个属性,可以详细了解其工作原理

capacity:buffer的大小,对于ByteBuffer,就是规定其能存储的最大byte数

buffer分为读模式和写模式,如下图所示

Java NIO真得就这么简单_第1张图片

position:下一个要被读或写的元素的位置。

写数据时,初始的position值为0.当一个byte、long等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1.

读数据时,position也是从0开始,向前读取数据。写模式向读模式切换时,会将position重置为0

limit:缓冲区里的数据总数。

在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。

重要方法

flip方法:从写模式切换到读模式,position置为0,limit设置为之前写模式的position值。

rewind方法:将position设置为0,但limit不变

clear方法:position设置为0,limit设置为capacity的值

compact方法:将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素正后面。limit属性依然像clear方法一样设置成capacity.现在Buffer准备好了写数据,但是不会覆盖未读数据。

mark、reset方法:通过调用Buffer.mark()方法,可以标记Buffer的一个特定position。之后可以通过调用Buffer.reset方法恢复到这个position

Buffer.mark();
//call Buffer.get() a couple of times, e.g. during parsing.
Buffer.reset();  //set position back to mark.

selector

Selector是channel的管理器,Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。这是在一个单线程中使用一个Selector处理3个Channel的图示:

Java NIO真得就这么简单_第2张图片

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

Java NIO IO复用

java NIO复用,主要是通过Selector对象实现的,通过Selector对象,注册监听多个通道的多个事件,事件主要有四种,如下所示

OP_ACCEPT: 监听连接事件,服务器监听客户端的连接请求

OP_CONNECT:连接就绪事件,客户端与服务器已经连接成功

OP_READ:读就绪事件,通道中已有可读数据,可以指向读操作

OP_WRITE:写就绪事件,可以向通道中写数据

IO复用代码实例

下面代码主要实现客户端向服务端传送图片数据的功能

client代码

public class NoBlockClient {

    public static void main(String[] args) throws IOException {
		//创建一个SocketChannel对象,通过从TCP读取数据,并绑定相应服务端ip和port
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
		
        //设置为非阻塞IO
        socketChannel.configureBlocking(false);
		
        //为需要传送的图片文件建立channel
        FileChannel fileChannel = FileChannel.open(Paths.get("C:\\Users\\LENOVO\\Desktop\\1.png"), StandardOpenOption.READ);
		
        //创建运载数据所需的ByteBuffer对象
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        //从FileChannel中读取数据到ByteBuffer
        //将ByteBuffer对象写入SocketChannel
        while (fileChannel.read(buffer) != -1) {

            buffer.flip();

            socketChannel.write(buffer);

            buffer.clear();
        }
		//关闭相应通道
        fileChannel.close();
        socketChannel.close();
    }
}

Server代码

public class NoBlockServer {

    public static void main(String[] args) throws IOException {
		//创建一个ServerSocketChannel对象
        ServerSocketChannel server = ServerSocketChannel.open();
		//设置为非阻塞IO
        server.configureBlocking(false);
		//绑定监听端口
        server.bind(new InetSocketAddress(6666));
		
        //建立Selector对象,并注册OP_ACCEPT事件
        Selector selector = Selector.open();
        server.register(selector, SelectionKey.OP_ACCEPT);
		
        //selector.select()方法一直会阻塞,除非监听的事件就绪
        while (selector.select() > 0) {
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

            while (iterator.hasNext()) {

                SelectionKey selectionKey = iterator.next();
			   //有新的连接
                if (selectionKey.isAcceptable()) {
				  //为连接创建一个SocketChannel	
                    SocketChannel client = server.accept();

                    client.configureBlocking(false);
				  //监听读事件
                    client.register(selector, SelectionKey.OP_READ);
                    
				//可读
                } else if (selectionKey.isReadable()) {
	
                    SocketChannel client = (SocketChannel) selectionKey.channel();

                    ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
				  
                    //创建一个FileChannel,接受网络数据
                    FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
                    
				  //从SocketChannel读取数据,并通过ByteBuffer,向FileChannel对象传输
                    while (client.read(buffer) > 0) {
                        buffer.flip();
                        outChannel.write(buffer);
                        buffer.clear();
                    }
                    client.close();
                }
                //处理完了的就绪事件,一定要删除,否则会反复处理
                iterator.remove();
            }
        }

    }
}

参考文章

如何学习Java的NIO?

你可能感兴趣的:(Java学习)