IO模型
IO 模型就是說用什麼樣的通道進行資料的傳送和接收,Java 共支援 3 種網路程式設計 IO 模式:
BIO,NIO,AIO
。
BIO(Blocking IO )
同步阻塞
模型,一個客戶端連線對應一個處理執行緒。
BIO程式碼示例:
// BIO 服務端程式碼import java。net。ServerSocket;import java。net。Socket;import java。util。logging。Handler; public class SocketServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(9000); while (true){ System。out。println(“等待連線”); // 阻塞連線 Socket clientSocket = serverSocket。accept(); System。out。println(“有客戶端連線。。。”); // 建立新執行緒執行 handle 方法 new Thread(new Runnable() { @Override public void run() { try { handle(clientSocket); } catch (Exception e) { e。printStackTrace(); } } })。start(); } } public static void handle(Socket clientSocket) throws Exception{ byte[] bytes = new byte[1024]; System。out。println(“準備read。。”); // 接收客戶端的資料,沒有資料可讀時就阻塞 int read = clientSocket。getInputStream()。read(bytes); System。out。println(“read 完畢。”); if (read !=-1){ System。out。println(“接收到客戶端資料:” + new String(bytes,0,read)); } clientSocket。getOutputStream()。write(“helloClient”。getBytes()); clientSocket。getOutputStream()。flush(); }}
// BIO 客戶端程式碼 import java。io。IOException;import java。net。Socket; public class SocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket(“localhost”, 9000); // 向服務端傳送資料 socket。getOutputStream()。write(“HelloServer”。getBytes()); socket。getOutputStream()。flush(); System。out。println(“向服務端傳送資料結束”); byte[] bytes = new byte[1024]; // 接收服務端回傳的資料 socket。getInputStream()。read(bytes); System。out。println(“接收到服務端的資料:” + new String(bytes)); socket。close(); }}
缺點:
從上面的程式碼我們可以看出來,BIO 程式碼中
連線事件和讀寫資料事件
都是
阻塞的
,所以這種模式的缺點非常的明顯。
1、如果我們連線完成以後,不做讀寫資料操作會導致執行緒阻塞,浪費資源。
2、如果沒來一個連線我們都需要啟動一個執行緒處理,那麼會導致伺服器執行緒太多,壓力太大,比如 C10K。
應用場景:
BIO 方式適用於
連線數目比較小且固定
的架構,這種方式對伺服器資源要求比較高,但是程式比較簡單。
NIO(Non Blocking IO)
同步非阻塞
模型,伺服器實現模式為一個執行緒可以處理多個請求連線,客戶端傳送的連線請求都會註冊到多路複用器(selector)上,多路複用器輪詢到連線有 IO 請求就進行處理,JDK1。4 開始引入。
// NIO 服務端程式碼(沒有引入多路複用器的程式碼) import java。net。InetSocketAddress;import java。nio。ByteBuffer;import java。nio。channels。ServerSocketChannel;import java。nio。channels。SocketChannel;import java。util。ArrayList;import java。util。Iterator;import java。util。List; public class NioServer { static List
缺點:
如果連線數太多的話,會有大量的無效遍歷,假如有 10000 個連線,其中只有 1000個 連線有寫資料,但是由於其他 9000 個連線並沒有斷開看我們還是每次輪詢遍歷一萬次,其中有 十分之一的遍歷都是無效的,這顯然是一個非常浪費資源的做法。
// NIO 服務端程式碼(引入多路複用器的程式碼) 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。security。Key;import java。util。Iterator;import java。util。Set; public class NioSelectorServer { public static void main(String[] args) throws Exception { // 建立 ServerSocketChannle ServerSocketChannel serverSocket = ServerSocketChannel。open(); serverSocket。bind(new InetSocketAddress(9000)); // 設定 ServerSocketChannel 為非阻塞 serverSocket。configureBlocking(false); // 開啟 Selector 處理 channel,即建立 epoll Selector selector = Selector。open(); // 把 ServerSocketChannel 註冊 selector 上,並且 select 對客戶端 accept 連線操作感興趣 serverSocket。register(selector, SelectionKey。OP_ACCEPT); System。out。println(“服務啟動”); while (true) { // 阻塞等待需要處理的事件發生 selector。select(); // 獲取 selector 中註冊的全部事件的 SelectionKey 例項 Set
上面程式碼是利用 NIO 一個執行緒處理所有請求,這種單個執行緒處理的方式肯定是存在問題的,例如現在有 10w 個請求中,有 1w 個連線進行讀寫資料,那麼 SelectionKey 就會有 1w 個請求,所以我們需要迴圈這 1w 個事件進行處理,比較費時間,如果這個時候再有連線進來,只能阻塞。
NIO 有三大核心元件:
Channel(通道),Buffer(緩衝區)Selector(多路複用器)
1、channel 類似流,每個 channel 對應一個 buffer 緩衝區,buffer 底層是個陣列。
2、channel 會註冊到 selector上,由 selector 根據 channel 的讀寫事件發生將其交由某個空閒的執行緒處理。
3、NIO 的 Buffer 和 channel 都是既可以讀又可以寫的。
NIO 底層在 JDK1。4 版本是用
linux 的核心函式 select() 或 poll()
來實現,跟上面的 NioServer 程式碼類似,
selector 每次都會輪詢
所有的 socktchannel 看下哪個 channel 有讀寫事件,有的話就處理,沒有就繼續遍歷,JDK1。5 開始引入了 epoll 基於事件響應機制來最佳化 NIO。
舉個例子:例如我們去酒吧喝酒,在吧檯坐下了 20 個人,中間一個服務員,select() 或者 poll() 模式就是,服務員每次都是詢問這個 20 個人是否需要喝酒,而 epoll 模型則是,20 個人誰需要喝酒誰就舉手,服務員每次只處理舉手的那幾個人即可。
NioSelectorServer 程式碼裡如下幾個方法非常重要,我們從 Hotspot 與 Linux 核心函式級別來理解下。
Selector。open() // 建立多路複用器socketChannel。register(selector, SelectionKey。OP_READ) // 將 channel 註冊到多路複用器上selector。select() // 阻塞等待需要處理的事件發生
總結:
NIO 整個呼叫流程就是 Java 呼叫了作業系統的核心函式來建立 Socket,獲取 Socket 檔案描述符,再建立一個 Selector 物件,對應作業系統的 Epoll 描述符,將獲取到的 Socket 連線的檔案描述符的事件繫結到 Selector 對應的檔案描述符上,進行事件的非同步通知,這樣就實現了使用一條執行緒,並且不需要太多的無效遍歷,將事件處理交給了作業系統核心(作業系統的終端程式),大大提高了效率。
Epoll 函式詳解
int epoll_create(int size);
建立一個 epoll 例項,並返回一個非負數作為檔案描述符,用於對 epoll 介面的所有後續呼叫。引數 size 代表可能會容納 size 個描述符,但 size 不是一個最大值,只是提示作業系統它的數量級,現在這個引數基本上已經棄用了。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
使用檔案描述符 epfd 引用 epoll 例項,對目標檔案描述符 fs 執行 op 操作。
引數 epfd 表示 epoll 對應的檔案描述符,引數 fd 表示 socket 對應的檔案描述符。
引數 op 有以下幾個值:
EPOLL_CTL_ADD:註冊新的 fd 到 epfd 中,並關聯事件 event;
EPOLL_CTL_MOD:修改已經註冊的 fd 的監聽事件;
EPOLL_CTL_DEL:從 epfd 中移除 fd,並且忽略掉繫結的 event,這時 event 可以為 null;
引數event是一個結構體
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
events 有很多可選值,這裡只舉例最常見的幾個:
EPOLLIN :表示對應的檔案描述符是可讀的;
EPOLLOUT:表示對應的檔案描述符是可寫的;
EPOLLERR:表示對應的檔案描述符發生了錯誤;
成功則返回 0,失敗返回 -1。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待檔案描述符 epfd 上的事件。
epfd 就是 Epoll 對應的檔案描述符,events 表示呼叫者所有可用事件的集合,maxevents 表示最大等到多少個事件就返回,timeout 是超時時間。
I/O 多路複用底層主要用 Linux 核心函式(select 、poll、epoll)來實現。
AIO模型
非同步非阻塞
模型,由
作業系統完成後回撥通知
服務端程式啟動執行緒去處理,一般適用於
連線數比較多且連線時間比較長
的應用。
應用場景
AIO 方式適用於
連線數多且連線比較長
(重操作)的架構,
JDK1.7
開始支援。
// AIO 服務端程式碼 import java。io。IOException;import java。net。InetSocketAddress;import java。nio。ByteBuffer;import java。nio。channels。AsynchronousChannel;import java。nio。channels。AsynchronousServerSocketChannel;import java。nio。channels。AsynchronousSocketChannel;import java。nio。channels。CompletionHandler; public class AIOServer { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel。open()。bind(new InetSocketAddress(9000)); serverChannel。accept(null, new CompletionHandler
// AIO 客戶端程式碼import java。net。InetSocketAddress;import java。nio。ByteBuffer;import java。nio。channels。AsynchronousSocketChannel; public class AIOClient { public static void main(String。。。 args) throws Exception { AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel。open(); socketChannel。connect(new InetSocketAddress(“127。0。0。1”, 9000))。get(); socketChannel。write(ByteBuffer。wrap(“HelloServer”。getBytes())); ByteBuffer buffer = ByteBuffer。allocate(512); Integer len = socketChannel。read(buffer)。get(); if (len != -1) { System。out。println(“客戶端收到資訊:” + new String(buffer。array(), 0, len)); } }}
為什麼 Netty 使用 NIO 而不是 AIO?
因為在 Linux 系統上,AIO 的底層實現扔使用 Epoll 模型,沒有很好的使用 AIO,因此在效能上沒有明顯的優勢,而且被 JDK 封裝了一層不容易再次進行深度最佳化,Linux 上 AIO 還不夠成熟。Netty 是非同步非阻塞框架,Netty在 NIO 上做了很多非同步封裝。
文章來源:阿里開發者_https://zhuanlan。zhihu。com/p/587641708