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分爲讀模式和寫模式,如下圖所示

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的圖示:

要使用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?

          }
            //處理完了的就緒事件,一定要刪除,否則會反覆處理
            iterator.remove();
        }
    }

}

}


### 參考文章

[如何學習Java的NIO?](https://www.zhihu.com/question/29005375/answer/667616386)

















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章