傳統的IO操作是同步阻塞IO模式(BIO),數據的讀取寫入必須阻塞在一個線程內等待其完成。NIO則是同步非阻塞IO模式。BIO面向流操作,NIO面向緩衝區操作。
NIO主要有三大核心部分:Channel(通道),Buffer(緩衝區), Selector。傳統IO基於字節流和字符流進行操作,而NIO基於Channel和Buffer(緩衝區)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個線程可以監聽多個數據通道。
一 Channel(通道)
Chanel通道相當於IO操作的載體,數據通過Channel讀取和寫入,全雙工模式(雙向)。Channel類似流,但是又和流不同,流的讀寫是單向的比如InputStream、OutputStream。但是Chanel既可以從通道里面讀取數據又可以把數據寫到通道里面去。通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。
1.1 FileChannel
FileChannel是一個基於文件的通道。可以通過文件通道讀寫文件。有一點要注意FileChannel無法設置爲非阻塞模式。它總是以阻止模式運行。
FileChannel提供的函數
方法 | 解釋 |
---|---|
open | 打開一個文件,把文件和通道關聯起來 |
read | 從當前通道讀取字節序列到給定的緩衝區 |
write | 從緩衝區向該通道寫入字節序列 |
position | 跳轉到文件的指定位置 |
size | 獲取文件大小 |
truncate | 截取文件 |
force | 將通道里尚未寫入磁盤的數據強制寫到磁盤上 |
transferTo | 將字節從當前通道傳輸到給定的可寫字節通道 |
transferFrom | 將給定的可讀字節通道上的字節傳輸到當前通道中 |
map | 將當前通道某個區域直接映射到內存中 |
lock | 獲取此通道文件的獨佔鎖定 |
tryLock | 嘗試獲取此通道文件的給定區域的鎖定 |
下面我們通過一個簡單的實例來看FileChannel怎麼使用。
@Test
public void fileChannelRead() {
try {
// 開啓FileChannel
RandomAccessFile aFile = new RandomAccessFile("D:\\job\\git\\java-study\\nio\\src\\main\\resources\\fileChanel.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
// 從FileChannel通道讀取數據到緩衝區ByteBuffer
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("讀取到的數據長度 " + bytesRead);
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
// 繼續讀取文件信息
bytesRead = inChannel.read(buf);
}
aFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void fileChannelWrite() {
try {
// 開啓FileChannel
RandomAccessFile aFile = new RandomAccessFile("D:\\job\\git\\java-study\\nio\\src\\main\\resources\\fileChanelWrite.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
byte[] forWrite = "需要寫入的字符串。".getBytes(StandardCharsets.UTF_8);
buf.put(forWrite, 0, forWrite.length);
buf.flip();
// 寫入數據
while (buf.hasRemaining()) {
inChannel.write(buf);
}
aFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
1.2 DatagramChannel
DatagramChannel主要是用來基於UDP通信的通道。
DatagramChannel方法介紹
DatagramChannel方法 | 返回值 | 解釋 |
---|---|---|
open() | DatagramChannel | 創建通道 |
bind(SocketAddress local) | DatagramChannel | 綁定端口 |
validOps() | int | 只支持OP_READ/OP_WRITE兩種操作 |
socket() | DatagramSocket | 獲取與其關聯的底層DatagramSocket |
isConnected() | boolean | 檢測是否已經建立了Socket鏈接 |
connect(SocketAddress remote) | DatagramChannel | 鏈接remote端 |
disconnect() | DatagramChannel | 斷開通道連接 |
getRemoteAddress() | SocketAddress | 獲取遠程地址 |
receive(ByteBuffer dst) | SocketAddress | 接收數據 |
send() | int | 發送數據,向指定的地址發送數據 |
read() | int | 必須在connect()之後調用,接收數據 |
write() | int | 必須在connect()之後調用,發送數據 |
getLocalAddress() | SocketAddress | 獲取本地地址 |
注意,connect()、send()、read() 三個函數是配套使用的。
接下來我們通過三個例子來說明DatagramChannel的用法,下面的例子都是阻塞模式的,等講到Selector的時候我們在講非阻塞的用法。
1.2.1 UDP服務端
UDP服務端需要調用bind()函數綁定本地端口。
/**
* UDP 服務端
*/
@Test
public void datagramChannelService() {
try {
// 獲取通道
DatagramChannel datagramChannel = DatagramChannel.open();
// 綁定端口8989,作爲UDP服務端
datagramChannel.bind(new InetSocketAddress(8989));
// 分配Buffer,用於收發數據
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
// 等待接受客戶端發送數據
SocketAddress socketAddress = datagramChannel.receive(buffer);
if (socketAddress != null) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到客戶端消息 " + socketAddress.toString() + ":" + new String(b, StandardCharsets.UTF_8));
// 接收到消息後給發送方迴應
sendDataBack(socketAddress, datagramChannel);
}
}
} catch (IOException e) {
// ignore
}
}
/**
* 給socketAddress地址發送消息
*/
private void sendDataBack(SocketAddress socketAddress, DatagramChannel datagramChannel) throws IOException {
String message = "send back";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(message.getBytes(StandardCharsets.UTF_8));
buffer.flip();
datagramChannel.send(buffer, socketAddress);
}
1.2.2 UDP客戶端
如果UDP作爲客戶端的話,可以直接往UDP服務端發送消息,服務端接收到消息的時候同時獲取到了對應客戶端的地址信息。又可以把消息發送回來。
// UDP客戶端
@Test
public void datagramChannelClient() {
try {
final DatagramChannel channel = DatagramChannel.open();
// 開一個線程一直接收UDP服務端發送過來的消息
new Thread(() -> {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
SocketAddress socketAddress = channel.receive(buffer);
if (socketAddress != null) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到消息 " + socketAddress.toString() + ":" + new String(b, StandardCharsets.UTF_8));
}
}
} catch (Exception e) {
// ignore
}
}).start();
int messageIndex = 0;
// 控制檯輸入數據,然後發送給指定的地址
while (true) {
// 5S發送一次數據
Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS);
sendMessage(channel, new InetSocketAddress("192.168.5.14", 8989), String.valueOf(messageIndex++));
}
} catch (IOException e) {
// ignore
}
}
private void sendMessage(DatagramChannel channel, InetSocketAddress address, String mes) throws IOException {
if (mes == null || mes.isEmpty()) {
return;
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put(mes.getBytes(StandardCharsets.UTF_8));
buffer.flip();
channel.send(buffer, address);
}
1.2.3 connect用法
DatagramChannel的connect()方法可以和指定的地址綁定起來,配合write()、read()函數在兩者之間收發消息。比如下面的實例我們和time-a.nist.gov建立連接獲取時間。
/**
* UDP connect() 在特定的地址上收發消息
*/
@Test
public void datagramChannelConnect() {
try {
// 獲取通道
DatagramChannel datagramChannel = DatagramChannel.open();
// 連接到特定的地址,time-a.nist.gov 獲取時間。只在這個地址間收發消息 write,read 方法
datagramChannel.connect(new InetSocketAddress("time-a.nist.gov", 37));
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put((byte) 0);
buffer.flip();
// 發送數據到 time-a.nist.gov
datagramChannel.write(buffer);
buffer.clear();
// 前四個字節補0
buffer.putInt(0);
// 從 time-a.nist.gov 讀取數據
datagramChannel.read(buffer);
buffer.flip();
// convert seconds since 1900 to a java.util.Date
long secondsSince1900 = buffer.getLong();
long differenceBetweenEpochs = 2208988800L;
long secondsSince1970 = secondsSince1900 - differenceBetweenEpochs;
long msSince1970 = secondsSince1970 * 1000;
Date time = new Date(msSince1970);
// 打印時間
System.out.println(time);
} catch (Exception e) {
// ignore
}
}
再次強調下,上面實例代碼我們都是用的阻塞模式實現的。等下面講到Selector的時候我們在講怎麼用非阻塞的方式實現。
1.3 SocketChannel
SocketChannel主要是用來基於TCP通信的通道,它一般用來作爲客戶端的套接字,它有點類似於java中的Socket類。
SocketChannel方法介紹
SocketChannel方法 | 返回值 | 解釋 |
---|---|---|
open() | SocketChannel | 創建SocketChannel通道 |
validOps() | int | 通道支持的操作,OP_READ、OP_WRITE、OP_CONNECT |
bind(SocketAddress local) | SocketChannel | 地址綁定 |
setOption(SocketOption name, T value) | SocketChannel | Socket的選項配置,StandardSocketOptions.SO_KEEPALIVE等 |
shutdownInput() | SocketChannel | 在沒有關閉通道的情況下,關閉讀操作連接 |
shutdownOutput() | SocketChannel | 在沒有關閉通道的情況下,關閉到通道的寫操作連接 |
socket() | Socket | 獲取與通道關聯的socket |
isConnected() | boolean | 判斷通道的網絡socket是否連接 |
isConnectionPending() | boolean | 判斷通道是否正在進行操作連接.只有當尚未finishConnection且已經調用connect時,返回true |
connect(SocketAddress remote) | boolean | 連接通道的socket |
finishConnect() | boolean | 完成到socket通道的連接任務,一般在非阻塞的情況下用到, |
getRemoteAddress() | SocketAddress | 返回通道socket連接的遠端地址 |
read() | int or long | 接收數據 |
write | int or long | 發送數據 |
getLocalAddress() | SocketAddress | 返回通道socket連接的本地地址 |
setOption():用於給socket設置一些選項配置,比如keep alive。socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);等等。具體可以看看StandardSocketOptions裏面的一些標準選項。
關於connect()、isConnectionPending()、finishConnect()三個函數的關係我們稍微屢一下。分兩種情況來考慮:
- 阻塞模式:connect()是阻塞的,這個時候isConnectionPending()、finishConnect()兩個函數我覺得意義不大,因爲你本來就是阻塞狀態的,connect()函數成功了,這兩個函數值也就確定了:isConnectionPending()->false、finishConnect()->true(如果連接失敗他就是false)。
- 非阻塞模式:調用connect()方法底層socket建立連接的時候。因爲是非阻塞的如果連接立即建立成功,則返回true,否則返回false。則此後一旦成功建立連接就必須通過調用finishConnect()方法來完成鏈接。在沒有調用finishConnect()之前isConnectionPending()->true,調用之後isConnectionPending()->false。
1.3.1 SocketChannel使用
/**
* TCP客戶端,阻塞模式
*/
@Test
public void socketChannelClient() {
try {
SocketChannel channel = SocketChannel.open();
// 這裏使用的是阻塞模式
channel.connect(new InetSocketAddress("192.168.5.14", 6800));
// KEEP ALIVE setOption()函數的使用,一定要在連接成功之後設置
channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int readLength = channel.read(buffer);
if (readLength >= 0) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到消息 " + ":" + new String(b, StandardCharsets.UTF_8));
// 把收到的消息又發送回去
buffer.clear();
buffer.put(b);
buffer.flip();
channel.write(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
1.4 ServerSocketChannel
ServerSocketChannel也是用來基於TCP通信的通道,它一般用來作爲服務端的套接字,它有點類似於java中的ServerSocket類。一般用來接收客戶端的連接,在客戶端連接的的基礎之上做一些收發消息的處理。
ServerSocketChannel主要方法
ServerSocketChannel方法 | 返回值 | 解釋 |
---|---|---|
open() | ServerSocketChannel | 建立通道 |
validOps() | int | 當前通道支持的操作,OP_ACCEPT |
bind(SocketAddress local) | ServerSocketChannel | 綁定到指定的端口,還可以指定最多多少個連接 |
setOption(SocketOption name, T value) | ServerSocketChannel | Socket的選項配置,StandardSocketOptions.SO_KEEPALIVE等 |
socket() | ServerSocket | 獲取與通道關聯的socket |
accept() | SocketChannel | 接收客戶端的連接 |
getLocalAddress() | SocketAddress | 返回通道socket連接的本地地址 |
ServerSocketChannel是用於接收客戶端連接的,在接收到(accept函數)客戶端連接之後會拿到基於客戶端連接的SocketChannel。和每個客戶端的操作都是通過SocketChannel實現的。
1.4.1 ServerSocketChannel的使用
/**
* TCP服務端 -- 阻塞模式
*/
@Test
public void socketChannelServer() {
try {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress("192.168.5.14", 6800));
while (true) {
// 接收客戶端的連接,之後拿到的就是SocketChannel了,之後都是基於SocketChannel做相應的操作
SocketChannel clientSocketChannel = channel.accept();
clientSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put("hello".getBytes(StandardCharsets.UTF_8));
buffer.flip();
// 給客戶端發送消息
clientSocketChannel.write(buffer);
// 在收下客戶端的消息
buffer.clear();
int readLength = clientSocketChannel.read(buffer);
if (readLength >= 0) {
buffer.flip();
byte[] b = new byte[buffer.limit()];
int bufferReceiveIndex = 0;
while (buffer.hasRemaining()) {
b[bufferReceiveIndex++] = buffer.get();
}
System.out.println("收到消息 " + ":" + new String(b, StandardCharsets.UTF_8));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
二 Buffer(緩衝區)
Buffer用於和Channel通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成了NIO Buffer對象。
我們先介紹先Buffer裏面的三個屬性,接着在介紹下Buffer裏面主要的方法。
Buffer裏面三個屬性:capacity、position、limit。
- capacity:作爲一個內存塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往裏寫capacity個byte、long,char等類型。一旦Buffer滿了,需要將其清空(通過讀數據或者清除數據)才能繼續往裏寫數據。
- position:當你寫數據到Buffer中時,position表示當前的位置。初始的position值爲0。當一個byte、long等數據寫到Buffer後,position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity–1;當讀取數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置爲0。當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。position簡單來說就相當於遊標的作用。
- limit:在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。寫模式下,limit等於Buffer的capacity;當切換Buffer到讀模式時,limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)。
一定要注意position、limit在讀模式和謝模式下代表的含義,以及兩個模式之間切換的時候position、limit做了那些變化。
Buffer裏面常用函數。
public abstract class Buffer {
/**
* 獲取當前緩衝區的容量 -- capacity
*/
public final int capacity();
/**
* 獲取當前緩衝區的位置 -- position
*/
public final int position();
/**
* 設置當前緩衝區的位置 -- position
*/
public final Buffer position(int newPosition);
/**
* 獲取當前緩衝區的限制 -- limit
*/
public final int limit();
/**
* 設置當前緩衝區的限制 -- limit
*/
public final Buffer limit(int newLimit);
/**
* mark(), reset()函數是配對使用的,將當前緩衝區的標記(mark)設置在當前位置(position) -- mark
*/
public final Buffer mark();
/**
* 通過調用mark()方法,可以標記Buffer中的一個特定position。
* 之後可以通過調用Buffer.reset()方法恢復到這個position
*/
public final Buffer reset();
/**
* 清除此緩存區。將position = 0;limit = capacity;mark = -1;一般在寫入數據之前調用
*/
public final Buffer clear();
/**
* flip()方法可以把Buffer從寫模式切換到讀模式。調用flip方法會把position歸零,
* 並設置limit爲之前的position的值。
* 也就是說,現在position代表的是讀取位置,limit標示的是已寫入的數據位置。
*/
public final Buffer flip();
/**
* 將position設回0,這個時候你可以重讀Buffer中的所有數據。
* limit保持不變,仍然表示能從Buffer中讀取多少個元素
*/
public final Buffer rewind();
/**
* return limit - position; 返回limit和position之間相對位置差
*/
public final int remaining();
/**
* return position < limit,返回是否還有未讀內容
*/
public final boolean hasRemaining();
/**
* 判斷此緩衝區是否爲只讀
*/
public abstract boolean isReadOnly();
/**
* 判斷此緩衝區是否由可訪問的數組支持
*/
public abstract boolean hasArray();
/**
* 返回支持此緩衝區的數組
*/
public abstract Object array();
/**
* 返回該緩衝區的緩衝區的第一個元素的背襯數組中的偏移量
*/
public abstract int arrayOffset();
/**
* 判斷個緩衝區是否爲 direct
*/
public abstract boolean isDirect();
}
flip()、hasRemaining()、clear()、rewind()、mark()、reset()幾個函數要着重理解下。
要想使用Buffer來讀寫數據一般遵循以下四個步驟:
- 寫數據到Buffer裏面。可以從Channel讀取出來寫入到緩衝區中,也可以調用put方法寫入到緩衝區中。
- 調用flip()方法,切換到讀數據模式。這個時候position指向第一個位置,limit指向寫入數據的最後位置。
- 從Buffer中讀取數據。一般從緩衝區讀取數據寫入到通道中,也可以調用get方法讀取到Buffer裏面的數據。
- 調用clear()或者compact()方法清空緩衝區。
當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。
在讀模式下,可以讀取之前寫入到buffer的所有數據。一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。
有兩種方式能清空緩衝區:
- clear():方法會清空整個緩衝區。
- compact():這個方法在ByteBuffer、CharBuffer、ShortBuffer等方法裏面提供。 該方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區。
我們使用Buffer緩衝區。一般使用的都是他的子類:ByteBuffer、CharBuffer、ShortBuffer、LongBuffer、FloatBuffer、DoubleBuffer這些。Buffer的這些子類都是基於JAVA一些基本數據類型實現的一個Buffer緩衝區。我們先看下這些子類一般都會有的一些方法(Buffer裏面的方法他們都會有,Buffer裏面的方法我們就不重複介紹了)。
出Buffer提供的放方法之外,子類裏面額外的方法。
Buffer子類方法 | 描述 | |
---|---|---|
allocate(int capacity) | Buffer實例化方法 | 從堆空間中分配一個容量大小爲capacity的對應類型的數組作爲緩衝區的數據存儲器 |
allocateDirect(int capacity) | Buffer實例化方法 | 不使用JVM堆棧而是通過操作系統來創建內存塊用作緩衝區,它與當前操作系統能夠更好的耦合,因此能進一步提高I/O操作速度。但是分配直接緩衝區的系統開銷很大,因此只有在緩衝區較大並長期存在,或者需要經常重用時,才使用這種緩衝區 |
wrap(T[] array) | Buffer實例化方法 | 這個緩衝區的數據會存放在對應數組中,對應數組或buff緩衝區任何一方中數據的改動都會影響另一方。其實Buffer底層本來就有一個對應數組負責來保存buffer緩衝區中的數據,通過allocate方法系統會幫你構造一個對應類型組 |
wrap(T[] array, int offset,intlength) | Buffer實例化方法 | 在上一個方法的基礎上可以指定偏移量和長度,這個offset也就是包裝後byteBuffer的position,而length呢就是limit-position的大小,從而我們可以得到limit的位置爲length+position(offset) |
slice() | 常規方法 | 創建新的緩衝區,其內容是此緩衝區內容的共享子序列 |
duplicate() | 常規方法 | 創建共享此緩衝區內容的新的字節緩衝區 |
asReadOnlyBuffer() | 常規方法 | 創建共享此緩衝區內容的新的只讀字節緩衝區 |
get() | 常規方法 | 從緩衝區獲取數據 |
put() | 常規方法 | 把數據放入到緩衝區中 |
compact() | 常規方法 | 清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處 |
關於Buffer的使用,我們以ByteBuffer和CharBuffer來舉例說明。其他的Buffer子類適應也都是很簡單的。
ByteBuffer的使用
@Test
public void byteBufferTest() {
// 創建一個ByteBuffer實例
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 清空
buffer.clear();
// 寫入數據
byte[] putByteArray = "hello word!".getBytes(StandardCharsets.UTF_8);
buffer.put(putByteArray);
// 切換到讀模式
buffer.flip();
// 把數據讀取出來
buffer.slice();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
// 重新讀
buffer.rewind();
// ps: 這個時候buffer.limit()就是數組元素的個數
byte[] retByte = new byte[buffer.limit()];
buffer.get(retByte);
System.out.println(new String(retByte, StandardCharsets.UTF_8));
}
CharBuffer使用距離
@Test
public void charBufferTest() {
// 創建一個ByteBuffer實例
CharBuffer buffer = CharBuffer.allocate(1024);
// 清空
buffer.clear();
// 寫入數據
char[] putArray = "hello word!".toCharArray();
buffer.put(putArray);
// 切換到讀模式
buffer.flip();
// 把數據讀取出來
buffer.slice();
while (buffer.hasRemaining()) {
System.out.print(buffer.get());
}
System.out.println();
// 重新讀
buffer.rewind();
// ps: 這個時候buffer.limit()就是數組元素的個數
char[] retByte = new char[buffer.limit()];
buffer.get(retByte);
System.out.println(String.valueOf(retByte));
}
三 Selector(多路複用器)
Selector提供了選擇已就緒任務的能力,Selector會不斷輪詢註冊在上面的Channel,某個Channel發生讀或寫事件,則該Channel就處於就緒狀態,會被Selector輪詢出來。然後通過SelectionKey獲取就緒Channel的集合,進行後續IO操作。Selector允許單線程處理多個Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。
Selector方法介紹
Selector方法 | 返回值 | 解釋 |
---|---|---|
open() | Selector | Selector的創建 |
isOpen() | boolean | 判斷此選擇器是否已打開 |
provider() | SelectorProvider | 返回創建此通道的提供程序 |
keys() | Set | 返回所有的SelectionKey |
selectedKeys() | Set | 返回已選擇的SelectionKey集合,要在select()之後調用 |
selectNow() | int | 非阻塞,返回有多少通道就緒 |
select(long timeout) | int | 阻塞到至少有一個通道在你註冊的事件上就緒了 |
select() | int | 阻塞到至少有一個通道在你註冊的事件上就緒了,返回值表示有多少通道就緒 |
wakeup() | Selector | Selector的喚醒 |
wakeup() 函數稍微講下。Selector的選擇方式有三種:select()、select(timeout)、selectNow()。selectNow的選擇過程是非阻塞的,與wakeup沒有太大關係。select(timeout)和select()的選擇過程是阻塞的,其他線程如果想終止這個過程,就可以調用wakeup來喚醒select()。
3.1 Selector創建
通過調用Selector.open()方法創建一個Selector對象。
Selector selector = Selector.open();
3.2 把Channel註冊到Selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
註冊到Selector的Channel是有前提添加的。Channel必須是非阻塞的,必須是SelectableChannel的子類。所以FileChannel不適用Selector,因爲FileChannel不能切換爲非阻塞模式,更準確的來說是因爲FileChannel沒有繼承SelectableChannel。
把Channel註冊到Selector的時候還得指定監聽事件。就是告訴Selector我這個Channel對什麼事件感興趣。當Channel上有這個事件發送的時候這個Channel就會被輪詢出來。NIO提供了四個事件:
Channel註冊事件 | 解釋 |
---|---|
Selectionkey.OP_READ | 讀就緒 |
Selectionkey.OP_WRITE | 寫就緒 |
Selectionkey.OP_CONNECT | 連接就緒 |
Selectionkey.OP_ACCEPT | 接收就緒 |
我們有兩種方式來設置Selector對Channel的哪些事件感興趣。一個是在把Channel註冊到Selector的時候設置。我們上面已經講了這種情況。另一個是調用SelectionKey的interestOps()函數來修改Selector對Channel感興趣的事件。
3.2.1 SelectionKey
每個Channel向Selector註冊時,都將會創建一個SelectionKey對象。一個SelectionKey鍵表示了一個特定的通道對象和一個特定的選擇器對象之間的註冊關係。並維護了Channel事件。
SelectionKey方法介紹
方法 | 返回值 | 解釋 |
---|---|---|
channel() | SelectableChannel | 返回此選擇鍵所關聯的通道 |
selector() | Selector | 返回此選擇鍵所關聯的選擇器 |
isValid() | boolean | 檢測此key是否有效 |
cancel() | void | 請求將此鍵取消註冊.一旦返回成功,那麼該鍵就是無效的 |
interestOps() | int | 判斷Selector對Channel的哪些事件感興趣,OP_READ、OP_WRITE等事件 |
interestOps(int ops) | SelectionKey | 設置Selector對Channel的哪些事件感興趣 |
readyOps() | int | 獲取此鍵上ready操作集合.即在當前通道上已經就緒的事件 |
isReadable() | boolean | 檢測此鍵是否爲"read"事件.等效於:k.readyOps() & OP_READ != 0 |
isWritable() | boolean | 檢測此鍵是否爲"write"事件 |
isConnectable() | boolean | 檢測此鍵是否爲"connect"事件 |
isAcceptable() | boolean | 檢測此鍵是否爲"accept"事件 |
attach(Object ob) | Object | 將給定的對象作爲附件添加到此key上.在key有效期間,附件可以在多個ops事件中傳遞 |
attachment() | Object | 獲取附件.一個channel的附件,可以再當前Channel(或者說是SelectionKey)生命週期中共享,但是attachment數據不會作爲socket數據在網絡中傳輸 |
3.3 從Selector中選擇就緒的Channel
從Selector中選擇就緒的Channel,其實是去選擇SelectionKey,然後通過SelectionKey拿到對應的Channel。通過Channel做相應的操作。
從Selector中選擇就緒的Channel也很簡單。先調用Selecotor的select()方法選擇出已經就緒的通道,Selector會幫助我們把這些就緒的通道放到一個就緒列表裏目前。然後我們在調用Selector的selectedKeys()方法把這些通道都拿出來。
3.4 Selecotr完整實例
@Test
public void tcpClient() {
try {
SocketChannel socketChannel = SocketChannel.open();
// 連接
socketChannel.connect(new InetSocketAddress("192.168.5.14", 6800));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip();
while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
readBuffer.clear();
socketChannel.read(readBuffer);
readBuffer.flip();
byte[] b = new byte[readBuffer.limit()];
int bufferReceiveIndex = 0;
while (readBuffer.hasRemaining()) {
b[bufferReceiveIndex++] = readBuffer.get();
}
System.out.println("received : " + new String(b));
}
} catch (Exception e) {
// ignore
}
}
@Test
public void tcpServer() {
try {
// 創建一個ServerSocketChannel通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 綁定6800端口
serverChannel.bind(new InetSocketAddress("192.168.5.14", 6800));
// 設置非阻塞
serverChannel.configureBlocking(false);
// Selector創建
Selector selector = Selector.open();
// 註冊 channel,並且指定感興趣的事件是 Accept
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(1024);
writeBuff.put("received".getBytes());
writeBuff.flip();
while (true) {
if (selector.select() > 0) {
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> readyKeyIterator = readyKeys.iterator();
while (readyKeyIterator.hasNext()) {
SelectionKey key = readyKeyIterator.next();
readyKeyIterator.remove();
if (key.isAcceptable()) {
// 連接
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 我們又給註冊到Selector裏面去了,聲明這個channel只對讀操作感興趣。
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 讀
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
socketChannel.read(readBuff);
readBuff.flip();
byte[] b = new byte[readBuff.limit()];
int bufferReceiveIndex = 0;
while (readBuff.hasRemaining()) {
b[bufferReceiveIndex++] = readBuff.get();
}
System.out.println("received : " + new String(b));
// 修改selector對channel感興趣的事件
key.interestOps(SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
// 寫
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuff);
// 修改selector對channel感興趣的事件
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
} catch (IOException e) {
// ignore
}
}
以上,就是我們對JAVA NIO編程的一個簡單介紹。最後我們用一個圖來做一個總結。