一 NIO介紹
1. NIO是非阻塞的
NIO非堵塞應用通常適用用在I/O讀寫等方面,我們知道,系統運行的性能瓶頸通常在I/O讀寫,包括對端口和文件的操作上,過去,在打開一個I/O通道後,read()將一直等待在端口一邊讀取字節內容,假如沒有內容進來,read()也是傻傻的等,這會影響我們程序繼續做其他事情,那麼改進做法就是開設線程,讓線程去等待,但是這樣做也是相當耗費資源的。
2. 實現原理
Java NIO非堵塞技術實際是採取Reactor模式,或者說是Observer模式爲我們監察I/O端口,假如有內容進來,會自動通知我們,這樣,我們就不必開啓多個線程死等。
Selector就是觀察者,觀察 Server 端的ServerSocketChannel 和 Client 端的 SocketChannel ;前提是它們需要先註冊到 同一個Selector,即觀察者中;
----詳細說明
NIO 有一個主要的類Selector,這個類似一個觀察者,只要我們把需要探知的socketchannel告訴Selector,我們接着做別的事情,當有事件發生時,他會通知我們,傳回一組SelectionKey,我們讀取這些Key,就會獲得我們剛剛註冊過的socketchannel,然後,我們從這個Channel中讀取數據,放心,包準能夠讀到,接着我們可以處理這些數據。Selector內部原理實際是在做一個對所註冊的Channel(SocketChannel)的輪詢訪問,不斷的輪詢(目前就這一個算法),一旦輪詢到一個channel有所註冊的事情發生,比如數據來了,它就會站起來報告,交出一把鑰匙,讓我們通過這把鑰匙來讀取這個channel的內容。
while(!stop) {
try {
selector.select(1000); // 等待客戶端發請求 1000ms
selector.selectedKeys().stream().forEach( key -> handleSelectionKey(key) );
}catch(Exception e) {
e.printStackTrace();
}
}
3. 小結
從外界看,實現了流暢的I/O讀寫,不堵塞了。Java NIO出現不只是一個技術性能的提高,你會發現網絡上到處在介紹它,因爲它具有里程碑意義,從JDK1.4開始,Java開始提高性能相關的功能,從而使得Java在底層或者並行分佈式計算等操作上已經可以和C或Perl等語言並駕齊驅。
下文部分轉載自 https://blog.csdn.net/robinjwong/article/details/41792623
二 Selector , SelectionKey , SocketChannel , ServerSocketChannel 具體應用和功能
NIO的通訊過程
1. Selector (選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否爲諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。
僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。
-
java.nio.channels
-
public abstract class Selector extends Object implements Closeable
1.1 Selector 的創建
通過調用Selector.open()方法創建一個Selector;Selector selector = Selector.open();
isOpen() —— 判斷Selector是否處於打開狀態。Selector對象創建後就處於打開狀態了。
close() —— 當調用了Selector對象的close()方法,就進入關閉狀態.。用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey實例無效。通道本身並不會關閉
1.2 ServerChanel 向 Selector 中註冊
爲了將Channel和Selector配合使用,必須將channel註冊到selector上。
通過SelectableChannel。register()方法來實現。
-
channel.configureBlocking(false);
-
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
與Selector一起使用時,Channel必須處於非阻塞模式下。這意味着FIleChannel與Selector不能一起使用。
注意register()方法的第二個參數,這是一個”interest集合“,意思是在通過Selector監聽Channel時對什麼事件感興趣。
可以監聽四種不同類型的事件:
- Connect
- Accept
- Read
- Write
通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱爲”連接就緒“。一個server socket channel準備號接收新進入的連接稱爲”接收就緒“。一個有數據可讀的通道可以說是”讀就緒“。等代寫數據的通道可以說是”寫就緒“。
這四種事件用SelectionKey的四個常量來表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
2. register()返回值 —— SelectionKey, Selector中的SelectionKey集合
只要ServerSocketChannel及SocketChannel向Selector註冊了特定的事件,Selector就會監控這些事件是否發生。
SelectableChannel的register()方法返回一個SelectionKey對象,該對象是用於跟蹤這些被註冊事件的句柄。
一個Selector對象會包含3種類型的SelectionKey集合:
- all-keys集合 —— 當前所有向Selector註冊的SelectionKey的集合,Selector的keys()方法返回該集合
- selected-keys集合 —— 相關事件已經被Selector捕獲的SelectionKey的集合,Selector的selectedKeys()方法返回該集合
- cancelled-keys集合 —— 已經被取消的SelectionKey的集合,Selector沒有提供訪問這種集合的方法
當register()方法執行時,新建一個SelectioKey,並把它加入Selector的all-keys集合中。
----selectionKey手動關閉 remove() 或cancel()
如果關閉了與SelectionKey對象關聯的Channel對象,或者調用了SelectionKey對象的cancel方法,這個SelectionKey對象就會被加入到cancelled-keys集合中,表示這個SelectionKey對象已經被取消。
在執行Selector的select()方法時,如果與SelectionKey相關的事件發生了,這個SelectionKey就被加入到selected-keys集合中,程序直接調用selected-keys集合的remove()方法,或者調用它的iterator的remove()方法,都可以從selected-keys集合中刪除一個SelectionKey對象。
3. SelectionKey——SelectableChannel 在 Selector 中的註冊的標記/句柄。
register()方法返回一個SelectinKey對象,這個對象包含一些你感興趣的屬性:
- interest集合
- ready集合
- Channel
- Selector
- 附加的對象
通過調用某個SelectionKey的cancel()方法,關閉其通道,或者通過關閉其選擇器來取消該Key之前,它一直保持有效。
取消某個Key之後不會立即從Selector中移除它,相反,會將該Key添加到Selector的已取消key set,以便在下一次進行選擇操作的時候移除它。
- interest集合 —— 感興趣的事件集合,可以通過SelectionKey讀寫interest集合,
-
int interestSet = selectionKey.interestOps();
-
boolean isInterestedInAccept = (interestSet & Selection.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
-
boolean isInterestedInConnect = interestSet & SelectioKey.OP_CONNECT;
-
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
-
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
- ready集合 —— 是通道已經準備就緒的操作的集合,在一個選擇後,你會是首先訪問這個ready set,
int readySet = selectionKey.readyOps();
可以向檢測interet集合那樣的方法,來檢測channel中什麼事件或操作已經就緒,也可以使用一下四個方法,
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
----Selector 內容
- 從SelectionKey中獲取Channel和Selector:
-
Channel channel = selectionKey.channel();
-
Selector selector = selectionKey.selector();
- 附加的對象 —— 可以將一個對象或者更多的信息附着到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的Buffer,或是包含聚集數據的某個對象,
-
selectionKey.attach(theObject);
-
Object attachedObj = selectionKey.attachment();
4. 通過Selector選擇就緒的通道
一旦向Selector註冊了一個或多個通道,就可以調用幾個重載的select()方法。
這些方法返回你所感興趣的事件(連接,接受,讀或寫)已經準備就緒的那些通道。換句話說,如果你對”讀就緒“的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。
- select() —— 阻塞到至少有一個通道在你註冊的事件上就緒了
- select(long timeout) —— 和select()一樣,除了最長會阻塞timeout毫秒
- selectNow() —— 不會阻塞,不管什麼通道就緒都立刻返回;此方法執行非阻塞的選擇操作,如果自從上一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回0
- select()方法返回的Int值表示多少通道就緒。
一旦調用了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過調用selector的selectorKeys()方法,訪問”已選擇鍵集“中的就緒通道,
Set selectedKeys = selector.selectedKeys();
可以遍歷這個已選擇的集合來訪問就緒的通道:
-
Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()){ SelectionKey key = keyIterator.next(); if (key.isAcceptable()){ // a connection was accepted by a ServerSocketChannel }else if (key.isConnectable()){ // a connection was eatablished with a remote server }else if (key.isReadable()){ // a channel is ready for reading }else if (key.isWritable()){ // a channel is ready for writing } keyIterator.remove(); }
這個循環遍歷已選擇集中的每個鍵,並檢測各個鍵所對象的通道的就緒事件。
注意每次迭代末尾的remove()調用,Selector不會自己從已選擇集中移除SelectioKey實例,必須在處理完通道時自己移除。
5. Selector的wakeUp()方法
某個線程調用select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其他線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。
-------個人實現的一個 案例代碼
import org.apache.commons.lang.StringUtils;
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.logging.Logger;
class MyTimeServer implements Runnable {
private static Logger logger = Logger.getLogger("MyTimeServer");
private int port;
private volatile boolean stop;
private Selector selector;
private ServerSocketChannel servChannel;
public MyTimeServer(int port) {
this.port = port;
}
private void startTimeServer(){
try{
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(this.port),1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
this.stop = false;
logger.info("TimeServer starts in port : " + port);
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stopTimeServer() {
this.stop = true;
try{
selector.close();
}catch (IOException e){
e.printStackTrace();
}
}
public void run() {
startTimeServer();
while(!stop) {
try {
selector.selectedKeys().stream().forEach( selectKey -> handleSelectionKey(selectKey) );
}catch(Exception e) {
e.printStackTrace();
}
}
//多路複用器關閉以後,所有註冊在上面的Channel和Pipe等資源都會自動去註冊並關閉,所以不需要重複釋放資源
if(selector != null) {
stopTimeServer();
}
}
private void handleSelectionKey(SelectionKey selectKey){
try{
handleRequest(selectKey);
if(selectKey != null) {
selectKey.cancel();
if(selectKey.channel() != null) {
selectKey.channel().close(); //處理client的key後 關閉該連接
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
private void handleRequest(SelectionKey selectKey) throws Exception {
if(selectKey.isValid()) {
if(selectKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector , SelectionKey.OP_READ);
}
if(selectKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel)selectKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024); //預分配1024
int readBytes = socketChannel.read(readBuffer);
if(readBytes > 0) {
byte[] readbytes = new byte[readBytes];
readBuffer.position(0); // 重置 byteBuffer 的位置 position 變量
readBuffer.get(readbytes , 0 , readBytes);
handleReadContext(socketChannel,new String(readbytes , "UTF-8"));
} else {
selectKey.cancel();
socketChannel.close();
}
}
}
}
private void handleReadContext(SocketChannel socketChannel , String requestBody) {
if(StringUtils.isNotBlank(requestBody) && requestBody.equals("time")) {
logger.info("The timeServer receive request : " + requestBody);
String response = "[success] msg : " + new Date(System.currentTimeMillis()).toString();
try {
reponse(socketChannel, response);
} catch (Exception e) {
e.printStackTrace();
}
}else {
try {
reponse(socketChannel, "[success] msg : nothing to say");
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void reponse(SocketChannel socketChannel, String response) throws Exception {
if(response != null && response.length() > 0) {
byte[] reponesBytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(reponesBytes.length);
writeBuffer.put(reponesBytes);
writeBuffer.flip();
socketChannel.write(writeBuffer);
writeBuffer.clear();
}
}
}
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.logging.Logger;
class MyTimeServer implements Runnable {
private static Logger logger = Logger.getLogger("MyTimeServer");
private int port;
private volatile boolean stop;
private Selector selector;
private ServerSocketChannel servChannel;
public MyTimeServer(int port) {
this.port = port;
}
private void startTimeServer(){
try{
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(this.port),1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
this.stop = false;
logger.info("TimeServer starts in port : " + port);
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stopTimeServer() {
this.stop = true;
try{
selector.close();
}catch (IOException e){
e.printStackTrace();
}
}
public void run() {
startTimeServer();
while(!stop) {
try {
selector.select(1000); // 等待客戶端發請求 1000ms
selector.selectedKeys().stream().forEach( key -> handleSelectionKey(key) );
}catch(Exception e) {
e.printStackTrace();
}
}
//多路複用器關閉以後,所有註冊在上面的Channel和Pipe等資源都會自動去註冊並關閉,所以不需要重複釋放資源
if(selector != null) {
stopTimeServer();
}
}
private void handleSelectionKey(SelectionKey key){
try{
handleRequest(key);
if(key != null) {
key.cancel();
if(key.channel() != null) {
key.channel().close(); //處理client的key後 關閉該連接
}
}
}catch(Exception e) {
e.printStackTrace();
}
}
private void handleRequest(SelectionKey key) throws Exception {
if(key.isValid()) {
if(key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector , SelectionKey.OP_READ);
}
if(key.isReadable()) {
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(readBuffer);
if(readBytes > 0) {
byte[] readbytes = new byte[readBytes];
readBuffer.position(0); // 重置 byteBuffer 的位置 position 變量
readBuffer.get(readbytes , 0 , readBytes);
String body = new String(readbytes, "UTF-8");
logger.info("The timeServer receive request : " + body);
String response = "[success] msg : " + new Date(System.currentTimeMillis()).toString();
reponse(socketChannel, response);
} else {
key.cancel();
socketChannel.close();
}
}
}
}
private void reponse(SocketChannel socketChannel, String response) throws Exception {
if(response != null && response.length() > 0) {
byte[] reponesBytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(reponesBytes.length);
writeBuffer.put(reponesBytes);
writeBuffer.flip();
socketChannel.write(writeBuffer);
writeBuffer.clear();
}
}
}