BIO:同步阻塞式IO,服務器實現模式爲一個鏈接一個線程,即客戶端有鏈接請求時服務器端就須要啓動一個線程進行處理,若是這個鏈接不作任何事情會形成沒必要要的線程開銷,固然能夠經過線程池機制改善。
NIO:同步非阻塞式IO,服務器實現模式爲一個請求一個線程,即客戶端發送的鏈接請求都會註冊到多路複用器上,多路複用器輪詢到鏈接有I/O請求時才啓動一個線程進行處理。
AIO(NIO.2):異步非阻塞式IO,服務器實現模式爲一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啓動線程進行處理。 java
同步阻塞式IO,相信每個學習過操做系統網絡編程或者任何語言的網絡編程的人都很熟悉,在while循環中服務端會調用accept方法等待接收客戶端的鏈接請求,一旦接收到一個鏈接請求,就能夠創建通訊套接字在這個通訊套接字上進行讀寫操做,此時不能再接收其餘客戶端鏈接請求,只能等待同當前鏈接的客戶端的操做執行完成。
若是BIO要可以同時處理多個客戶端請求,就必須使用多線程,即每次accept阻塞等待來自客戶端請求,一旦受到鏈接請求就創建通訊套接字同時開啓一個新的線程來處理這個套接字的數據讀寫請求,而後馬上又繼續accept等待其餘客戶端鏈接請求,即爲每個客戶端鏈接請求都建立一個線程來單獨處理,大概原理圖就像這樣: 編程
雖然此時服務器具有了高併發能力,即可以同時處理多個客戶端請求了,可是卻帶來了一個問題,隨着開啓的線程數目增多,將會消耗過多的內存資源,致使服務器變慢甚至崩潰,NIO能夠必定程度解決這個問題。bootstrap
同步非阻塞式IO,關鍵是採用了事件驅動的思想來實現了一個多路轉換器。
NIO與BIO最大的區別就是隻須要開啓一個線程就能夠處理來自多個客戶端的IO事件,這是怎麼作到的呢?
就是多路複用器,能夠監聽來自多個客戶端的IO事件:
A. 若服務端監聽到客戶端鏈接請求,便爲其創建通訊套接字(java中就是通道),而後返回繼續監聽,若同時有多個客戶端鏈接請求到來也能夠所有收到,依次爲它們都創建通訊套接字。
B. 若服務端監聽到來自已經建立了通訊套接字的客戶端發送來的數據,就會調用對應接口處理接收到的數據,若同時有多個客戶端發來數據也能夠依次進行處理。
C. 監聽多個客戶端的鏈接請求和接收數據請求同時還能監聽本身時候有數據要發送。 api
總之就是在一個線程中就能夠調用多路複用接口(java中是select)阻塞同時監聽來自多個客戶端的IO請求,一旦有收到IO請求就調用對應函數處理。 數組
異步IO:安全
異步 I/O 是一種沒有阻塞地讀寫數據的方法。一般,在代碼進行 read() 調用時,代碼會阻塞直至有可供讀取的數據。一樣, write()調用將會阻塞直至數據可以寫入。服務器
異步 I/O 的一個優點在於,它容許您同時根據大量的輸入和輸出執行 I/O。同步程序經常要求助於輪詢,或者建立許許多多的線程以處理大量的鏈接。使用異步 I/O,您能夠監放任何數量的通道上的事件,不用輪詢,也不用額外的線程。網絡
Buffer和Channel是標準NIO中的核心對象,幾乎每個IO操做中都會用到它們。多線程
Channel是對原IO中流的模擬,任何來源和目的數據都必須經過一個Channel對象。一個Buffer實質上是一個容器對象,發給Channel的全部對象都必須先放到Buffer中;一樣的,從Channel中讀取的任何數據都要讀到Buffer中。架構
Buffer是一個對象,它包含一些要寫入或讀出的數據。在NIO中,數據是放入buffer對象的,而在IO中,數據是直接寫入或者讀到Stream對象的。應用程序不能直接對 Channel 進行讀寫操做,而必須經過 Buffer 來進行,即 Channel 是經過 Buffer 來讀寫數據的。
在NIO中,全部的數據都是用Buffer處理的,它是NIO讀寫數據的中轉池。Buffer實質上是一個數組,一般是一個字節數據,但也能夠是其餘類型的數組。但一個緩衝區不只僅是一個數組,重要的是它提供了對數據的結構化訪問,並且還能夠跟蹤系統的讀寫進程。
使用 Buffer 讀寫數據通常遵循如下四個步驟:
當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,須要經過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到 Buffer 的全部數據。
當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,須要經過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到 Buffer 的全部數據。
Buffer主要幾種:
Channel是一個對象,能夠經過它讀取和寫入數據。能夠把它看作IO中的流。可是它和流相比還有一些不一樣:
正如上面提到的,全部數據都經過Buffer對象處理,因此,您永遠不會將字節直接寫入到Channel中,相反,您是將數據寫入到Buffer中;一樣,您也不會從Channel中讀取字節,而是將數據從Channel讀入Buffer,再從Buffer獲取這個字節。
由於Channel是雙向的,因此Channel能夠比流更好地反映出底層操做系統的真實狀況。特別是在Unix模型中,底層操做系統一般都是雙向的。
在Java NIO中Channel主要有以下幾種類型:
IO中的讀和寫,對應的是數據和Stream,NIO中的讀和寫,則對應的就是通道和緩衝區。NIO中從通道中讀取:建立一個緩衝區,而後讓通道讀取數據到緩衝區。NIO寫入數據到通道:建立一個緩衝區,用數據填充它,而後讓通道用這些數據來執行寫入。
咱們已經知道,在NIO系統中,任什麼時候候執行一個讀操做,您都是從Channel中讀取,而您不是直接從Channel中讀取數據,由於全部的數據都必須用Buffer來封裝,因此您應該是從Channel讀取數據到Buffer。
所以,若是從文件讀取數據的話,須要以下三步:
下面咱們看一下具體過程:
第一步:獲取通道
FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();
第二步:建立緩衝區
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
第三步:將數據從通道讀到緩衝區
fc.read( buffer );
第一步:獲取一個通道
FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();
第二步:建立緩衝區,將數據放入緩衝區
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
第三步:把緩衝區數據寫入通道中
fc.write( buffer );
CopyFile是一個很是好的讀寫結合的例子,咱們將經過CopyFile這個實力讓你們體會NIO的操做過程。CopyFile執行三個基本的操做:建立一個Buffer,而後從源文件讀取數據到緩衝區,而後再將緩衝區寫入目標文件。
/**
* 用java NIO api拷貝文件
* @param src
* @param dst
* @throws IOException
*/
public static void copyFileUseNIO(String src,String dst) throws IOException{
//聲明源文件和目標文件
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
//得到傳輸通道channel
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
//得到容器buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
//判斷是否讀完文件
int eof =inChannel.read(buffer);
if(eof==-1){
break;
}
//重設一下buffer的position=0,limit=position
buffer.flip();
//開始寫
outChannel.write(buffer);
//寫完要重置buffer,重設position=0,limit=capacity
buffer.clear();
}
inChannel.close();
outChannel.close();
fi.close();
fo.close();
}
Selector是一個對象,它能夠註冊到不少個Channel上,監聽各個Channel上發生的事件,而且可以根據事件狀況決定Channel讀寫。這樣,經過一個線程管理多個Channel,就能夠處理大量網絡鏈接了。
有了Selector,咱們就能夠利用一個線程來處理全部的channel。線程之間的切換對操做系統來講代價是很高的,而且每一個線程也會佔用必定的系統資源。因此,對系統來講使用的線程越少越好。
可是,須要記住,現代的操做系統和CPU在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個CPU有多個內核,不使用多任務多是在浪費CPU能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用Selector可以處理多個通道就足夠了。
下面這幅圖展現了一個線程處理3個 Channel的狀況:
如何建立一個Selector:
異步 I/O 中的核心對象名爲 Selector。Selector 就是您註冊對各類 I/O 事件興趣的地方,並且當那些事件發生時,就是這個對象告訴您所發生的事件。
Selector selector = Selector.open();
而後,就須要註冊Channel到Selector了
如何註冊Channel到Selector:
爲了能讓Channel和Selector配合使用,咱們須要把Channel註冊到Selector上。經過調用 channel.register()方法來實現註冊:
channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
注意,註冊的Channel 必須設置成異步模式 才能夠,,不然異步IO就沒法工做,這就意味着咱們不能把一個FileChannel註冊到Selector,由於FileChannel沒有異步模式,可是網絡編程中的SocketChannel是能夠的。
須要注意register()方法的第二個參數,它是一個「interest set」,意思是註冊的Selector對Channel中的哪些時間感興趣,事件類型有四種:
通道觸發了一個事件意思是該事件已經 Ready(就緒)。因此,某個Channel成功鏈接到另外一個服務器稱爲 Connect Ready。一個ServerSocketChannel準備好接收新鏈接稱爲 Accept Ready,一個有數據可讀的通道能夠說是 Read Ready,等待寫數據的通道能夠說是Write Ready。
上面這四個事件對應到SelectionKey中的四個常量:
1. SelectionKey.OP_CONNECT
2. SelectionKey.OP_ACCEPT
3. SelectionKey.OP_READ
4. SelectionKey.OP_WRITE
若是你對多個事件感興趣,能夠經過or操做符來鏈接這些常量:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
關於SelectionKey
請注意對register()的調用的返回值是一個SelectionKey。 SelectionKey 表明這個通道在此 Selector 上的這個註冊。當某個 Selector 通知您某個傳入事件時,它是經過提供對應於該事件的 SelectionKey 來進行的。SelectionKey 還能夠用於取消通道的註冊。SelectionKey中包含以下屬性:
Interest Set
就像咱們在前面講到的把Channel註冊到Selector來監聽感興趣的事件,interest set就是你要選擇的感興趣的事件的集合。你能夠經過SelectionKey對象來讀寫interest set:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
經過上面例子能夠看到,咱們能夠經過用AND 和SelectionKey 中的常量作運算,從SelectionKey中找到咱們感興趣的事件。
Ready Set
ready set 是通道已經準備就緒的操做的集合。在一次選Selection以後,你應該會首先訪問這個ready set。Selection將在下一小節進行解釋。能夠這樣訪問ready集合:
int readySet = selectionKey.readyOps();
能夠用像檢測interest集合那樣的方法,來檢測Channel中什麼事件或操做已經就緒。可是,也可使用如下四個方法,它們都會返回一個布爾類型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel 和Selector
咱們能夠經過SelectionKey得到Selector和註冊的Channel:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attach 一個對象
能夠將一個對象或者更多信息attach 到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,能夠附加 與通道一塊兒使用的Buffer,或是包含彙集數據的某個對象。使用方法以下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
Attach 一個對象
能夠將一個對象或者更多信息attach 到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,能夠附加 與通道一塊兒使用的Buffer,或是包含彙集數據的某個對象。使用方法以下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
還能夠在用register()方法向Selector註冊Channel的時候附加對象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
經過Selector選擇通道
一旦向Selector註冊了一或多個通道,就能夠調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫)已經準備就緒的那些通道。換句話說,若是你對「Read Ready」的通道感興趣,select()方法會返回讀事件已經就緒的那些通道:
select()方法返回的int值表示有多少通道已經就緒。亦即,自上次調用select()方法後有多少通道變成就緒狀態。若是調用select()方法,由於有一個通道變成就緒狀態,返回了1,若再次調用select()方法,若是另外一個通道就緒了,它會再次返回1。若是對第一個就緒的channel沒有作任何操做,如今就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道處於就緒狀態。
selectedKeys()
一旦調用了select()方法,它就會返回一個數值,表示一個或多個通道已經就緒,而後你就能夠經過調用selector.selectedKeys()方法返回的SelectionKey集合來得到就緒的Channel。請看演示方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
當你經過Selector註冊一個Channel時,channel.register()方法會返回一個SelectionKey對象,這個對象就表明了你註冊的Channel。這些對象能夠經過selectedKeys()方法得到。你能夠經過迭代這些selected key來得到就緒的Channel,下面是演示代碼:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> 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 established 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();
}
這個循環遍歷selected key的集合中的每一個key,並對每一個key作測試來判斷哪一個Channel已經就緒。
請注意循環中最後的keyIterator.remove()方法。Selector對象並不會從本身的selected key集合中自動移除SelectionKey實例。咱們須要在處理完一個Channel的時候本身去移除。當下一次Channel就緒的時候,Selector會再次把它添加到selected key集合中。
SelectionKey.channel()方法返回的Channel須要轉換成你具體要處理的類型,好比是ServerSocketChannel或者SocketChannel等等。
一個完整的例子:
public class MultiPortEcho {
private int ports[];
private ByteBuffer echoBuffer = ByteBuffer.allocate(1024);
public MultiPortEcho(int ports[]) throws IOException {
this.ports = ports;
go();
}
private void go() throws IOException {
// 1. 建立一個selector,select是NIO中的核心對象
// 它用來監聽各類感興趣的IO事件
Selector selector = Selector.open();
// 爲每一個端口打開一個監聽, 並把這些監聽註冊到selector中
for (int i = 0; i < ports.length; ++i) {
//2. 打開一個ServerSocketChannel
//其實咱們沒監聽一個端口就須要一個channel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//設置爲非阻塞
ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress(ports[i]);
ss.bind(address);//監聽一個端口
//3. 註冊到selector
//register的第一個參數永遠都是selector
//第二個參數是咱們要監聽的事件
//OP_ACCEPT是新創建鏈接的事件
//也是適用於ServerSocketChannel的惟一事件類型
SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Going to listen on " + ports[i]);
}
//4. 開始循環,咱們已經註冊了一些IO興趣事件
while (true) {
//這個方法會阻塞,直到至少有一個已註冊的事件發生。當一個或者更多的事件發生時
// select() 方法將返回所發生的事件的數量。
int num = selector.select();
//返回發生了事件的 SelectionKey 對象的一個 集合
Set selectedKeys = selector.selectedKeys();
//咱們經過迭代 SelectionKeys 並依次處理每一個 SelectionKey 來處理事件
//對於每個 SelectionKey,您必須肯定發生的是什麼 I/O 事件,以及這個事件影響哪些 I/O 對象。
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
//5. 監聽新鏈接。程序執行到這裏,咱們僅註冊了 ServerSocketChannel
//而且僅註冊它們「接收」事件。爲確認這一點
//咱們對 SelectionKey 調用 readyOps() 方法,並檢查發生了什麼類型的事件
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
//6. 接收了一個新鏈接。由於咱們知道這個服務器套接字上有一個傳入鏈接在等待
//因此能夠安全地接受它;也就是說,不用擔憂 accept() 操做會阻塞
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 7. 講新鏈接註冊到selector。將新鏈接的 SocketChannel 配置爲非阻塞的
//並且因爲接受這個鏈接的目的是爲了讀取來自套接字的數據,因此咱們還必須將 SocketChannel 註冊到 Selector上
SelectionKey newKey = sc.register(selector,SelectionKey.OP_READ);
it.remove();
System.out.println("Got connection from " + sc);
} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
// Read the data
SocketChannel sc = (SocketChannel) key.channel();
// Echo data
int bytesEchoed = 0;
while (true) {
echoBuffer.clear();
int r = sc.read(echoBuffer);
if (r <= 0) {
break;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed += r;
}
System.out.println("Echoed " + bytesEchoed + " from " + sc);
it.remove();
}
}
// System.out.println( "going to clear" );
// selectedKeys.clear();
// System.out.println( "cleared" );
}
}
static public void main(String args2[]) throws Exception {
String args[]={"9001","9002","9003"};
if (args.length <= 0) {
System.err.println("Usage: java MultiPortEcho port [port port ...]");
System.exit(1);
}
int ports[] = new int[args.length];
for (int i = 0; i < args.length; ++i) {
ports[i] = Integer.parseInt(args[i]);
}
new MultiPortEcho(ports);
}
}
一個完整的業務可能會被TCP拆分紅多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這個就是TCP的拆包和封包問題。
1. 第一種狀況,Data1和Data2都分開發送到了Server端,沒有產生粘包和拆包的狀況。
2. 第二種狀況,Data1和Data2數據粘在了一塊兒,打成了一個大的包發送到Server端,這個狀況就是粘包。
3. 第三種狀況,Data2被分離成Data2_1和Data2_2,而且Data2_1在Data1以前到達了服務端,這種狀況就產生了拆包。
因爲網絡的複雜性,可能數據會被分離成N多個複雜的拆包/粘包的狀況,因此在作TCP服務器的時候就須要首先解決拆包/粘包的問題。
1. 應用程序寫入數據的字節大小大於套接字發送緩衝區的大小
2. 進行MSS大小的TCP分段。MSS是最大報文段長度的縮寫。MSS是TCP報文段中的數據字段的最大長度。數據字段加上TCP首部纔等於整個的TCP報文段。因此MSS並非TCP報文段的最大長度,而是:MSS=TCP報文段長度-TCP首部長度
3. 以太網的payload大於MTU進行IP分片。MTU指:一種通訊協議的某一層上面所能經過的最大數據包大小。若是IP層有一個數據包要傳,並且數據的長度比鏈路層的MTU大,那麼IP層就會進行分片,把數據包分紅若干片,讓每一片都不超過MTU。注意,IP分片能夠發生在原始發送端主機上,也能夠發生在中間路由器上。
1. 消息定長。例如100字節。
2. 在包尾部增長回車或者空格符等特殊字符進行分割,典型的如FTP協議
3. 將消息分爲消息頭和消息尾。
4. 其它複雜的協議,如RTMP協議等。
Netty是由JBOSS提供的一個java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。
也就是說,Netty 是一個基於NIO的客戶、服務器端編程框架,使用Netty 能夠確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。Netty至關簡化和流線化了網絡應用的編程開發過程,例如,TCP和UDP的socket服務開發。
「快速」和「簡單」並不用產生維護性或性能上的問題。Netty 是一個吸取了多種協議的實現經驗,這些協議包括FTP,SMTP,HTTP,各類二進制,文本協議,並通過至關精心設計的項目,最終,Netty 成功的找到了一種方式,在保證易於開發的同時還保證了其應用的性能,穩定性和伸縮性。
爲了更好的理解和進一步深刻Netty,咱們先整體認識一下Netty用到的組件及它們在整個Netty架構中是怎麼協調工做的。Netty的10個核心類:
· Bootstrap or ServerBootstrap
· EventLoop
· EventLoopGroup
· ChannelPipeline
· Channel
· Future or ChannelFuture
· ChannelInitializer
· ChannelHandler
一個Netty程序開始於Bootstrap類,Bootstrap類是Netty提供的一個能夠經過簡單配置來設置或"引導"程序的一個很重要的類。Netty中設計了Handlers來處理特定的"event"和設置Netty中的事件,從而來處理多個協議和數據。事件能夠描述成一個很是通用的方法,由於你能夠自定義一個handler,用來將Object轉成byte[]或將byte[]轉成Object;也能夠定義個handler處理拋出的異常。
你會常常編寫一個實現ChannelInboundHandler的類,ChannelInboundHandler是用來接收消息,當有消息過來時,你能夠決定如何處理。當程序須要返回消息時能夠在ChannelInboundHandler裏write/flush數據。能夠認爲應用程序的業務邏輯都是在ChannelInboundHandler中來處理的,業務羅的生命週期在ChannelInboundHandler中。
Netty鏈接客戶端端或綁定服務器須要知道如何發送或接收消息,這是經過不一樣類型的handlers來作的,多個Handlers是怎麼配置的?Netty提供了ChannelInitializer類用來配置Handlers。ChannelInitializer是經過ChannelPipeline來添加ChannelHandler的,如發送和接收消息,這些Handlers將肯定發的是什麼消息。ChannelInitializer自身也是一個ChannelHandler,在添加完其餘的handlers以後會自動從ChannelPipeline中刪除本身。
全部的Netty程序都是基於ChannelPipeline。ChannelPipeline和EventLoop和EventLoopGroup密切相關,由於它們三個都和事件處理相關,因此這就是爲何它們處理IO的工做由EventLoop管理的緣由。
Netty中全部的IO操做都是異步執行的,例如你鏈接一個主機默認是異步完成的;寫入/發送消息也是一樣是異步。也就是說操做不會直接執行,而是會等一會執行,由於你不知道返回的操做結果是成功仍是失敗,可是須要有檢查是否成功的方法或者是註冊監聽來通知;Netty使用Futures和ChannelFutures來達到這種目的。Future註冊一個監聽,當操做成功或失敗時會通知。ChannelFuture封裝的是一個操做的相關信息,操做被執行時會馬上返回ChannelFuture。
下圖顯示一個EventLoopGroup和一個Channel關聯一個單一的EventLoop,Netty中的EventLoopGroup包含一個或多個EventLoop,而EventLoop就是一個Channel執行實際工做的線程。EventLoop老是綁定一個單一的線程,在其生命週期內不會改變。
當註冊一個Channel後,Netty將這個Channel綁定到一個EventLoop,在Channel的生命週期內老是被綁定到一個EventLoop。在Netty IO操做中,你的程序不須要同步,由於一個指定通道的全部IO始終由同一個線程來執行。
EventLoop和EventLoopGroup的關聯不是直觀的,由於咱們說過EventLoopGroup包含一個或多個EventLoop,可是上面的圖顯示EventLoop是一個EventLoopGroup,這意味着你能夠只使用一個特定的EventLoop。
「引導」是Netty中配置程序的過程,當你須要鏈接客戶端或服務器綁定指定端口時須要使用bootstrap。如前面所述,「引導」有兩種類型,一種是用於客戶端的Bootstrap(也適用於DatagramChannel),一種是用於服務端的ServerBootstrap。
兩種bootstraps之間有一些類似之處,其實他們有不少類似之處,也有一些不一樣。Bootstrap和ServerBootstrap之間的差別:
· Bootstrap用來鏈接遠程主機,有1個EventLoopGroup
· ServerBootstrap用來綁定本地端口,有2個EventLoopGroup
第一個差別
「ServerBootstrap」監聽在服務器監聽一個端口輪詢客戶端的「Bootstrap」或DatagramChannel是否鏈接服務器。一般須要調用「Bootstrap」類的connect()方法,可是也能夠先調用bind()再調用connect()進行鏈接,以後使用的Channel包含在bind()返回的ChannelFuture中。
第二個差別
客戶端bootstraps/applications使用一個單例EventLoopGroup,而ServerBootstrap使用2個EventLoopGroup(實際上使用的是相同的實例),它可能不是顯而易見的,可是它是個好的方案。一個ServerBootstrap能夠認爲有2個channels組,第一組包含一個單例ServerChannel,表明持有一個綁定了本地端口的socket;第二組包含全部的Channel,表明服務器已接受了的鏈接。例以下圖這個狀況:
上圖中,EventLoopGroup A惟一的目的就是接受鏈接而後交給EventLoopGroup B。Netty可使用兩個不一樣的Group,由於服務器程序須要接受不少客戶端鏈接的狀況下,一個EventLoopGroup將是程序性能的瓶頸,由於事件循環忙於處理鏈接請求,沒有多餘的資源和空閒來處理業務邏輯,最後的結果會是不少鏈接請求超時。如有兩EventLoops, 即便在高負載下,全部的鏈接也都會被接受,由於EventLoops接受鏈接不會和哪些已經鏈接了的處理共享資源。
EventLoopGroup和EventLoop是什麼關係?EventLoopGroup能夠包含不少個EventLoop,每一個Channel綁定一個EventLoop不會被改變,由於EventLoopGroup包含少許的EventLoop的Channels,不少Channel會共享同一個EventLoop。這意味着在一個Channel保持EventLoop繁忙會禁止其餘Channel綁定到相同的EventLoop。咱們能夠理解爲EventLoop是一個事件循環線程,而EventLoopGroup是一個事件循環集合。
要明白Netty程序wirte或read時發生了什麼,首先要對Handler是什麼有
必定的瞭解。Handlers自身依賴於ChannelPipeline來決定它們執行的順序,所以不可能經過ChannelPipeline定義處理程序的某些方面,反過來不可能定義也不可能經過ChannelHandler定義ChannelPipeline的某些方面。不必說咱們必須定義一個本身和其餘的規定。本節將介紹ChannelHandler和ChannelPipeline在某種程度上細微的依賴。
在不少地方,Netty的ChannelHandler是你的應用程序中處理最多的。即便你沒有意思到這一點,若果你使用Netty應用將至少有一個ChannelHandler參與,換句話說,ChannelHandler對不少事情是關鍵的。咱們能夠理解爲ChannelHandler是一段執行業務邏輯處理數據的代碼,它們來來每每的經過ChannelPipeline。實際上,ChannelHandler是定義一個handler的父接口,ChannelInboundHandler和ChannelOutboundHandler都實現ChannelHandler接口,以下圖:
爲了使數據從一端到達另外一端,一個或多個ChannelHandler將以某種方式操做數據。這些ChannelHandler會在程序的「引導」階段被添加ChannelPipeline中,而且被添加的順序將決定處理數據的順序。ChannelPipeline的做用咱們能夠理解爲用來管理ChannelHandler的一個容器,每一個ChannelHandler處理各自的數據(例如入站數據只能由ChannelInboundHandler處理),處理完成後將轉換的數據放到ChannelPipeline中交給下一個ChannelHandler繼續處理,直到最後一個ChannelHandler處理完成。
如前面所說,有不少不一樣類型的handlers,每一個handler的依賴於它們的基類。Netty提供了一系列的「Adapter」類,這讓事情變的很簡單。每一個handler負責轉發時間到ChannelPipeline的下一個handler。在*Adapter類(和子類)中是自動完成的,所以咱們只須要在感興趣的*Adapter中重寫方法。這些功能能夠幫助咱們很是簡單的編碼/解碼消息。有幾個適配器(adapter)容許自定義ChannelHandler,通常自定義ChannelHandler須要繼承編碼/解碼適配器類中的一個。Netty有一下適配器:
ChannelHandlerAdapter
ChannelInboundHandlerAdapter
ChannelOutboundHandlerAdapter
三個ChannelHandler漲,咱們重點看看ecoders,decoders和SimpleChannelInboundHandler<I>,SimpleChannelInboundHandler<I>繼承ChannelInboundHandlerAdapter。
由於咱們在網絡傳輸時只能傳輸字節流,所以,才發送數據以前,咱們必須把咱們的message型轉換爲bytes,與之對應,咱們在接收數據後,必須把接收到的bytes再轉換成message。咱們把bytes to message這個過程稱做Decode(解碼成咱們能夠理解的),把message to bytes這個過程成爲Encode。
Netty中提供了不少現成的編碼/解碼器,咱們通常從他們的名字中即可知道他們的用途,如ByteToMessageDecoder、MessageToByteEncoder,如專門用來處理Google Protobuf協議的ProtobufEncoder、 ProtobufDecoder。
咱們前面說過,具體是哪一種Handler就要看它們繼承的是InboundAdapter仍是OutboundAdapter,對於Decoders,很容易即可以知道它是繼承自ChannelInboundHandlerAdapter或 ChannelInboundHandler,由於解碼的意思是把ChannelPipeline傳入的bytes解碼成咱們能夠理解的message(即Java Object),而ChannelInboundHandler正是處理Inbound Event,而Inbound Event中傳入的正是字節流。Decoder會覆蓋其中的「ChannelRead()」方法,在這個方法中來調用具體的decode方法解碼傳遞過來的字節流,而後經過調用ChannelHandlerContext.fireChannelRead(decodedMessage)方法把編碼好的Message傳遞給下一個Handler。與之相似,Encoder就沒必要多少了。
Handler,爲了支持各類協議和處理數據的方式,便誕生了Handler組件。Handler主要用來處理各類事件,這裏的事件很普遍,好比能夠是鏈接、數據接收、異常、數據轉換等。
ChannelInboundHandler,一個最經常使用的Handler。這個Handler的做用就是處理接收到數據時的事件,也就是說,咱們的業務邏輯通常就是寫在這個Handler裏面的,ChannelInboundHandler就是用來處理咱們的核心業務邏輯。
ChannelInitializer,當一個連接創建時,咱們須要知道怎麼來接收或者發送數據,固然,咱們有各類各樣的Handler實現來處理它,那麼ChannelInitializer即是用來配置這些Handler,它會提供一個ChannelPipeline,並把Handler加入到ChannelPipeline。
ChannelPipeline,一個Netty應用基於ChannelPipeline機制,這種機制須要依賴於EventLoop和EventLoopGroup,由於它們三個都和事件或者事件處理相關。
EventLoops的目的是爲Channel處理IO操做,一個EventLoop能夠爲多個Channel服務。
EventLoopGroup會包含多個EventLoop。
Channel表明了一個Socket連接,或者其它和IO操做相關的組件,它和EventLoop一塊兒用來參與IO處理。
Future,在Netty中全部的IO操做都是異步的,所以,你不能馬上得知消息是否被正確處理,可是咱們能夠過一會等它執行完成或者直接註冊一個監聽,具體的實現就是經過Future和ChannelFutures,他們能夠註冊一個監聽,當操做執行成功或失敗時監聽會自動觸發。總之,全部的操做都會返回一個ChannelFuture。
也許最多見的是應用程序處理接收到消息後進行解碼,而後供相關業務邏輯模塊使用。因此應用程序只須要擴展SimpleChannelInboundHandler<I>,也就是咱們自定義一個繼承SimpleChannelInboundHandler<I>的handler類,其中<I>是handler能夠處理的消息類型。經過重寫父類的方法能夠得到一個ChannelHandlerContext的引用,它們接受一個ChannelHandlerContext的參數,你能夠在class中當一個屬性存儲。
處理程序關注的主要方法是「channelRead0(ChannelHandlerContext ctx, I msg)」,每當Netty調用這個方法,對象「I」是消息,這裏使用了Java的泛型設計,程序就能處理I。如何處理消息徹底取決於程序的須要。在處理消息時有一點須要注意的,在Netty中事件處理IO通常有不少線程,程序中儘可能不要阻塞IO線程,由於阻塞會下降程序的性能。
必須不阻塞IO線程意味着在ChannelHandler中使用阻塞操做會有問題。幸運的是Netty提供瞭解決方案,咱們能夠在添加ChannelHandler到ChannelPipeline中時指定一個EventExecutorGroup,EventExecutorGroup會得到一個EventExecutor,EventExecutor將執行ChannelHandler的全部方法。EventExecutor將使用不一樣的線程來執行和釋放EventLoop。
Netty自帶了一些傳輸協議的實現,雖然沒有支持全部的傳輸協議,可是其自帶的已足夠咱們來使用。Netty應用程序的傳輸協議依賴於底層協議,本節咱們將學習Netty中的傳輸協議。
Netty中的傳輸方式有以下幾種:
NIO,io.netty.channel.socket.nio,基於java.nio.channels的工具包,使用選擇器做爲基礎的方法。
OIO,io.netty.channel.socket.oio,基於java.net的工具包,使用阻塞流。
Local,io.netty.channel.local,用來在虛擬機之間本地通訊。
Embedded,io.netty.channel.embedded,嵌入傳輸,它容許在沒有真正網絡的運輸中使用ChannelHandler,能夠很是有用的來測試ChannelHandler的實現。
NIO傳輸是目前最經常使用的方式,它經過使用選擇器提供了徹底異步的方式操做全部的I/O,NIO從Java 1.4才被提供。NIO中,咱們能夠註冊一個通道或得到某個通道的改變的狀態,通道狀態有下面幾種改變:
一個新的Channel被接受並已準備好
Channel鏈接完成
Channel中有數據並已準備好讀取
Channel發送數據出去
處理完改變的狀態後需從新設置他們的狀態,用一個線程來檢查是否有已準備好的Channel,若是有則執行相關事件。在這裏可能只同時一個註冊的事件而忽略其餘的。選擇器所支持的操做在SelectionKey中定義,具體以下:
OP_ACCEPT,有新鏈接時獲得通知
OP_CONNECT,鏈接完成後獲得通知
OP_READ,準備好讀取數據時獲得通知
OP_WRITE,寫入數據到通道時獲得通知
Netty中的NIO傳輸就是基於這樣的模型來接收和發送數據,經過封裝將本身的接口提供給用戶使用,這徹底隱藏了內部實現。如前面所說,Netty隱藏內部的實現細節,將抽象出來的API暴露出來供使用,下面是處理流程圖:
Netty包含了本地傳輸,這個傳輸實現使用相同的API用於虛擬機之間的通訊,傳輸是徹底異步的。每一個Channel使用惟一的SocketAddress,客戶端經過使用SocketAddress進行鏈接,在服務器會被註冊爲長期運行,一旦通道關閉,它會自動註銷,客戶端沒法再使用它。
鏈接到本地傳輸服務器的行爲與其餘的傳輸實現幾乎是相同的,須要注意的一個重點是隻能在本地的服務器和客戶端上使用它們。Local未綁定任何Socket,值提供JVM進程之間的通訊。
ByteBuf
ByteBufHolder
ByteBufAllocator
使用這些接口分配緩衝和執行操做
Netty的緩衝API有兩個接口:
ByteBuf
ByteBufHolder
Netty使用reference-counting(引用計數)的時候知道安全釋放Buf和其餘資源,雖然知道Netty有效的使用引用計數,這都是自動完成的。這容許Netty使用池和其餘技巧來加快速
度和保持內存利用率在正常水平,你不須要作任何事情來實現這一點,可是在開發Netty應用程序時,你應該處理數據儘快釋放池資源。
Netty緩衝API提供了幾個優點:
能夠自定義緩衝類型
經過一個內置的複合緩衝類型實現零拷貝
擴展性好,好比StringBuffer
不須要調用flip()來切換讀/寫模式
讀取和寫入索引分開
方法鏈
引用計數
Pooling(池)
當須要與遠程進行交互時,須要以字節碼發送/接收數據。因爲各類緣由,一個高效、方便、易用的數據接口是必須的,而Netty的ByteBuf知足這些需求,ByteBuf是一個很好的通過優化的數據容器,咱們能夠將字節數據有效的添加到ByteBuf中或從ByteBuf中獲取數據。ByteBuf有2部分:一個用於讀,一個用於寫。咱們能夠按順序的讀取數據,而且能夠跳到開始從新讀一遍。全部的數據操做,咱們只須要作的是調整讀取數據索引和再次開始讀操做。
最經常使用的類型是ByteBuf將數據存儲在JVM的堆空間,這是經過將數據存儲在數組的實現。堆緩衝區能夠快速分配,當不使用時也能夠快速釋放。它還提供了直接訪問數組的方法,經過ByteBuf.array()來獲取byte[]數據。
訪問非堆緩衝區ByteBuf的數組會致使UnsupportedOperationException,可使用ByteBuf.hasArray()來檢查是否支持訪問數組。
直接緩衝區,在堆以外直接分配內存。直接緩衝區不會佔用堆空間容量,使用時應該考慮到應用程序要使用的最大內存容量以及如何限制它。直接緩衝區在使用Socket傳遞數據時性能很好,由於若使用間接緩衝區,JVM會先將數據複製到直接緩衝區再進行傳遞;可是直接緩衝區的缺點是在分配內存空間和釋放內存時比堆緩衝區更復雜,而Netty使用內存池來解決這樣的問題,這也是Netty使用內存池的緣由之一。直接緩衝區不支持數組訪問數據,可是咱們能夠間接的訪問數據數組,以下面代碼:
1. ByteBuf directBuf = Unpooled.directBuffer(16);
2. if(!directBuf.hasArray()){
3. int len = directBuf.readableBytes();
4. byte[] arr = new byte[len];
5. directBuf.getBytes(0, arr);
6. }
訪問直接緩衝區的數據數組須要更多的編碼和更復雜的操做,建議若須要在數組訪問數據使用堆緩衝區會更好。
複合緩衝區,咱們能夠建立多個不一樣的ByteBuf,而後提供一個這些ByteBuf組合的視圖。複合緩衝區就像一個列表,咱們能夠動態的添加和刪除其中的ByteBuf,JDK的
ByteBuffer沒有這樣的功能。Netty提供了CompositeByteBuf類來處理複合緩衝區,CompositeByteBuf只是一個視圖,CompositeByteBuf.hasArray()老是返回false,由於它可能包含一些直接或間接的不一樣類型的ByteBuf。
例如,一條消息由header和body兩部分組成,將header和body組裝成一條消息發送出去,可能body相同,只是header不一樣,使用CompositeByteBuf就不用每次都從新分配一個新的緩衝區。下圖顯示CompositeByteBuf組成header和body:
一個Channel會對應一個EventLoop,而一個EventLoop會對應着一個線程,也就是說,僅有一個線程在負責一個Channel的IO操做。因此不須要想若是同步代碼。
如圖所示:當一個鏈接到達,Netty會註冊一個channel,而後EventLoopGroup會分配一個EventLoop綁定到這個channel,在這個channel的整個生命週期過程當中,都會由綁定的這個EventLoop來爲它服務,而這個EventLoop就是一個線程。
說到這裏,那麼EventLoops和EventLoopGroup關係是如何的呢?咱們前面說過一個EventLoopGroup包含多個Eventloop,可是咱們看一下這幅圖,這幅圖是一個繼承樹,從這幅圖中咱們能夠看出,EventLoop其實繼承自EventloopGroup,也就是說,在某些狀況下,咱們能夠把一個EventLoopGroup當作一個EventLoop來用。
咱們利用BootsStrapping來配置netty 應用,它有兩種類型,一種用於Client端:BootsStrap,另外一種用於Server端:ServerBootstrap,要想區別如何使用它們,你僅須要記住一個用在Client端,一個用在Server端。下面咱們來詳細介紹一下這兩種類型的區別:
1.第一個最明顯的區別是,ServerBootstrap用於Server端,經過調用bind()方法來綁定到一個端口監聽鏈接;Bootstrap用於Client端,須要調用connect()方法來鏈接服務器端,但咱們也能夠經過調用bind()方法返回的ChannelFuture中獲取Channel去connect服務器端。
2.客戶端的Bootstrap通常用一個EventLoopGroup,而服務器端的ServerBootstrap會用到兩個(這兩個也能夠是同一個實例)。爲什麼服務器端要用到兩個EventLoopGroup呢?這麼設計有明顯的好處,若是一個ServerBootstrap有兩個EventLoopGroup,那麼就能夠把第一個EventLoopGroup用來專門負責綁定到端口監聽鏈接事件,而把第二個EventLoopGroup用來處理每一個接收到的鏈接,下面咱們用一幅圖來展示一下這種模式:
若是僅由一個EventLoopGroup處理全部請求和鏈接的話,在併發量很大的狀況下,這個EventLoopGroup有可能會忙於處理已經接收到的鏈接而不能及時處理新的鏈接請求,用兩個的話,會有專門的線程來處理鏈接請求,不會致使請求超時的狀況,大大提升了併發處理能力。
咱們的應用程序中用到的最多的應該就是ChannelHandler,咱們能夠這麼想象,數據在一個ChannelPipeline中流動,而ChannelHandler即是其中的一個個的小閥門,這些數據都會通過每個ChannelHandler而且被它處理。這裏有一個公共接口ChannelHandler:
從上圖中咱們能夠看到,ChannelHandler有兩個子類ChannelInboundHandler和ChannelOutboundHandler,這兩個類對應了兩個數據流向,若是數據是從外部流入咱們的應用程序,咱們就看作是inbound,相反即是outbound。其實ChannelHandler和Servlet有些相似,一個ChannelHandler處理完接收到的數據會傳給下一個Handler,或者什麼不處理,直接傳遞給下一個。下面咱們看一下ChannelPipeline是如何安排ChannelHandler的:
從上圖中咱們能夠看到,一個ChannelPipeline能夠把兩種Handler(ChannelInboundHandler和ChannelOutboundHandler)混合在一塊兒,當一個數據流進入ChannelPipeline時,它會從ChannelPipeline頭部開始傳給第一個ChannelInboundHandler,當第一個處理完後再傳給下一個,一直傳遞到管道的尾部。與之相對應的是,當數據被寫出時,它會從管道的尾部開始,先通過管道尾部的「最後」一個ChannelOutboundHandler,當它處理完成後會傳遞給前一個ChannelOutboundHandler。
當一個ChannelHandler被加入到ChannelPipeline中時,它便會得到一個ChannelHandlerContext的引用,而ChannelHandlerContext能夠用來讀寫Netty中的數據流。所以,如今能夠有兩種方式來發送數據,一種是把數據直接寫入Channel,一種是把數據寫入ChannelHandlerContext,它們的區別是寫入Channel的話,數據流會從Channel的頭開始傳遞,而若是寫入ChannelHandlerContext的話,數據流會流入管道中的下一個Handler。
Netty中會有不少Handler,具體是哪一種Handler還要看它們繼承的是InboundAdapter仍是OutboundAdapter。固然,Netty中還提供了一些列的Adapter來幫助咱們簡化開發,咱們知道在Channelpipeline中每個Handler都負責把Event傳遞給下一個Handler,若是有了這些輔助Adapter,這些額外的工做均可自動完成,咱們只需覆蓋實現咱們真正關心的部分便可。
Domain Logic
其實咱們最最關心的事情就是如何處理接收到的解碼後的數據,咱們真正的業務邏輯即是處理接收到的數據。Netty提供了一個最經常使用的基類SimpleChannelInboundHandler<T>,其中T就是這個Handler處理的數據的類型(上一個Handler已經替咱們解碼好了),消息到達這個Handler時,Netty會自動調用這個Handler中的channelRead0(ChannelHandlerContext,T)方法,T是傳遞過來的數據對象,在這個方法中咱們即可以任意寫咱們的業務邏輯了。
EventLoopGroup類的建立
EventLoopGroup bossGroup= new NioEventLoopGroup();
主要方法:
ServerBootstrap類的建立
ServerBootstrap b=new ServerBootstrap();
主要方法:
group()