高清思惟導圖原件(xmind/pdf/jpg
)能夠關注公衆號:一枝花算不算浪漫
回覆nio
便可。前端
前言
抱歉很久沒更原創文章了,看了下上篇更新時間,已經拖更一個多月了。java
這段時間也一直在學習Netty
相關知識,由於涉及知識點比較多,也走了很多彎路。目前網上關於Netty學習資料玲琅滿目,不知如何下手,其實你們都是同樣的,學習方法和技巧都是總結出來的,咱們在沒有找到很好的方法以前不如循序漸進先從基礎開始,通常從總分總的漸進方式,既觀森林,又見草木。linux
以前恰巧跟杭州一個朋友小飛也提到過,二者在這方面的初衷是一致的,也但願更多的朋友可以加入一塊兒學習和探討。(PS:本篇文章是和小飛一塊兒學習整理所得~)程序員
Netty
是一款提供異步的、事件驅動的網絡應用程序框架和工具,是基於NIO
客戶端、服務器端的編程框架。因此這裏咱們先以NIO
和依賴相關的基礎鋪墊來進行剖析講解,從而做爲Netty
學習之旅的一個開端。算法
1、網絡編程基礎回顧
1. Socket
Socket
自己有「插座」的意思,不是Java中特有的概念,而是一個語言無關的標準,任何能夠實現網絡編程的編程語言都有Socket
。在Linux
環境下,用於表示進程間網絡通訊的特殊文件類型,其本質爲內核藉助緩衝區造成的僞文件。既然是文件,那麼理所固然的,咱們可使用文件描述符引用套接字。編程
與管道相似的,Linux
系統將其封裝成文件的目的是爲了統一接口,使得讀寫套接字和讀寫文件的操做一致。區別是管道主要應用於本地進程間通訊,而套接字多應用於網絡進程間數據的傳遞。設計模式
能夠這麼理解:Socket
就是網絡上的兩個應用程序經過一個雙向通訊鏈接實現數據交換的編程接口API。數組
Socket
通訊的基本流程具體步驟以下所示:緩存
(1)服務端經過Listen
開啓監聽,等待客戶端接入。服務器
(2)客戶端的套接字經過Connect
鏈接服務器端的套接字,服務端經過Accept
接收客戶端鏈接。在connect-accept
過程當中,操做系統將會進行三次握手。
(3)客戶端和服務端經過write
和read
發送和接收數據,操做系統將會完成TCP
數據的確認、重發等步驟。
(4)經過close
關閉鏈接,操做系統會進行四次揮手。
針對Java編程語言,java.net
包是網絡編程的基礎類庫。其中ServerSocket
和Socket
是網絡編程的基礎類型。
SeverSocket
是服務端應用類型。Socket
是創建鏈接的類型。當鏈接創建成功後,服務器和客戶端都會有一個Socket
對象示例,能夠經過這個Socket
對象示例,完成會話的全部操做。對於一個完整的網絡鏈接來講,Socket
是平等的,沒有服務器客戶端分級狀況。
2. IO模型介紹
對於一次IO操做,數據會先拷貝到內核空間中,而後再從內核空間拷貝到用戶空間中,因此一次read
操做,會經歷兩個階段:
(1)等待數據準備
(2)數據從內核空間拷貝到用戶空間
基於以上兩個階段就產生了五種不一樣的IO模式。
- 阻塞IO:從進程發起IO操做,一直等待上述兩個階段完成,此時兩階段一塊兒阻塞。
- 非阻塞IO:進程一直詢問IO準備好了沒有,準備好了再發起讀取操做,這時才把數據從內核空間拷貝到用戶空間。第一階段不阻塞但要輪詢,第二階段阻塞。
- 多路複用IO:多個鏈接使用同一個select去詢問IO準備好了沒有,若是有準備好了的,就返回有數據準備好了,而後對應的鏈接再發起讀取操做,把數據從內核空間拷貝到用戶空間。兩階段分開阻塞。
- 信號驅動IO:進程發起讀取操做會當即返回,當數據準備好了會以通知的形式告訴進程,進程再發起讀取操做,把數據從內核空間拷貝到用戶空間。第一階段不阻塞,第二階段阻塞。
- 異步IO:進程發起讀取操做會當即返回,等到數據準備好且已經拷貝到用戶空間了再通知進程拿數據。兩個階段都不阻塞。
這五種IO模式不難發現存在這兩對關係:同步和異步、阻塞和非阻塞。那麼稍微解釋一下:
同步和異步
- 同步: 同步就是發起一個調用後,被調用者未處理完請求以前,調用不返回。
- 異步: 異步就是發起一個調用後,馬上獲得被調用者的迴應表示已接收到請求,可是被調用者並無返回結果,此時咱們能夠處理其餘的請求,被調用者一般依靠事件,回調等機制來通知調用者其返回結果。
同步和異步的區別最大在於異步的話調用者不須要等待處理結果,被調用者會經過回調等機制來通知調用者其返回結果。
阻塞和非阻塞
- 阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,沒法從事其餘任務,只有當條件就緒才能繼續。
- 非阻塞: 非阻塞就是發起一個請求,調用者不用一直等着結果返回,能夠先去幹其餘事情。
阻塞和非阻塞是針對進程在訪問數據的時候,根據IO操做的就緒狀態來採起的不一樣方式,說白了是一種讀取或者寫入操做方法的實現方式,阻塞方式下讀取或者寫入函數將一直等待,而非阻塞方式下,讀取或者寫入方法會當即返回一個狀態值。
若是組合後的同步阻塞(blocking-IO
)簡稱BIO
、同步非阻塞(non-blocking-IO
)簡稱NIO
和異步非阻塞(asynchronous-non-blocking-IO
)簡稱AIO
又表明什麼意思呢?
- BIO (同步阻塞I/O模式): 數據的讀取寫入必須阻塞在一個線程內等待其完成。這裏使用那個經典的燒開水例子,這裏假設一個燒開水的場景,有一排水壺在燒開水,BIO的工做模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,纔去處理下一個水壺。可是實際上線程在等待水壺燒開的時間段什麼都沒有作。
- NIO(同步非阻塞): 同時支持阻塞與非阻塞模式,但這裏咱們以其同步非阻塞I/O模式來講明,那麼什麼叫作同步非阻塞?若是還拿燒開水來講,NIO的作法是叫一個線程不斷的輪詢每一個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操做。
- AIO(異步非阻塞I/O模型): 異步非阻塞與同步非阻塞的區別在哪裏?異步非阻塞無需一個線程去輪詢全部IO操做的狀態改變,在相應的狀態改變後,系統會通知對應的線程來處理。對應到燒開水中就是,爲每一個水壺上面裝了一個開關,水燒開以後,水壺會自動通知我水燒開了。
java
中的 BIO
、NIO
和AIO
理解爲是 Java 語言
在操做系統層面對這三種 IO
模型的封裝。程序員在使用這些 封裝API 的時候,不須要關心操做系統層面的知識,也不須要根據不一樣操做系統編寫不一樣的代碼,只須要使用Java
的API就能夠了。由此,爲了使讀者對這三種模型有個比較具體和遞推式的瞭解,而且和本文主題NIO
有個清晰的對比,下面繼續延伸。
Java BIO
BIO
編程方式一般是是Java的上古產品,自JDK 1.0-JDK1.4就有的東西。編程實現過程爲:首先在服務端啓動一個ServerSocket
來監聽網絡請求,客戶端啓動Socket
發起網絡請求,默認狀況下SeverSocket
會創建一個線程來處理此請求,若是服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕。服務器實現模式爲一個鏈接一個線程,即客戶端有鏈接請求時服務器端就須要啓動一個線程進行處理。大體結構以下:
若是要讓 BIO
通訊模型可以同時處理多個客戶端請求,就必須使用多線程(主要緣由是 socket.accept()
、socket.read()
、 socket.write()
涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端鏈接請求以後爲每一個客戶端建立一個新的線程進行鏈路處理,處理完成以後,經過輸出流返回應答給客戶端,線程銷燬。這就是典型的 一請求一應答通訊模型 。咱們能夠設想一下若是這個鏈接不作任何事情的話就會形成沒必要要的線程開銷,不過能夠經過線程池機制改善,線程池還可讓線程的建立和回收成本相對較低。使用線程池機制改善後的 BIO
模型圖以下:
BIO
方式適用於鏈接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,是JDK1.4之前的惟一選擇,但程序直觀簡單易懂。Java BIO
編程示例網上不少,這裏就不進行coding舉例了,畢竟後面NIO
纔是重點。
Java NIO
NIO
(New IO或者No-Blocking IO),從JDK1.4 開始引入的非阻塞IO
,是一種非阻塞
+ 同步
的通訊模式。這裏的No Blocking IO
用於區分上面的BIO
。
NIO
自己想解決 BIO
的併發問題,經過Reactor模式
的事件驅動機制來達到Non Blocking
的。當 socket
有流可讀或可寫入 socket
時,操做系統會相應的通知應用程序進行處理,應用再將流讀取到緩衝區或寫入操做系統。
也就是說,這個時候,已經不是一個鏈接就 要對應一個處理線程了,而是有效的請求,對應一個線程,當鏈接沒有數據時,是沒有工做線程來處理的。
當一個鏈接建立後,不須要對應一個線程,這個鏈接會被註冊到 多路複用器
上面,因此全部的鏈接只須要一個線程就能夠搞定,當這個線程中的多路複用器
進行輪詢的時候,發現鏈接上有請求的話,纔開啓一個線程進行處理,也就是一個請求一個線程模式。
NIO
提供了與傳統BIO模型中的Socket
和ServerSocket
相對應的SocketChannel
和ServerSocketChannel
兩種不一樣的套接字通道實現,以下圖結構所示。這裏涉及的Reactor
設計模式、多路複用Selector
、Buffer
等暫時不用管,後面會講到。
NIO 方式適用於鏈接數目多且鏈接比較短(輕操做)的架構,好比聊天服務器,併發局 限於應用中,編程複雜,JDK1.4 開始支持。同時,NIO
和普通IO的區別主要能夠從存儲數據的載體、是否阻塞等來區分:
Java AIO
與 NIO
不一樣,當進行讀寫操做時,只須直接調用 API 的 read
或 write
方法便可。這兩種方法均爲異步的,對於讀操做而言,當有流可讀取時,操做系統會將可讀的流傳入 read
方 法的緩衝區,並通知應用程序;對於寫操做而言,當操做系統將 write
方法傳遞的流寫入完畢時,操做系統主動通知應用程序。便可以理解爲,read/write
方法都是異步的,完成後會主動調用回調函數。在 JDK7
中,提供了異步文件通道和異步套接字通道的實現,這部份內容被稱做 NIO
.
AIO
方式使用於鏈接數目多且鏈接比較長(重操做)的架構,好比相冊服務器,充分調用 OS
參與併發操做,編程比較複雜,JDK7
開始支持。
目前來講 AIO
的應用還不是很普遍,Netty
以前也嘗試使用過 AIO
,不過又放棄了。
2、NIO核心組件介紹
1. Channel
在NIO
中,基本全部的IO操做都是從Channel
開始的,Channel
經過Buffer(緩衝區)
進行讀寫操做。
read()
表示讀取通道中數據到緩衝區,write()
表示把緩衝區數據寫入到通道。
Channel
有好多實現類,這裏有三個最經常使用:
SocketChannel
:一個客戶端發起TCP鏈接的ChannelServerSocketChannel
:一個服務端監聽新鏈接的TCP Channel,對於每個新的Client鏈接,都會創建一個對應的SocketChannelFileChannel
:從文件中讀寫數據
其中SocketChannel
和ServerSocketChannel
是網絡編程中最經常使用的,一會在最後的示例代碼中會有講解到具體用法。
2. Buffer
概念
Buffer
也被成爲內存緩衝區,本質上就是內存中的一塊,咱們能夠將數據寫入這塊內存,以後從這塊內存中讀取數據。也能夠將這塊內存封裝成NIO Buffer
對象,並提供一組經常使用的方法,方便咱們對該塊內存進行讀寫操做。
Buffer
在java.nio
中被定義爲抽象類:
咱們能夠將Buffer
理解爲一個數組的封裝,咱們最經常使用的ByteBuffer
對應的數據結構就是byte[]
屬性
Buffer
中有4個很是重要的屬性:capacity、limit、position、mark
capacity
屬性:容量,Buffer可以容納的數據元素的最大值,在Buffer初始化建立的時候被賦值,並且不能被修改。
上圖中,初始化Buffer的容量爲8(圖中從0~7,共8個元素),因此capacity = 8
-
limit
屬性:表明Buffer可讀可寫的上限。- 寫模式下:
limit
表明能寫入數據的上限位置,這個時候limit = capacity
讀模式下:在Buffer
完成全部數據寫入後,經過調用flip()
方法,切換到讀模式,此時limit
等於Buffer
中實際已經寫入的數據大小。由於Buffer
可能沒有被寫滿,因此limit<=capacity
- 寫模式下:
-
position
屬性:表明讀取或者寫入Buffer
的位置。默認爲0。- 寫模式下:每往
Buffer
中寫入一個值,position
就會自動加1,表明下一次寫入的位置。 - 讀模式下:每往
Buffer
中讀取一個值,position
就自動加1,表明下一次讀取的位置。
- 寫模式下:每往
從上圖就能很清晰看出,讀寫模式下capacity、limit、position的關係了。
mark
屬性:表明標記,經過mark()方法,記錄當前position值,將position值賦值給mark,在後續的寫入或讀取過程當中,能夠經過reset()方法恢復當前position爲mark記錄的值。
這幾個重要屬性講完,咱們能夠再來回顧下:
0 <= mark <= position <= limit <= capacity
如今應該很清晰這幾個屬性的關係了~
Buffer常見操做
建立Buffer
allocate(int capacity)
ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer);
例子中建立的ByteBuffer
是基於堆內存的一個對象。
wrap(array)
wrap
方法能夠將數組包裝成一個Buffer
對象:
ByteBuffer buffer = ByteBuffer.wrap("hello world".getBytes()); channel.write(buffer);
allocateDirect(int capacity)
經過allocateDirect
方法也能夠快速實例化一個Buffer
對象,和allocate
很類似,這裏區別的是allocateDirect
建立的是基於堆外內存的對象。
堆外內存不在JVM堆上,不受GC的管理。堆外內存進行一些底層系統的IO操做時,效率會更高。
Buffer寫操做
Buffer
寫入能夠經過put()
和channel.read(buffer)
兩種方式寫入。
一般咱們NIO的讀操做的時候,都是從Channel
中讀取數據寫入Buffer
,這個對應的是Buffer
的寫操做。
Buffer讀操做
Buffer
讀取能夠經過get()
和channel.write(buffer)
兩種方式讀入。
仍是同上,咱們對Buffer
的讀入操做,反過來講就是對Channel
的寫操做。讀取Buffer
中的數據而後寫入Channel
中。
其餘常見方法
rewind()
:重置position位置爲0,能夠從新讀取和寫入buffer,通常該方法適用於讀操做,能夠理解爲對buffer的重複讀。
public final Buffer rewind() { position = 0; mark = -1; return this; }
flip()
:很經常使用的一個方法,通常在寫模式切換到讀模式的時候會常常用到。也會將position設置爲0,而後設置limit等於原來寫入的position。
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
clear()
:重置buffer中的數據,該方法主要是針對於寫模式,由於limit設置爲了capacity,讀模式下會出問題。
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
mark()&reset()
:mark()
方法是保存當前position
到變量mark
z中,而後經過reset()
方法恢復當前position
爲mark
,實現代碼很簡單,以下:
public final Buffer mark() { mark = position; return this; } public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this; }
經常使用的讀寫方法能夠用一張圖總結一下:
3. Selector
概念
Selector
是NIO中最爲重要的組件之一,咱們經常說的多路複用器
就是指的Selector
組件。 Selector
組件用於輪詢一個或多個NIO Channel
的狀態是否處於可讀、可寫。經過輪詢的機制就能夠管理多個Channel,也就是說能夠管理多個網絡鏈接。
輪詢機制
- 首先,須要將Channel註冊到Selector上,這樣Selector才知道須要管理哪些Channel
- 接着Selector會不斷輪詢其上註冊的Channel,若是某個Channel發生了讀或寫的時間,這個Channel就會被Selector輪詢出來,而後經過SelectionKey能夠獲取就緒的Channel集合,進行後續的IO操做。
屬性操做
- 建立Selector
經過open()
方法,咱們能夠建立一個Selector
對象。
Selector selector = Selector.open();
- 註冊Channel到Selector中
咱們須要將Channel
註冊到Selector
中,纔可以被Selector
管理。
channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
某個Channel
要註冊到Selector
中,那麼該Channel必須是非阻塞,全部上面代碼中有個configureBlocking()
的配置操做。
在register(Selector selector, int interestSet)
方法的第二個參數,標識一個interest
集合,意思是Selector對哪些事件感興趣,能夠監聽四種不一樣類型的事件:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << ; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
Connect事件
:鏈接完成事件( TCP 鏈接 ),僅適用於客戶端,對應 SelectionKey.OP_CONNECT。Accept事件
:接受新鏈接事件,僅適用於服務端,對應 SelectionKey.OP_ACCEPT 。Read事件
:讀事件,適用於兩端,對應 SelectionKey.OP_READ ,表示 Buffer 可讀。Write事件
:寫時間,適用於兩端,對應 SelectionKey.OP_WRITE ,表示 Buffer 可寫。
Channel
觸發了一個事件,代表該時間已經準備就緒:
- 一個Client Channel成功鏈接到另外一個服務器,成爲「鏈接就緒」
- 一個Server Socket準備好接收新進入的接,稱爲「接收就緒」
- 一個有數據可讀的Channel,稱爲「讀就緒」
- 一個等待寫數據的Channel,稱爲」寫就緒「
固然,Selector
是能夠同時對多個事件感興趣的,咱們使用或運算便可組合多個事件:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Selector其餘一些操做
選擇Channel
public abstract int select() throws IOException; public abstract int select(long timeout) throws IOException; public abstract int selectNow() throws IOException;
當Selector執行select()
方法就會產生阻塞,等到註冊在其上的Channel準備就緒就會當即返回,返回準備就緒的數量。
select(long timeout)
則是在select()
的基礎上增長了超時機制。 selectNow()
當即返回,不產生阻塞。
有一點很是須要注意: select
方法返回的 int
值,表示有多少 Channel
已經就緒。
自上次調用select
方法後有多少 Channel
變成就緒狀態。若是調用 select
方法,由於有一個 Channel
變成就緒狀態則返回了 1 ;
若再次調用 select
方法,若是另外一個 Channel
就緒了,它會再次返回1。
獲取可操做的Channel
Set selectedKeys = selector.selectedKeys();
當有新增就緒的Channel
,調用select()
方法,就會將key添加到Set集合中。
3、代碼示例
前面鋪墊了這麼多,主要是想讓你們可以看懂NIO
代碼示例,也方便後續你們來本身手寫NIO
網絡編程的程序。建立NIO服務端的主要步驟以下:
1. 打開ServerSocketChannel,監聽客戶端鏈接 2. 綁定監聽端口,設置鏈接爲非阻塞模式 3. 建立Reactor線程,建立多路複用器並啓動線程 4. 將ServerSocketChannel註冊到Reactor線程中的Selector上,監聽ACCEPT事件 5. Selector輪詢準備就緒的key 6. Selector監聽到新的客戶端接入,處理新的接入請求,完成TCP三次握手,創建物理鏈路 7. 設置客戶端鏈路爲非阻塞模式 8. 將新接入的客戶端鏈接註冊到Reactor線程的Selector上,監聽讀操做,讀取客戶端發送的網絡消息 9. 異步讀取客戶端消息到緩衝區 10.對Buffer編解碼,處理半包消息,將解碼成功的消息封裝成Task 11.將應答消息編碼爲Buffer,調用SocketChannel的write將消息異步發送給客戶端
NIOServer.java
:
public class NIOServer { private static Selector selector; public static void main(String[] args) { init(); listen(); } private static void init() { ServerSocketChannel serverSocketChannel = null; try { selector = Selector.open(); serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(9000)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("NioServer 啓動完成"); } catch (IOException e) { e.printStackTrace(); } } private static void listen() { while (true) { try { selector.select(); Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator(); while (keysIterator.hasNext()) { SelectionKey key = keysIterator.next(); keysIterator.remove(); handleRequest(key); } } catch (Throwable t) { t.printStackTrace(); } } } private static void handleRequest(SelectionKey key) throws IOException { SocketChannel channel = null; try { if (key.isAcceptable()) { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); channel = serverSocketChannel.accept(); channel.configureBlocking(false); System.out.println("接受新的 Channel"); channel.register(selector, SelectionKey.OP_READ); } if (key.isReadable()) { channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer); if (count > 0) { System.out.println("服務端接收請求:" + new String(buffer.array(), 0, count)); channel.register(selector, SelectionKey.OP_WRITE); } } if (key.isWritable()) { ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("收到".getBytes()); buffer.flip(); channel = (SocketChannel) key.channel(); channel.write(buffer); channel.register(selector, SelectionKey.OP_READ); } } catch (Throwable t) { t.printStackTrace(); if (channel != null) { channel.close(); } } } }
NIOClient.java
:
public class NIOClient { public static void main(String[] args) { new Worker().start(); } static class Worker extends Thread { @Override public void run() { SocketChannel channel = null; Selector selector = null; try { channel = SocketChannel.open(); channel.configureBlocking(false); selector = Selector.open(); channel.register(selector, SelectionKey.OP_CONNECT); channel.connect(new InetSocketAddress(9000)); while (true) { selector.select(); Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator(); while (keysIterator.hasNext()) { SelectionKey key = keysIterator.next(); keysIterator.remove(); if (key.isConnectable()) { System.out.println(); channel = (SocketChannel) key.channel(); if (channel.isConnectionPending()) { channel.finishConnect(); ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("你好".getBytes()); buffer.flip(); channel.write(buffer); } channel.register(selector, SelectionKey.OP_READ); } if (key.isReadable()) { channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int len = channel.read(buffer); if (len > 0) { System.out.println("[" + Thread.currentThread().getName() + "]收到響應:" + new String(buffer.array(), 0, len)); Thread.sleep(5000); channel.register(selector, SelectionKey.OP_WRITE); } } if(key.isWritable()) { ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("你好".getBytes()); buffer.flip(); channel = (SocketChannel) key.channel(); channel.write(buffer); channel.register(selector, SelectionKey.OP_READ); } } } } catch (Exception e) { e.printStackTrace(); } finally{ if(channel != null){ try { channel.close(); } catch (IOException e) { e.printStackTrace(); } } if(selector != null){ try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }
打印結果:
// Server端 NioServer 啓動完成 接受新的 Channel 服務端接收請求:你好 服務端接收請求:你好 服務端接收請求:你好 // Client端 [Thread-0]收到響應:收到 [Thread-0]收到響應:收到 [Thread-0]收到響應:收到
4、總結
回顧一下使用 NIO
開發服務端程序的步驟:
- 建立
ServerSocketChannel
和業務處理線程池。 - 綁定監聽端口,並配置爲非阻塞模式。
- 建立
Selector
,將以前建立的ServerSocketChannel
註冊到Selector
上,監聽SelectionKey.OP_ACCEPT
。 - 循環執行
Selector.select()`` 方法,輪詢就緒的
Channel`。 - 輪詢就緒的
Channel
時,若是是處於OP_ACCEPT
狀態,說明是新的客戶端接入,調用ServerSocketChannel.accept
接收新的客戶端。 - 設置新接入的
SocketChannel
爲非阻塞模式,並註冊到Selector
上,監聽OP_READ
。 - 若是輪詢的
Channel
狀態是OP_READ
,說明有新的就緒數據包須要讀取,則構造ByteBuffer
對象,讀取數據。
那從這些步驟中基本知道開發者須要熟悉的知識點有:
jdk-nio
提供的幾個關鍵類:Selector
,SocketChannel
,ServerSocketChannel
,FileChannel
,ByteBuffer
,SelectionKey
- 須要知道網絡知識:tcp粘包拆包 、網絡閃斷、包體溢出及重複發送等
- 須要知道
linux
底層實現,如何正確的關閉channel
,如何退出註銷selector
,如何避免selector
太過於頻繁 - 須要知道如何讓
client
端得到server
端的返回值,而後才返回給前端,須要如何等待或在怎樣做熔斷機制 - 須要知道對象序列化,及序列化算法
- 省略等等,由於我已經有點不舒服了,做爲程序員的我習慣了舒舒服服簡單的API,不用太知道底層細節,就能寫出比較健壯和沒有Bug的代碼...
NIO 原生 API 的弊端 :
① NIO 組件複雜 : 使用原生 NIO
開發服務器端與客戶端 , 須要涉及到 服務器套接字通道 ( ServerSocketChannel
) , 套接字通道 ( SocketChannel
) , 選擇器 ( Selector
) , 緩衝區 ( ByteBuffer
) 等組件 , 這些組件的原理 和API 都要熟悉 , 才能進行 NIO
的開發與調試 , 以後還須要針對應用進行調試優化
② NIO 開發基礎 : NIO
門檻略高 , 須要開發者掌握多線程、網絡編程等才能開發而且優化 NIO
網絡通訊的應用程序
③ 原生 API 開發網絡通訊模塊的基本的傳輸處理 : 網絡傳輸不光是實現服務器端和客戶端的數據傳輸功能 , 還要處理各類異常狀況 , 如 鏈接斷開重連機制 , 網絡堵塞處理 , 異常處理 , 粘包處理 , 拆包處理 , 緩存機制 等方面的問題 , 這是全部成熟的網絡應用程序都要具備的功能 , 不然只能說是入門級的 Demo
④ NIO BUG : NIO
自己存在一些 BUG , 如 Epoll
, 致使 選擇器 ( Selector
) 空輪詢 , 在 JDK 1.7 中尚未解決
Netty
在 NIO
的基礎上 , 封裝了 Java 原生的 NIO API
, 解決了上述哪些問題呢 ?
相比 Java NIO,使用 Netty
開發程序,都簡化了哪些步驟呢?...等等這系列問題也都是咱們要問的問題。不過由於這篇只是介紹NIO
相關知識,沒有介紹Netty API
的使用,因此介紹Netty API
使用簡單開發門檻低等優勢有點站不住腳。那麼就留到後面跟你們一塊兒開啓Netty
學習之旅,探討人人說好的Netty
究竟是不是江湖傳言的那麼好。
一塊兒期待後續的Netty
之旅吧!