高清思維導圖原件(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)客戶端和服務端通過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
,不過又放棄了。
二、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集合中。
三、代碼示例
前面鋪墊了這么多,主要是想讓大家能夠看懂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
開發服務端程序的步驟:
- 創建
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
之旅吧!