Netty之旅:你想要的NIO知識點,這裏都有!

NIO思維導圖.png

高清思維導圖原件(xmind/pdf/jpg)可以關注公衆號:一枝花算不算浪漫 回覆nio即可。(文末有二維碼)

前言

抱歉好久沒更原創文章了,看了下上篇更新時間,已經拖更一個多月了。

這段時間也一直在學習Netty相關知識,因爲涉及知識點比較多,也走了不少彎路。目前網上關於Netty學習資料玲琅滿目,不知如何下手,其實大家都是一樣的,學習方法和技巧都是總結出來的,我們在沒有找到很好的方法之前不如按部就班先從基礎開始,一般從總分總的漸進方式,既觀森林,又見草木。

之前恰巧跟杭州一個朋友小飛也提到過,兩者在這方面的初衷是一致的,也希望更多的朋友能夠加入一起學習和探討。(PS:本篇文章是和小飛一起學習整理所得~)

Netty是一款提供異步的、事件驅動的網絡應用程序框架和工具,是基於NIO客戶端、服務器端的編程框架。所以這裏我們先以NIO和依賴相關的基礎鋪墊來進行剖析講解,從而作爲Netty學習之旅的一個開端。

一、網絡編程基礎回顧

1. Socket

Socket本身有“插座”的意思,不是Java中特有的概念,而是一個語言無關的標準,任何可以實現網絡編程的編程語言都有Socket。在Linux環境下,用於表示進程間網絡通信的特殊文件類型,其本質爲內核藉助緩衝區形成的僞文件。既然是文件,那麼理所當然的,我們可以使用文件描述符引用套接字。

與管道類似的,Linux系統將其封裝成文件的目的是爲了統一接口,使得讀寫套接字和讀寫文件的操作一致。區別是管道主要應用於本地進程間通信,而套接字多應用於網絡進程間數據的傳遞。

可以這麼理解:Socket就是網絡上的兩個應用程序通過一個雙向通信連接實現數據交換的編程接口API。

Socket通信的基本流程具體步驟如下所示:

(1)服務端通過Listen開啓監聽,等待客戶端接入。

(2)客戶端的套接字通過Connect連接服務器端的套接字,服務端通過Accept接收客戶端連接。在connect-accept過程中,操作系統將會進行三次握手。

(3)客戶端和服務端通過writeread發送和接收數據,操作系統將會完成TCP數據的確認、重發等步驟。

(4)通過close關閉連接,操作系統會進行四次揮手。

針對Java編程語言,java.net包是網絡編程的基礎類庫。其中ServerSocketSocket是網絡編程的基礎類型。

SeverSocket是服務端應用類型。Socket是建立連接的類型。當連接建立成功後,服務器和客戶端都會有一個Socket對象示例,可以通過這個Socket對象示例,完成會話的所有操作。對於一個完整的網絡連接來說,Socket是平等的,沒有服務器客戶端分級情況。

2. IO模型介紹

對於一次IO操作,數據會先拷貝到內核空間中,然後再從內核空間拷貝到用戶空間中,所以一次read操作,會經歷兩個階段:

(1)等待數據準備

(2)數據從內核空間拷貝到用戶空間

基於以上兩個階段就產生了五種不同的IO模式。

  1. 阻塞IO:從進程發起IO操作,一直等待上述兩個階段完成,此時兩階段一起阻塞。
  2. 非阻塞IO:進程一直詢問IO準備好了沒有,準備好了再發起讀取操作,這時才把數據從內核空間拷貝到用戶空間。第一階段不阻塞但要輪詢,第二階段阻塞。
  3. 多路複用IO:多個連接使用同一個select去詢問IO準備好了沒有,如果有準備好了的,就返回有數據準備好了,然後對應的連接再發起讀取操作,把數據從內核空間拷貝到用戶空間。兩階段分開阻塞。
  4. 信號驅動IO:進程發起讀取操作會立即返回,當數據準備好了會以通知的形式告訴進程,進程再發起讀取操作,把數據從內核空間拷貝到用戶空間。第一階段不阻塞,第二階段阻塞。
  5. 異步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 中的 BIONIOAIO理解爲是 Java 語言在操作系統層面對這三種 IO 模型的封裝。程序員在使用這些 封裝API 的時候,不需要關心操作系統層面的知識,也不需要根據不同操作系統編寫不同的代碼,只需要使用Java的API就可以了。由此,爲了使讀者對這三種模型有個比較具體和遞推式的瞭解,並且和本文主題NIO有個清晰的對比,下面繼續延伸。

Java BIO

BIO編程方式通常是是Java的上古產品,自JDK 1.0-JDK1.4就有的東西。編程實現過程爲:首先在服務端啓動一個ServerSocket來監聽網絡請求,客戶端啓動Socket發起網絡請求,默認情況下SeverSocket會建立一個線程來處理此請求,如果服務端沒有線程可用,客戶端則會阻塞等待或遭到拒絕。服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理。大致結構如下:

aJ2i9K.png

如果要讓 BIO 通信模型能夠同時處理多個客戶端請求,就必須使用多線程(主要原因是 socket.accept()socket.read()socket.write() 涉及的三個主要函數都是同步阻塞的),也就是說它在接收到客戶端連接請求之後爲每個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,線程銷燬。這就是典型的 一請求一應答通信模型 。我們可以設想一下如果這個連接不做任何事情的話就會造成不必要的線程開銷,不過可以通過線程池機制改善,線程池還可以讓線程的創建和回收成本相對較低。使用線程池機制改善後的 BIO 模型圖如下:

aJ2NEn.png

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模型中的SocketServerSocket相對應的SocketChannelServerSocketChannel兩種不同的套接字通道實現,如下圖結構所示。這裏涉及的Reactor設計模式、多路複用SelectorBuffer等暫時不用管,後面會講到。

aJ2a40.png

NIO 方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,併發局 限於應用中,編程複雜,JDK1.4 開始支持。同時,NIO和普通IO的區別主要可以從存儲數據的載體、是否阻塞等來區分:

NIO和普通IO區別.png

Java AIO

NIO 不同,當進行讀寫操作時,只須直接調用 API 的 readwrite 方法即可。這兩種方法均爲異步的,對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入 read 方 法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將 write 方法傳遞的流寫入完畢時,操作系統主動通知應用程序。即可以理解爲,read/write 方法都是異步的,完成後會主動調用回調函數。在 JDK7 中,提供了異步文件通道和異步套接字通道的實現,這部分內容被稱作 NIO.

AIO 方式使用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用 OS 參與併發操作,編程比較複雜,JDK7 開始支持。

目前來說 AIO 的應用還不是很廣泛,Netty 之前也嘗試使用過 AIO,不過又放棄了。

二、NIO核心組件介紹

1. Channel

NIO中,基本所有的IO操作都是從Channel開始的,Channel通過Buffer(緩衝區)進行讀寫操作。

read()表示讀取通道中數據到緩衝區,write()表示把緩衝區數據寫入到通道。

Channel和Buffer互相操作.png

Channel有好多實現類,這裏有三個最常用:

  • SocketChannel:一個客戶端發起TCP連接的Channel
  • ServerSocketChannel:一個服務端監聽新連接的TCP Channel,對於每一個新的Client連接,都會建立一個對應的SocketChannel
  • FileChannel:從文件中讀寫數據

其中SocketChannelServerSocketChannel是網絡編程中最常用的,一會在最後的示例代碼中會有講解到具體用法。

2. Buffer

概念

Buffer也被成爲內存緩衝區,本質上就是內存中的一塊,我們可以將數據寫入這塊內存,之後從這塊內存中讀取數據。也可以將這塊內存封裝成NIO Buffer對象,並提供一組常用的方法,方便我們對該塊內存進行讀寫操作。

Bufferjava.nio中被定義爲抽象類:

Buffer結構體系.png

我們可以將Buffer理解爲一個數組的封裝,我們最常用的ByteBuffer對應的數據結構就是byte[]

屬性

Buffer中有4個非常重要的屬性:capacity、limit、position、mark

Buffer中基本屬性.png

  • capacity屬性:容量,Buffer能夠容納的數據元素的最大值,在Buffer初始化創建的時候被賦值,而且不能被修改。

capacity.png

上圖中,初始化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,代表下一次讀取的位置。

NIO屬性概念.png

從上圖就能很清晰看出,讀寫模式下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中。

Channel和Buffer互相操作.png

其他常見方法
  • 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到變量markz中,然後通過reset()方法恢復當前positionmark,實現代碼很簡單,如下:
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;
}

常用的讀寫方法可以用一張圖總結一下:

Buffer讀寫操作.png

3. Selector

概念

Selector是NIO中最爲重要的組件之一,我們常常說的多路複用器就是指的Selector組件。 Selector組件用於輪詢一個或多個NIO Channel的狀態是否處於可讀、可寫。通過輪詢的機制就可以管理多個Channel,也就是說可以管理多個網絡連接。

Selector原理圖.png

輪詢機制

  1. 首先,需要將Channel註冊到Selector上,這樣Selector才知道需要管理哪些Channel
  2. 接着Selector會不斷輪詢其上註冊的Channel,如果某個Channel發生了讀或寫的時間,這個Channel就會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒的Channel集合,進行後續的IO操作。

輪詢機制.png

屬性操作

  1. 創建Selector

通過open()方法,我們可以創建一個Selector對象。

Selector selector = Selector.open();
  1. 註冊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集合中。

三、代碼示例

前面鋪墊了這麼多,主要是想讓大家能夠看懂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]收到響應:收到

四、總結

回顧一下使用 NIO 開發服務端程序的步驟:

  1. 創建 ServerSocketChannel 和業務處理線程池。
  2. 綁定監聽端口,並配置爲非阻塞模式。
  3. 創建 Selector,將之前創建的 ServerSocketChannel 註冊到 Selector 上,監聽 SelectionKey.OP_ACCEPT
  4. 循環執行 Selector.select()`` 方法,輪詢就緒的 Channel`。
  5. 輪詢就緒的 Channel 時,如果是處於 OP_ACCEPT 狀態,說明是新的客戶端接入,調用 ServerSocketChannel.accept 接收新的客戶端。
  6. 設置新接入的 SocketChannel 爲非阻塞模式,並註冊到 Selector 上,監聽 OP_READ
  7. 如果輪詢的 Channel 狀態是 OP_READ,說明有新的就緒數據包需要讀取,則構造 ByteBuffer 對象,讀取數據。

那從這些步驟中基本知道開發者需要熟悉的知識點有:

  1. jdk-nio提供的幾個關鍵類:Selector , SocketChannel , ServerSocketChannel , FileChannel ,ByteBuffer ,SelectionKey
  2. 需要知道網絡知識:tcp粘包拆包 、網絡閃斷、包體溢出及重複發送等
  3. 需要知道linux底層實現,如何正確的關閉channel,如何退出註銷selector ,如何避免selector太過於頻繁
  4. 需要知道如何讓client端獲得server端的返回值,然後才返回給前端,需要如何等待或在怎樣作熔斷機制
  5. 需要知道對象序列化,及序列化算法
  6. 省略等等,因爲我已經有點不舒服了,作爲程序員的我習慣了舒舒服服簡單的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 中還沒有解決

NettyNIO 的基礎上 , 封裝了 Java 原生的 NIO API , 解決了上述哪些問題呢 ?

相比 Java NIO,使用 Netty 開發程序,都簡化了哪些步驟呢?...等等這系列問題也都是我們要問的問題。不過因爲這篇只是介紹NIO相關知識,沒有介紹Netty API的使用,所以介紹Netty API使用簡單開發門檻低等優點有點站不住腳。那麼就留到後面跟大家一起開啓Netty學習之旅,探討人人說好的Netty到底是不是江湖傳言的那麼好。

一起期待後續的Netty之旅吧!

原創乾貨分享.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章