想要開發高性能的服務器,傳統的BIO顯然是不行的,Java提供了java.nio類庫來幫助我們實現這件事。關於NIO的文章網上有很多博客,但是相應的解釋圖則比較少。於是我便自己整理了幾張關係圖,便於理解。在看原理圖之前,我們還是需要先看下關於NIO的一些基礎概念。
一、什麼是NIO
NIO的全稱是non-block IO,也就是非阻塞IO。與傳統的BIO相對應。
Java IO 的各種流都是阻塞的,這意味着,當一個線程進行流處理(如read()和write())時,無論是否有數據,該線程會一直被阻塞,直到讀取到數據或者發生異常纔會返回。在此期間線程不能幹其他的事情,就算當前沒有數據,線程依然保持等待狀態。這樣無疑會浪費大量的資源。而在NIO的非阻塞模式下,線程發送數據與接收數據都是通過通道進行的,線程只需要去查看是否有數據要處理,如果沒有就直接返回,不會等待。
我們可以設想下這麼一個場景。在一家餐廳裏面,每來一位客戶老闆就分配一位服務員前去接待。期間服務員需要一直待在客戶身邊,處理客戶的各種請求,比如:點餐、加菜等等。一開始這麼做還行,後面隨着店的生意越來越好,服務員就開始不夠用了。這時解決方案有兩個,一個是再找些服務員進來,當這種方法顯然不現實,開銷太大。於是老闆就像出了另一種方法,他發現在客戶用餐的過程中,大部分客戶是沒有提需求的,服務員處於閒置狀態。於是他便安排了一位總管,這位總管負責查看所有客戶的狀態。一旦有某個客戶提出需求,他便安排一位空閒的服務員去處理。如此一來,老闆在沒有增加額外開銷的情況下便解決了問題。
在這個場景中,傳統的一個服務員服務一個客戶的方法就相當於BIO,而後來的改進就是NIO。
二、NIO的三個關鍵組件
1、Buffer
(1)定義:緩衝區。它本質上就是一個數組,不過它還提供了對數據的結構化訪問以及維護讀寫位置等信息。
(2)作用:負責與管道進行交互。在面向流的IO中我們可以直接把數據寫到Stream對象裏,而面向管道的IO則不行,進程需要先把要寫入的數據寫到緩衝區,再通過緩衝區把數據寫到管道對象裏。
(3)類別:最常用的是ByteBuffer,也就是字節緩衝區,其他的還有CharBuffer、ShortBuffer等。ByteBuffer除了具有一般緩衝的操作之外還提供了一些特有操作,方便網絡讀寫。
2、Channel
(1)定義:通道,可以通過它來讀出和寫入數據,與流的不同之處在於,通道是全雙工的,同時支持讀寫操作,而流只能在一個方向上移動。
(2)作用:用於在字節緩衝區和通道的另一側的實體(通常是一個文件或者套接字)進行進行有效的數據傳輸。
3、Selector
多路複用器,也叫選擇器。它會不斷地輪詢註冊在它上面的管道Channel,並且返回那些準備就緒的管道,以便進行後續的IO處理。Selector就相當於我們前面所舉例子中的總管。而總管查看客戶狀態的方式有兩種,一種是輪詢,也就是總管每隔一段時間就挨個去問,你有沒有什麼需要我幫助的。另一種則是總管就站在中間不懂,由客戶主動告知總管,我有什麼請求。這篇博客用的是輪詢的方法。
三、NIO原理
1、概念解釋
(1)serversocketchannel和socketchannel的區別:
ServerSocketChannel和SocketChannel是一對,它們是java.nio下面實現通信的類。服務器必須先建立ServerSocketChannel來等待客戶端的連接。客戶端則必須建立相對應的SocketChannel來與服務器建立連接,服務器接收到客戶端的連接後,創建一個新的SocketChannel並通過ServerSocketChannel.accept()方法和Client端的SocketChannel建立連接,之後雙方就可以進行通信了。也就是服務器端的每一個SocketChannel都唯一標識了一個客戶端。
2、原理圖
根據原理圖我們來梳理一下Java中NIO通信的過程:
(1)、服務器打開了ServerSocketChannel,並綁定端口號。
(2)、打開Selector多路複用器,同時將ServerSocketChannel註冊到Selector模塊上,並指明我們感興趣的事件是OP_ACCEPT。
(3)、開啓Selector,Selector將會每隔一段時間輪詢註冊在它上面的SocketChannel是否有用戶感興趣的事件發生。目前Selector上面只有一個ServerSocketChannel。
(4)、當有用戶想要和服務器建立連接時,ServerSocketChannel的標誌位被置爲OP_ACCPET。Selector將該SocketChannel返回給服務器進程。服務器創建一個SocketChannel和該客戶端的SocketChannel建立連接。
(5)、服務器將創建的SocketChannel註冊到Selector上,並指明感興趣的事件是OP_READ。也就是當這個管道中數據可讀時再來通知我。
(6)、現在Selector上面一共監聽了兩個SocketChannel,一個是ServerSocketChannel,用來處理客戶端連接;一個是SocketChannel,用來監聽某個客戶端是否有數據發過來。後面每當有新的客戶端嘗試連接服務器時,服務器就會重複第四和第五步操作。
注意點:這裏的Buffer不是所有的Client共享一個,而是每次處理一個事件都新建一個Buffer。
四、代碼實現
TimeServer主類
package nioserver;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try{
port = Integer.valueOf(args[0]);
}catch(NumberFormatException e){
}
}
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
}
}
NIO類
package nioserver;
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.Iterator;
import java.util.Set;
public class MultiplexerTimeServer implements Runnable{
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
public MultiplexerTimeServer(int port){
try {
//1、打開ServerSocketChannel,用於監聽客戶端連接,是所有客戶端連接的父通道
servChannel = ServerSocketChannel.open();
//2、綁定監聽端口號,設置連接爲非阻塞IO
servChannel.configureBlocking(false);
//這裏的1024是請求傳入連接隊列的最大長度
servChannel.socket().bind(new InetSocketAddress(port),1024);
//3、創建選擇器
selector = Selector.open();
//4、將管道註冊到Selector上,監聽accept事件
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
this.stop = true;
}
public void run() {
while(!stop){
try {
//其中的1000爲休眠時間,Selector每隔1s都被喚醒一次
selector.select(1000);
//5、Selector輪詢註冊在它上面的所有SocketChannel,並返回所有有服務器感興趣事件發生的SocketChannel
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
//6、服務器迭代處理所有需要處理的SocketChannel
while(it.hasNext()){
key = it.next();
//移除出未處理的隊列
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null)
key.channel().close();
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//多路複用器關閉後,所有註冊在上面的Channel和Pipe等資源都會被自動去
//註冊和關閉,所以不需要重複關閉資源
if(selector != null){
try {
selector.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
//判斷key值是否有效
if(key.isValid()){
//6、Selector監聽到有新的客戶端接入,處理新接入的連接請求
if(key.isAcceptable()){
/*
* 通過ServerSocketChannel的accept接收客戶端的連接請求並創建SocketChannel實例
* 完成上述操作之後相當於完成了TCP的三次握手,TCP物理鏈路正式建立
* 我們將SocketChannel設置爲異步非阻塞
* 同時也可以對其TCP參數進行設置,例如TCP發送和接收緩存區的大小等
*/
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
//設置客戶端鏈路爲非阻塞模式
sc.configureBlocking(false);
//8、將新接入的SocketChannel註冊到Selector上,監聽讀操作
sc.register(selector,SelectionKey.OP_READ);
String welcome = "Welcome,Please input your order:\n";
doWrite(sc,welcome);
}
//9、監聽到註冊的SocketChannel有可讀事件發生,進行處理
if(key.isReadable()){
/*
* 讀取客戶端的請求消息
* 我們無法得知客戶端發送的碼流大小,作爲例程,我們開闢一個1k的緩衝區
* 然後調用read方法讀取請求碼流
*/
//讀取數據
SocketChannel sc = (SocketChannel) key.channel();
//10、分配一個新的緩存空間,大小爲1024,異步讀取客戶端的消息
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
/*
* read()方法的三種返回值
* 返回值大於0:讀到了直接,對字節進行編解碼
* 返回值等於0:沒有讀到字節,屬於正常場景,忽略
* 返回值爲-1:鏈路已經關閉,需要關閉SocketChannel釋放資源
*/
if(readBytes > 0){
readBuffer.flip();
//開闢一個空間,大小爲緩存區中還剩餘的字節數
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("The time server receive order : " + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(sc,currentTime);
}else if(readBytes < 0){
//對端鏈路關閉
key.cancel();
sc.close();
}else
;//讀到0字節忽略
}
}
}
private void doWrite(SocketChannel channel, String response) throws IOException {
//如果接受到消息不爲空,並且不是空白行
//strim方法可用於從字符串的開始和結束處修剪空白(如上所定義)。
if(response != null && response.trim().length() > 0){
byte[] bytes = response.getBytes();
//9、將消息異步發送給客戶端
//分配寫空間緩存區
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//把bytes放入到寫緩存中
writeBuffer.put(bytes);
/*
* 翻轉這個緩衝區,將limit設爲當前位置,
* 當使用完put()方法時,position位於數據的末尾,我們需要把它移動到0
* 這樣調用get操作時我們才能把緩存區中的字節數組複製到新創建的直接數組中
*/
writeBuffer.flip();
//調用write方法將緩存區中的字節數組發送出去
//需要處理寫半包的場景
channel.write(writeBuffer);
}
}
}
五、性能測試
從壓測的結果我們可以看出,併發在2.5w的時候是沒有任何異常的。我嘗試再網上增的話就開始出現異常了。
說明:本文代碼部分主要來自《netty權威指南一書》
C10k系列文章: