第18章 JAVA NIO

Java NIO (New IO)是從Java1.4版本開始引入的一個新的IO API,可以替代次奧準的Java IO API。NIO支持面向緩衝區的,基於通道的IO操作。NIO將以更加高效的方式進行文件的讀寫操作。

比較NIO與IO

IO模型 IO NIO
通信 面向流(Stream Oriented) 面向緩衝區(Buffer Oriented)
處理 阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
觸發 (無) 選擇器(Selectors)

面向流與面向緩衝區的區別以及對通道與緩衝區的理解
面向流是單向的,文件與程序之間建立數據流,輸入流和輸出流都需要建立不同的“管道”。抽象的理解爲自來水管和下水道吧,水就是傳輸的數據。

面向緩衝區,文件與程序之間建立通道,裏面存在緩衝區。抽象的理解可以把通道認爲是鐵路,緩衝區認爲是一輛火車,而載着的貨物也就是所要傳輸的數據了。

簡單認爲:通道負責傳輸,緩衝區負責存儲。

緩衝區(Buffer)

Buffer在Java NIO 中負責數據的存取,緩衝區就是數組,用於存儲不同數據類型的數據。

緩衝區類型

根據數據類型的不同(boolean除外),提供了相應類型的緩衝區。
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述緩衝區的管理方式幾乎一致,通過allocate()獲取緩衝區。ByteBuffer最爲常用。

緩衝區存取數據的兩個核心方法

獲取 Buffer 中的數據
get() :讀取單個字節
get(byte[] dst):批量讀取多個字節到 dst 中
get(int index):讀取指定索引位置的字節(不會移動 position)

放入數據到 Buffer
put(byte b):將給定單個字節寫入緩衝區的當前位置
put(byte[] src):將 src 中的字節寫入緩衝區的當前位置
put(int index, byte b):將指定字節寫入緩衝區的索引位置(不會移動 position)

緩衝區的四個核心屬性

①capacity: 容量,表示緩衝區中最大存儲數據的容量,一但聲明不能改變。(因爲底層是數組,數組一但被創建就不能被改變)
②limit: 界限,表示緩衝區中可以操作數據的大小。(limit後數據不能進行讀寫)
③position: 位置,表示緩衝區中正在操作數據的位置,0<= mark <= position <= limit <= capacity
④mark:標記,表示記錄當前position的位置,可以通過reset()恢復到mark的位置。
在這裏插入圖片描述

幾個常用方法

①allocate():分配緩衝區:

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

②put():將數據存入緩衝區

String str = "這是一個測試數據";
byteBuffer.put(str.getBytes());

③flip():切換到讀取數據的模式
④get():讀取數據

byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(new String(bytes,0,bytes.length));

⑤rewind():重複讀,使position歸0
⑥clear():清空緩衝區,緩衝區中的數據還在,只是處於“被遺忘“的狀態。只是不知道位置界限等,讀取會有困難。
⑦mark():標記。mark會記錄當前的position,limit,capacity
⑧reset():position,limit,capacity恢復到mark記錄的位置
案例:

package com.hanker.nio1;

import java.nio.ByteBuffer;

public class TestByteBuffer {

	public static void main(String[] args) {
		//分配直接緩衝區
		ByteBuffer buf = ByteBuffer.allocateDirect(1024);
		System.out.println("是否直接緩衝區:"+buf.isDirect());
	}

	private static void test2() {
		String str = "abcde";
		//1.分配一個指定大小的緩衝區
		ByteBuffer buf = ByteBuffer.allocate(1024);
		buf.put(str.getBytes());
		buf.flip();
		
		byte[] dst = new byte[buf.limit()];
		buf.get(dst, 0, 2);
		System.out.println(new String(dst,0,2));
		System.out.println(buf.position());
		//7. mark():標記
		buf.mark();
		
		buf.get(dst, 2, 2);
		System.out.println(new String(dst,2,2));
		System.out.println(buf.position());
		
		//8.reset():恢復到mark的位置
		buf.reset();
		System.out.println(buf.position());
		
		//9.是否還有剩餘數據
		if(buf.hasRemaining()) {
			//如果有還剩下多少數據
			System.out.println(buf.remaining());
		}
	}

	private static void test1() {
		String str = "abcde";
		//1.分配一個指定大小的緩衝區
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		System.out.println("------------allocate()--------");
		System.out.println(buf.position());
		System.out.println(buf.limit());
		System.out.println(buf.capacity());
		
		//2.利用put()存入數據到緩衝區
		buf.put(str.getBytes());
		
		System.out.println("------------put()--------");
		System.out.println(buf.position());
		System.out.println(buf.limit());
		System.out.println(buf.capacity());
		
		//3.切換讀取數據模式
		buf.flip();
		
		System.out.println("------------flip()--------");
		System.out.println(buf.position());
		System.out.println(buf.limit());
		System.out.println(buf.capacity());
		//4.利用get()讀取緩衝區中的數據
		byte[] dst = new byte[buf.limit()];
		buf.get(dst);
		System.out.println("讀取的數據:"+new String(dst,0,dst.length));
		
		System.out.println("------------get()--------");
		System.out.println(buf.position());
		System.out.println(buf.limit());
		System.out.println(buf.capacity());
		
		//5.rewind():可重複讀數據
		buf.rewind();
		
		System.out.println("------------rewind()--------");
		System.out.println(buf.position());
		System.out.println(buf.limit());
		System.out.println(buf.capacity());
		
		//6.clear():清空緩衝區,緩衝區中的數據依然存在,處於被遺忘狀態
		buf.clear();
		
		System.out.println("------------clear()--------");
		System.out.println(buf.position());
		System.out.println(buf.limit());
		System.out.println(buf.capacity());
		
		System.out.println((char)buf.get());
	}

}

直接緩衝區與非直接緩衝區

①字節緩衝區要麼是直接的,要麼是非直接的。如果爲直接字節緩衝區,則 Java 虛擬機會盡最大努力直接在此緩衝區上執行本機 I/O 操作。也就是說,在每次調用基礎操作系統的一個本機 I/O 操作之前(或之後),虛擬機都會盡量避免將緩衝區的內容複製到中間緩衝區中(或從中間緩衝區中複製內容)。

②直接字節緩衝區可以通過調用此類的 allocateDirect() 工廠方法來創建。此方法返回的緩衝區進行分配和取消分配所需成本通常高於非直接緩衝區。直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對應用程序的內存需求量造成的影響可能並不明顯。所以,建議將直接緩衝區主要分配給那些易受基礎系統的本機 I/O 操作影響的大型、持久的緩衝區。一般情況下,最好僅在直接緩衝區能在程序性能方面帶來明顯好處時分配它們。

③直接字節緩衝區還可以通過 FileChannel 的 map() 方法 將文件區域直接映射到內存中來創建。該方法返回MappedByteBuffer 。Java 平臺的實現有助於通過 JNI 從本機代碼創建直接字節緩衝區。如果以上這些緩衝區中的某個緩衝區實例指的是不可訪問的內存區域,則試圖訪問該區域不會更改該緩衝區的內容,並且將會在訪問期間或稍後的某個時間導致拋出不確定的異常。

④字節緩衝區是直接緩衝區還是非直接緩衝區可通過調用其 isDirect() 方法來確定。提供此方法是爲了能夠在性能關鍵型代碼中執行顯式緩衝區管理。

非直接緩衝區:通過allocate()方法分配緩衝區,將緩衝區建立在JVM的內存中。在每次調用基礎操作系統的一個本機IO之前或者之後,虛擬機都會將緩衝區的內容複製到中間緩衝區(或者從中間緩衝區複製內容),緩衝區的內容駐留在JVM內,因此銷燬容易,但是佔用JVM內存開銷,處理過程中有複製操作。

非直接緩衝區的寫入步驟:
①創建一個臨時的ByteBuffer對象。
②將非直接緩衝區的內容複製到臨時緩衝中。
③使用臨時緩衝區執行低層次I/O操作。
④臨時緩衝區對象離開作用域,並最終成爲被回收的無用數據。
在這裏插入圖片描述

直接緩衝區:通過allocateDirect()方法分配直接緩衝區,將緩衝區建立在物理內存中,可以提高效率。

直接緩衝區在JVM內存外開闢內存,在每次調用基礎操作系統的一個本機IO之前或者之後,虛擬機都會避免將緩衝區的內容複製到中間緩衝區(或者從中間緩衝區複製內容),緩衝區的內容駐留在物理內存內,會少一次複製過程,如果需要循環使用緩衝區,用直接緩衝區可以很大地提高性能。雖然直接緩衝區使JVM可以進行高效的I/O操作,但它使用的內存是操作系統分配的,繞過了JVM堆棧,建立和銷燬比堆棧上的緩衝區要更大的開銷.
在這裏插入圖片描述
觀察源碼
allocate():

 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;
      */
 }

可以看出直接在堆內存中開闢空間,也就是數組。
allocateDriect():

public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
}

進入到DirectByteBuffer()中可以看到:

DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

由 VM.isDirectMemoryPageAligned();可以看出直接調用了內存頁,讓操作系統開闢緩存空間。

通道

通道(Channel)表示IO源與目標打開的連接。Channel類似於傳統的”流“,只不過Channel本身不能直接訪問數據,Channel只能與Buffer進行交互。
在這裏插入圖片描述

  • Channel是一個獨立的處理器,專門用於IO操作,附屬於CPU。
  • 在提出IO請求的時候,CPU不需要進行干預,也就提高了效率。

作用:用於源節點與目標節點的連接。在Java NIO中負責緩衝區中數據的傳輸。Channel本身並不存儲數據,因此需要配合Buffer一起使用。

主要實現類
java.nio.channels.Channel接口:
用於本地數據傳輸:FileChannel
用於網絡數據傳輸:SocketChannel/ServerSocketChannel/DatagramChannel

獲取通道
①Java 針對支持通道的類提供了一個 getChannel() 方法。本地IO操:FileInputStream/FileOutputStream/RandomAccessFile
網絡IO:Socket/ServerSocket/DatagramSocket
②在JDK1.7中的NIO.2 針對各個通道提供了靜態方法 open();
③在JDK1.7中的NIO.2 的Files工具類的 newByteChannel();
案例:

package com.hanker.nio1;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestChannel {

	public static void main(String[] args) throws IOException {
		String url = "http://henan.163.com/20/0120/08/F3AQO6B404398DMR.html";
		//讀取web網頁
		try(FileChannel destChannel = 
				FileChannel.open(Paths.get("html.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);){
			InputStream input = new URL(url).openStream();
			ReadableByteChannel srcChannel = Channels.newChannel(input);
			destChannel.transferFrom(srcChannel, 0, Integer.MAX_VALUE);
		}
	}
	//通道之間的數據傳輸(也是利用的直接緩衝器的方式)
	private static void test4() throws IOException {
		//獲取通道對象
		FileChannel inChannel = FileChannel.open(Paths.get("1.jpeg"), StandardOpenOption.READ);
		FileChannel outChannel = FileChannel.open(Paths.get("4.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
		//inChannel.transferTo(0, inChannel.size(), outChannel);
		outChannel.transferFrom(inChannel, 0, inChannel.size());
		inChannel.close();
		outChannel.close();
	}
	//內存映射文件-直接緩衝區
	private static void test3() throws IOException {
		//獲取通道對象
		FileChannel inChannel = FileChannel.open(Paths.get("1.jpeg"), StandardOpenOption.READ);
		FileChannel outChannel = FileChannel.open(Paths.get("3.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
		//內存映射文件
		MappedByteBuffer inMappeBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
		MappedByteBuffer outMappeBuf  = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
		//直接對緩衝區進行數據的讀寫操作
		byte[] dst = new byte[inMappeBuf.limit()];
		inMappeBuf.get(dst);
		outMappeBuf.put(dst);
		
		inChannel.close();
		outChannel.close();
	}
	//利用通道完成文件複製-非直接緩衝區
	private static void test2() throws FileNotFoundException, IOException {
		FileInputStream fis = new FileInputStream("1.jpeg");
		FileOutputStream fos = new FileOutputStream("2.jpeg");
		
		//①獲取通道
		FileChannel inChannel = fis.getChannel();
		FileChannel outChannel = fos.getChannel();
		
		//②分配指定大小的緩衝區
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//③將通道中的數據寫入緩衝區
		while(inChannel.read(buf) != -1) {
			buf.flip();//切換讀取數據的模式
			outChannel.write(buf);//將緩衝區的數據寫入通道中
			buf.clear();//清空緩衝區
		}
		outChannel.close();
		inChannel.close();
		fos.close();
		fis.close();
	}

	private static void test1() throws IOException {
		// 創建FileChannel對象 
		FileChannel channel = FileChannel.open(Paths.get("my.txt"), StandardOpenOption.CREATE,StandardOpenOption.WRITE);
		ByteBuffer buffer = ByteBuffer.allocate(64);
		buffer.putChar('A').flip();
		channel.write(buffer);
	}

}

分散(Scatter)與聚集(Gather)

分散讀取(Scattering Reads):將通道中的數據分散到多個緩衝區中
聚集寫入(Gathering Writes):將多個緩衝區中的數據聚集到通道中
分散讀取
在這裏插入圖片描述
聚集寫入
在這裏插入圖片描述

package com.hanker.nio1;

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestScatter_Gather {

	public static void main(String[] args) throws Exception {
		RandomAccessFile raf1 = new RandomAccessFile("my.txt", "rw");
		//1.獲取通道
		FileChannel ch1 = raf1.getChannel();
		//2.分配指定大小的緩衝區
		ByteBuffer buf1 = ByteBuffer.allocate(100);
		ByteBuffer buf2 = ByteBuffer.allocate(1024);
		//3.分散讀取
		ByteBuffer [] bufs = {buf1,buf2};
		ch1.read(bufs);
		for(ByteBuffer byteBuffer : bufs) {
			byteBuffer.flip();
		}
		System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
		System.out.println("==========");
		System.out.println(new String(bufs[1].array(),0,bufs[0].limit()));
		//4.聚集寫入
		RandomAccessFile raf2 = new RandomAccessFile("my2.txt", "rw");
		FileChannel ch2 = raf2.getChannel();
		ch2.write(bufs);
		ch2.close();
		ch1.close();
	}

}

字符集Charset

設置字符集,解決亂碼問題
編碼:字符串->字節數組
解碼:字節數組->字符串
思路
用Charset.forName(String)構造一個編碼器或解碼器,利用編碼器和解碼器來對CharBuffer編碼,對ByteBuffer解碼。需要注意的是,在對CharBuffer編碼之前、對ByteBuffer解碼之前,請記得對CharBuffer、ByteBuffer進行flip()切換到讀模式如果編碼和解碼的格式不同,則會出現亂碼。
案例:

package com.hanker.nio1;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Map.Entry;
import java.util.SortedMap;

public class TestCharSet {

	public static void main(String[] args) throws CharacterCodingException {
		Charset charset = Charset.forName("utf-8");
        Charset charset1 = Charset.forName("gbk");

        // 獲取編碼器 utf-8
        CharsetEncoder encoder = charset1.newEncoder();

        // 獲得解碼器 gbk
        CharsetDecoder decoder = charset1.newDecoder();

        CharBuffer buffer = CharBuffer.allocate(1024);
        buffer.put("絕不敷衍,從不懈怠!");
        buffer.flip();

        // 編碼
        ByteBuffer byteBuffer = encoder.encode(buffer);
        for (int i = 0; i < 20; i++) {
            System.out.println(byteBuffer.get());
        }

        // 解碼
        byteBuffer.flip();
        CharBuffer charBuffer = decoder.decode(byteBuffer);
        System.out.println(charBuffer.toString());
	}
	//獲取系統支持的所有字符集
	private static void test1() {
		SortedMap<String, Charset> sets = Charset.availableCharsets();
		for(Entry<String, Charset>  entry  :sets.entrySet()) {
			System.out.println(entry.getKey()+"--->"+entry.getValue());
		}
	}

}

在for循環中使用過到了ByteBuffer的get()方法。一開始習慣性的在get()方法里加上了變量i隨即出現了問題,無法取得數據。註釋代碼byteBuffer.flip();之後可以執行。當直接使用get()方法時,不加byteBuffer.flip();則會報錯。所以就來區別一下ByteBuffer裏的get();與get(int index);的區別。
查看get();方法源碼:

/**
 * Relative <i>get</i> method.  Reads the byte at this buffer's
 * current position, and then increments the position.
 * @return  The byte at the buffer's current position
 *
 * @throws  BufferUnderflowException
 * If the buffer's current position is not smaller than its limit
 */
public abstract byte get();

可以看出返回的值是“ The byte at the buffer’s current position”,就是返回緩衝區當前位置的字節。"then increments the position"也說明了返回字節之後,position自動加1,就是指向下一字節。上述情況如果是get(index),則是下面的方法:

/**
  * Absolute <i>get</i> method.  Reads the byte at the given
  * index.
  * @param  index
  *         The index from which the byte will be read
  *
  * @return  The byte at the given index
  *
  * @throws  IndexOutOfBoundsException
  *          If <tt>index</tt> is negative
  *          or not smaller than the buffer's limit
  */
public abstract byte get(int index);
  • 由“The byte at the given index”可以知道返回的是給定索引處的字節。position並未移動。如果之後再執行flip();操作則讀取不到任何數據。原因接着往下看。
  • 再來看一看 flip();方法源碼:
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}

注意:limit=position,如果使用get(index);的方法,則執行完position = 0,所以limit也會變成0,之後無法讀取數據。

網絡阻塞IO與非阻塞IO

傳統IO是阻塞式的,也就是說,當一個線程調用 read() 或 write()時,該線程被阻塞,直到有一些數據被讀取或寫入,該線程在此期間不能執行其他任務。因此,在完成網絡通信進行 IO 操作時,由於線程會阻塞,所以服務器端必須爲每個客戶端都提供一個獨立的線程進行處理,當服務器端需要處理大量客戶端時,性能急劇下降。
NIO是非阻塞式的,當線程從某通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO操作,所以單獨的線程可以管理多個輸入和輸出通道。因此, NIO 可以讓服務器端使用一個或有限幾個線程來同時處理連接到服務器端的所有客戶端。

阻塞模式與非阻塞模式

傳統阻塞IO方式:客戶端向服務器端發送請求,服務器端便開始監聽客戶端的數據是否傳過來。這時候客戶端在準備自己的數據,而服務器端就需要乾等着。即使服務器端是多線程的,但有時一味增加線程數,只會讓阻塞的線程越來越多。NIO的非阻塞方式:將用於傳輸的通道全部註冊到選擇器上。選擇器的作用是監控這些通道的IO狀況(讀,寫,連接,接收數據的情況等狀況)。

選擇器與通道之間的聯繫:

通道註冊到選擇器上,選擇器監控通道;當某一通道,某一個事件就緒之後,選擇器纔會將這個通道分配到服務器端的一個或多個線程上,再繼續運行。例如客戶端需要發送數據給服務器端,只當客戶端所有的數據都準備完畢後,選擇器纔會將這個註冊的通道分配到服務器端的一個或多個線程上。在客戶端準備數據這段時間,服務器端的線程可以執行別的任務。
在這裏插入圖片描述

選擇器( Selector)與SelectableChannle

選擇器( Selector) 是 SelectableChannle 對象的多路複用器, Selector 可以同時監控多個 SelectableChannel 的 IO 狀況,也就是說,利用 Selector可使一個單獨的線程管理多個 Channel。 Selector 是非阻塞 IO 的核心。

SelectableChannle 的結構如下圖(注意:FileChannel不是可作爲選擇器複用的通道!FileChannel不能註冊到選擇器Selector!FileChannel不能切換到非阻塞模式!FileChannel不是SelectableChannel的子類!)
在這裏插入圖片描述

選擇器的使用方法

①創建 Selector :通過調用 Selector.open() 方法創建一個 Selector
②向選擇器註冊通道: SelectableChannel.register(Selector sel, int ops)
當調用 register(Selector sel, int ops) 將通道註冊選擇器時,選擇器對通道的監聽事件,需要通過第二個參數 ops 指定。可以監聽的事件類型( 可使用 SelectionKey 的四個常量表示):
讀 : SelectionKey.OP_READ ( 1)
寫 : SelectionKey.OP_WRITE ( 4)
連接 : SelectionKey.OP_CONNECT ( 8)
接收 : SelectionKey.OP_ACCEPT ( 16)
若註冊時不止監聽一個事件,則可以使用“位或”操作符連接。

選擇鍵(SelectionKey)

SelectionKey: 表示 SelectableChannel 和 Selector 之間的註冊關係。每次向選擇器註冊通道時就會選擇一個事件(選擇鍵)。 選擇鍵包含兩個表示爲整數值的操作集。操作集的每一位都表示該鍵的通道所支持的一類可選擇操作。

方 法 描 述
int interestOps() 獲取感興趣事件集合
int readyOps() 獲取通道已經準備就緒的操作的集合
SelectableChannel channel() 獲取註冊通道
Selector selector() 返回選擇器
boolean isReadable() 檢測 Channal 中讀事件是否就緒
boolean isWritable() 檢測 Channal 中寫事件是否就緒
boolean isConnectable() 檢測 Channel 中連接是否就緒
boolean isAcceptable() 檢測 Channel 中接收是否就緒

Selector常用方法

Set keys() 所有的 SelectionKey 集合。代表註冊在該Selector上的Channel
selectedKeys() 被選擇的 SelectionKey 集合。返回此Selector的已選擇鍵集
int select() 監控所有註冊的Channel,當它們中間有需要處理的 IO 操作時,
該方法返回,並將對應得的 SelectionKey 加入被選擇的
SelectionKey 集合中,該方法返回這些 Channel 的數量。
int select(long timeout) 可以設置超時時長的 select() 操作
int selectNow() 執行一個立即返回的 select() 操作,該方法不會阻塞線程
Selector wakeup() 使一個還未返回的 select() 方法立即返回
void close() 關閉該選擇器

使用NIO完成網絡通信的三個核心

  1. 通道(Channel):負責連接

  2. 緩衝區(Buffer):負責數據的存取

  3. 選擇器(Select):是SelectableChannel的多路複用器。用於監控SelectableChannel的IO狀況。

    java.mio.channels.Channel 接口:
    	|-- SelectableChannel
    		|--SocketChannel
    		|--ServerSocketChannel
    		|--DatagramChannel
    		|--Pipe.SinkChannel
    		|--Pipe.sourceChannel
    

阻塞模式完成客戶端向服務器端傳輸數據

//===========服務器端======
package com.hanker.nio1;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestServerSocketChannel {

	public static void main(String[] args) throws IOException {
		//1.獲取通道
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.bind(new InetSocketAddress(9898));//綁定鏈接
		//2. 創建FileChannel
		FileChannel outChannel = FileChannel.open(Paths.get("5.jpeg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
		//3.獲取客戶端鏈接的通道
		SocketChannel socketChannel = ssc.accept();
		//4.分配指定大小的緩衝區
		ByteBuffer dst = ByteBuffer.allocate(1024);
		//5.接收數據並保存
		while(socketChannel.read(dst) != -1) {
			dst.flip();
			outChannel.write(dst);
			dst.clear();
		}
		//6.關閉通道
		socketChannel.close();
		outChannel.close();
		ssc.close();
	}

}
//==========客戶端==========
package com.hanker.nio1;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestSocketChannel {

	public static void main(String[] args) throws Exception {
		SocketChannel socketChannel = 
            SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
        // 切換成非 阻塞模式
        socketChannel.configureBlocking(false);
        FileChannel inputChannel = 
            FileChannel.open(Paths.get("1.jpeg"), StandardOpenOption.READ);
        ByteBuffer clientBuffer = ByteBuffer.allocate(1024);
        while (inputChannel.read(clientBuffer) != -1){
            clientBuffer.flip();
            socketChannel.write(clientBuffer);
            clientBuffer.clear();
        }
        socketChannel.close();
        inputChannel.close();
	}
}

一次聊天:

package com.hanker.nio1;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestServerSocketChannel2 {

	public static void main(String[] args) throws IOException {
		//1.獲取通道
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.bind(new InetSocketAddress(9898));//綁定鏈接
		//2. 創建FileChannel
		FileChannel outChannel = FileChannel.open(Paths.get("6.jpeg"), 	StandardOpenOption.WRITE,StandardOpenOption.CREATE);
		//3.獲取客戶端鏈接的通道
		SocketChannel socketChannel = ssc.accept();
		//4.分配指定大小的緩衝區
		ByteBuffer dst = ByteBuffer.allocate(1024);
		//5.接收數據並保存
		while(socketChannel.read(dst) != -1) {
			dst.flip();
			outChannel.write(dst);
			dst.clear();
		}
		//6.返回信息
		dst.put("服務端接收數據成功".getBytes());
		dst.flip();
		socketChannel.write(dst);
		//7.關閉通道
		socketChannel.close();
		outChannel.close();
		ssc.close();
	}

}
//======================服務器端============
package com.hanker.nio1;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TestSocketChannel2 {

	public static void main(String[] args) throws Exception {
		SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
        // 切換成非 阻塞模式
        socketChannel.configureBlocking(false);
        FileChannel inputChannel = FileChannel.open(Paths.get("1.jpeg"), StandardOpenOption.READ);
        ByteBuffer clientBuffer = ByteBuffer.allocate(1024);
        while (inputChannel.read(clientBuffer) != -1){
            clientBuffer.flip();
            socketChannel.write(clientBuffer);
            clientBuffer.clear();
        }
        socketChannel.shutdownOutput();
        //接收服務器的反饋
        int len = 0;
        while( (len = socketChannel.read(clientBuffer))!= -1 ) {
        	clientBuffer.flip();
        	System.out.println(new String(clientBuffer.array(),0,len));
        	clientBuffer.clear();
        }
        
        socketChannel.close();
        inputChannel.close();
	}
}

非阻塞模式完成客戶端向服務器端傳輸數據

package com.hanker.nio1;

import java.io.IOException;
import java.net.InetSocketAddress;
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.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;

public class TestNonBlockingNIO {

	//客戶端
	@Test
	public void client() throws IOException{
		//1.獲取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
		//2.切換非阻塞模式
		sChannel.configureBlocking(false);
		//3.分配指定大小的緩衝區
		ByteBuffer buf = ByteBuffer.allocate(1024);
		//4.發送數據給服務器
		Scanner scan = new Scanner(System.in);
		while(scan.hasNext()) {
			String str = scan.next();
			buf.put((new Date().toString() + "\n" + str).getBytes());
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		//5.關閉通道
		scan.close();
		sChannel.close();
	}
	
	//服務端
	@Test
	public void server() throws IOException{
		//1.獲取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		//2.切換非阻塞模式
		ssChannel.configureBlocking(false);
		//3.綁定鏈接
		ssChannel.bind(new InetSocketAddress(9898));
		//4.獲取選擇器
		Selector selector = Selector.open();
		//5.將通道註冊到選擇器,並且指定“監聽接收事件”
		ssChannel.register(selector, SelectionKey.OP_ACCEPT);
		//6.輪詢式的獲取選擇器上已經“準備就緒”的事件
		while(selector.select() > 0) {
			//7.獲取當前選擇器中所有註冊的“選擇鍵(已就緒的監聽事件)”
			Iterator<SelectionKey> it =  selector.selectedKeys().iterator();
			while(it.hasNext()) {
				//8.獲取準備就緒的事件
				SelectionKey sk =  it.next();
				//9.判斷具體是什麼事件準備就緒
				if(sk.isAcceptable()) {
					//10.若“接收就緒”,獲取客戶端鏈接
					SocketChannel sChannel = ssChannel.accept();
					//11.切換非阻塞模式
					sChannel.configureBlocking(false);
					//12.將該通道註冊到選擇器上
					sChannel.register(selector, SelectionKey.OP_READ);
				}else if(sk.isReadable()) {
					//13.獲取當前選擇器上“讀就緒”狀態的通道
					SocketChannel sChannel =(SocketChannel) sk.channel();
					//14.讀取數據
					ByteBuffer buf = ByteBuffer.allocate(1024);
					int len  = 0;
					while( (len = sChannel.read(buf))>0) {
						buf.flip();
						System.out.println(new String(buf.array(),0,len));
						buf.clear();
					}
				}
				//15.取消選擇鍵
				it.remove();
			}
		}
	}
}

先啓動服務器端,再啓動客戶端。可以啓動多個客戶端進行訪問。

DatagramChannel

這個與上面的非常相似,所以這裏只給一個實現的代碼案例:

package com.hanker.nio1;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;

import org.junit.Test;

public class TestNonBlockingNIO2 {

	@Test
	public void send() throws IOException{
		DatagramChannel dc = DatagramChannel.open();
		dc.configureBlocking(false);
		ByteBuffer buf = ByteBuffer.allocate(1024);
		Scanner input = new Scanner(System.in);
		while(input.hasNext()) {
			String str = input.next();
			buf.put( (new Date().toString()+":\n"+str).getBytes()  );
			buf.flip();
			dc.send(buf,new InetSocketAddress("127.0.0.1",9898));
			buf.clear();
		}
		input.close();
		dc.close();
	}

	@Test
	public void receive() throws IOException{
		DatagramChannel dc = DatagramChannel.open();
		dc.configureBlocking(false);
		dc.bind(new InetSocketAddress(9898));
		Selector selector = Selector.open();
		dc.register(selector, SelectionKey.OP_READ);
		while(selector.select()>0) {
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			while(it.hasNext()) {
				SelectionKey sk = it.next();
				if(sk.isReadable()) {
					ByteBuffer dst = ByteBuffer.allocate(1024);
					dc.receive(dst);
					dst.flip();
					System.out.println(new String( dst.array(),0,dst.limit() ));
					dst.clear();
				}
			}
			it.remove();
		}
	}	
}

管道(Pipe)

Java NIO 管道是兩個線程之間的單向數據連接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。
在這裏插入圖片描述

package com.hanker.nio1;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;

import org.junit.Test;

public class TestPipe {
	@Test
	public void test1() throws IOException{
		//1.獲取管道
		Pipe pipe = Pipe.open();
		//2.將緩衝區中的時速局寫入管道
		ByteBuffer buf = ByteBuffer.allocate(1024);
		Pipe.SinkChannel sinkChannel = pipe.sink();
		buf.put("通過單向管道發送數據".getBytes());
		buf.flip();
		sinkChannel.write(buf);
		
		//3.讀取緩衝區中的數據
		Pipe.SourceChannel sourceChannel  = pipe.source();
		buf.flip();
		int len = sourceChannel.read(buf);
		System.out.println(new String(buf.array(),0,len));
		
		sourceChannel.close();
		sinkChannel.close();
	}
}

源碼分析

源碼分析請參考

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