從BIO和NIO到Netty實踐
一、什麼是BIO
傳統的BIO模型使用是java.net包中API實現Socket通訊,而傳統的BIO通訊中阻塞共發現在兩個 地方。
服務端ServerSocker::accept
客戶端Socket資料讀寫
這種IO的弊端即是無法處理併發的客戶端請求,因此可以通過為每個客戶端單獨分配一個執行緒,則客戶的端的Socket資料讀寫不在阻塞,可以滿足併發的客戶端請求。
同樣的在高併發,大量客戶端連線造成大量執行緒,容易產生執行緒OOM,同時也有大量的執行緒上下文切換影響效能。
二、什麼是NIO
- 在JAVA中NIO是指new IO,是JDK為實現非阻塞IO實現的一套新API
- 在linux中NIO是指非阻塞的IO,主要與poll和epoll核心呼叫有關
JAVA的NIO一種重要的方法為configureBlocking,示例程式碼如下:
package com.zte.sunquan.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.List;
public class SocketServerNIO {
public static void main(String[] args) throws Exception {
List<SocketChannel> channels = new ArrayList<>();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);
//非阻塞
while (true) {
Thread.sleep(1000);
//不阻塞,但需要每次都問一下核心
SocketChannel clientSocket = ssc.accept();
if (clientSocket == null) {
System.out.println("No client....");
} else {
//將clientSocket保留
clientSocket.configureBlocking(false);
System.out.println("client connected in:" + clientSocket.socket().getPort());
channels.add(clientSocket);
}
ByteBuffer buffer = ByteBuffer.allocate(4096);
//不阻塞,但這裡每次也需要問一下核心,即使有些客戶端沒有事件
for (SocketChannel channel : channels) {
int num = channel.read(buffer);
if (num > 0) {
buffer.flip();
byte[] content=new byte[buffer.limit()];
buffer.get(content);
String s = new String(content);
System.out.println(channel.socket().getPort()+" Read client msg:"+s);
buffer.clear();
}
}
}
}
}
如上程式碼中一個執行緒負責了IO讀寫與連線建立,當然可以優雅一點的方法,即將連線建立與IO讀寫分執行緒處理,讓IO的讀寫不影響高併發下連線請求與建立。但上述程式碼仍有明顯的弊端。
for (SocketChannel channel : channels) {
int num = channel.read(buffer);
在上述程式碼中,每次迴圈都要與所有建立的客戶端進行一次read操作,涉及使用者究竟與核心空間的切換,考慮到一個連線數特別多背景下,一次可能只會有幾個連線有IO事件,如上的實現會造成大量的效能浪費。
那有沒有一種可能,讓核心主動告知我們哪些連線有IO事件,這樣應用精確地去指定的連線上進行IO事件的處理,而不是傻傻地每個連線read一遍?
三、多路複用器
多路複用器使用,可以解決第二節最後的問題,通過selector.select,核心只會將有事件的socket返回,避免了應用迴圈遍歷嘗試。
下面示例程式碼描述了使用JAVA中NIO的API實現的服務端程式碼
package com.zte.sdn.nio;
import java.io.IOException;
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.util.Iterator;
import java.util.Set;
/**
* 多路複用器示例程式碼
**/
public class NioServer {
private Selector startServer() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);
//開啟一個多路複用器
//poll系統呼叫:
//epoll系統呼叫:
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Start server at port:9090");
return selector;
}
public static void main(String[] args) throws IOException {
NioServer nioServer = new NioServer();
Selector selector = nioServer.startServer();
while (true) {
System.out.println("ask");
//使用select向核心詢問是否有事件(由於一開始只註冊了ServerSocket的OP_ACCEPT)
//所以第一次只判斷是否有連線事件
//後面由於註冊客戶端OP_READ,從面判斷是否有可讀事件
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
nioServer.handler(selectionKey, selector);
}
}
}
}
private void handler(SelectionKey selectionKey, Selector selector) throws IOException {
if (selectionKey.isAcceptable()) {
//一個連線事件
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
//再註冊進去
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(8192));
System.out.println("receive connect:" + client.getRemoteAddress());
} else if (selectionKey.isReadable()) {
//可讀事件
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = 0;
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
//講到內容寫回客戶端
client.write(buffer);
}
System.out.println("receive client msg:" + new String(buffer.array()));
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
}
}
}
在上述示例中,一個執行緒完成了服務端介面客戶端連線以及IO讀寫動作。思考上面程式設計思路的弊端是什麼?
考慮到一個IO的讀寫如何非常耗時,必然會影響客戶端建立連線併發效能,以及大量IO讀寫的效能。
自然地針對客戶端連線與IO讀寫可以分不同selector單獨處理,各司其職,所以改進的實現如下:
else if (selectionKey.isReadable()) {
executorService.submit(()->{
//可讀事件
try {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = 0;
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
//講到內容寫回客戶端
client.write(buffer);
}
System.out.println("receive client msg:" + new String(buffer.array()));
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
});
}
如上所示,實現了IO的處理非同步化。但上述實現雖然緩解了大量長時間IO帶來的效能問題,但不能從根本上解決,那有沒有辦法,將客戶端連線事件與IO事件完全分離開?當然如果IO讀取的資料業務處理比較耗時,則可以另起執行緒再進行非同步處理。
四、多Selector版本
程式碼:
package com.zte.sdn.nio;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class IOHandler extends Thread {
private Selector selector;
public IOHandler(Selector selector, String name) {
this.selector = selector;
this.setName(name);
}
@Override
public void run() {
while (true) {
try {
handler();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handler() throws IOException {
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
int read = 0;
while (true) {
try {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
//講到內容寫回客戶端
client.write(buffer);
}
System.out.println(Thread.currentThread().getName() + " receive client msg:" + new String(buffer.array()));
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
} catch (Exception e) {
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
break;
}
}
}
}
}
}
}
MultiNioServer
package com.zte.sdn.nio;
import java.io.IOException;
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.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多路複用器示例程式碼
**/
public class MultiNioServer {
private ExecutorService executorService = Executors.newFixedThreadPool(5);
private Selector startServer() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);
//開啟一個多路複用器
//poll系統呼叫:
//epoll系統呼叫:
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Start server at port:9090");
return selector;
}
public static void main(String[] args) throws IOException {
MultiNioServer nioServer = new MultiNioServer();
Selector selector = nioServer.startServer();
Selector selector1 = Selector.open();
Selector selector2 = Selector.open();
Selector[] selectors = new Selector[]{selector1, selector2};
new IOHandler(selector1, "A").start();
new IOHandler(selector2, "B").start();
int i = 0;
while (true) {
while (selector.select(10) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
//一個連線事件
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
//迴圈註冊至另外的Selector
client.register(selectors[i++ % 2], SelectionKey.OP_READ, ByteBuffer.allocate(8192));
System.out.println("receive connect:" + client.getRemoteAddress());
}
}
}
}
}
}
五、IO總結
傳統的NIO(不使用多路利用器),雖然解決了IO阻塞問題,但需要使用者程式遍歷地向核心詢問所有客戶端;
當使用了多路複用器(使用select/poll),每次迴圈需要將客戶端列表傳遞給核心,由核心遍歷將有事件的fd返回,避免使用者態與核心態的頻繁切換。
但使用select/poll仍然存在弊端,即每次迴圈都要進行fd列表的傳遞,如何能夠避免每次迴圈向核心傳遞fd列表?
思考:如核心能提前申請一塊空間儲存(紅黑樹)事件控制程式碼,在有客戶端連線上,則記錄在該空間,如此客戶端程式詢問是否事件時,則只需簡單問一句,而不需要每次都傳遞fd列表。
這說的其實epoll核心呼叫能解決的。
我們老師去教室收作業進行批改的場景為例類比上述實現:
IO型別 | 說明 | 弊端 |
---|---|---|
BIO | 老師來到教室,依次詢問每個學生寫了作業沒有,寫了則批改,沒寫則需等他寫好再批改,期間有同學入學畢業則需要等老師忙完這一輪 | 兩處阻塞 |
NIO | 老師來到教室,依次詢問每個學生寫了作業沒有,寫了則批改,沒寫則直接下一個,期間有同學入學畢業則需要等老師忙完這一輪,相對較快 | 依次詢問,沒寫作業的也要問 |
NIO-select/poll | 老師每次來到教室,拿一個名單貼到教室黑板,並告知在名單上同學且完成作業,報給我,後序老師直接拿到報名同學作業,批改即可 | 不同於每個同學依次詢問,但還要每次準備名單 |
NIO-epoll | 開班時,則在教室黑板貼上人員名單,有新同學加入畢業,則增加刪除,老師每次來到教室,不用準確名單,只需要告知在名單上同學且完成作業,報給我,後序老師直接拿到報名同學作業,批改即可 | 解決上述所有弊端 |
ps.select的fd有1024的數量約束,poll無此限制
六、Strace分析
七、Netty的執行緒模型
Netty擁有兩個NIO執行緒池,分別是bossGroup和workerGroup,前者處理新建連線請求,然後將新建立的連線輪詢交給workerGroup中的其中一個NioEventLoop來處理,後續該連線上的讀寫操作都是由同一個NioEventLoop來處理。注意,雖然bossGroup也能指定多個NioEventLoop(一個NioEventLoop對應一個執行緒),但是預設情況下只會有一個執行緒,因為一般情況下應用程式只會使用一個對外監聽埠。
為何不能使用多執行緒來監聽同一個對外埠麼,即多執行緒epoll_wait到同一個epoll例項上?
這裡會引來驚群的問題和epoll設定的是LT模式
現代linux中,多個socker同時監聽同一個埠也是可行的,nginx 1.9.1也支援這一行為。linux 3.9以上核心支援SO_REUSEPORT選項,允許多個socker bind/listen在同一埠上。這樣,多個程式可以各自申請socker監聽同一埠,當連線事件來臨時,核心做負載均衡,喚醒監聽的其中一個程式來處理,reuseport機制有效的解決了epoll驚群問題
單執行緒模型
Reactor 單執行緒模型,是指所有的 I/O 操作都在同一個 NIO 執行緒上面完成的,此時NIO執行緒職責包括:接收新建連線請求、讀寫操作等。
多執行緒模型
Rector 多執行緒模型與單執行緒模型最大的區別就是有一組 NIO 執行緒來處理連線讀寫操作,一個NIO執行緒處理Accept。一個NIO執行緒可以處理多個連線事件,一個連線的事件只能屬於一個NIO執行緒
主從模型
主從 Reactor 執行緒模型的特點是:服務端用於接收客戶端連線的不再是一個單獨的 NIO 執行緒,而是一個獨立的 NIO 執行緒池。Acceptor 接收到客戶端 TCP連線請求並處理完成後(可能包含接入認證等),將新建立的 SocketChannel注 冊 到 I/O 線 程 池(sub reactor 線 程 池)的某個I/O執行緒上, 由它負責SocketChannel 的讀寫和編解碼工作。Acceptor 執行緒池僅僅用於客戶端的登入、握手和安全認證,一旦鏈路建立成功,就將鏈路註冊到後端 subReactor 執行緒池的 I/O 執行緒上,由 I/O 執行緒負責後續的 I/O 操作。
相關文章
- 從 BIO、NIO 到 Netty【前置知識點】Netty
- Netty-BIO、NIO、AIO、零複製-2NettyAI
- 網路程式設計NIO:BIO和NIO程式設計
- Java雜記10—BIO,BIO和NIO的區別Java
- BIO、NIO、AIOAI
- NIO、BIO、Selector
- BIO到NIO原始碼的一些事兒之BIO原始碼
- NIO、BIO、AIO 與 PHP 實現AIPHP
- Java BIO,NIO,AIOJavaAI
- From BIO to NIO series —— BIO source code interpretation
- BIO到NIO原始碼的一些事兒之NIO 上原始碼
- BIO到NIO原始碼的一些事兒之NIO 中原始碼
- Java IO學習筆記五:BIO到NIOJava筆記
- netty系列之:NIO和netty詳解Netty
- From BIO to NIO —— NIO source code interpretation 1
- java BIO、NIO學習Java
- BIO,NIO,AIO概覽AI
- NIO、BIO、AIO區別AI
- BIO到NIO原始碼的一些事兒之NIO 下 之 Selector原始碼
- java BIO/NIO/AIO 學習JavaAI
- BIO、NIO、AIO的區別AI
- BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上原始碼
- BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 下原始碼
- Netty - 眼熟NIONetty
- BIO、NIO、多路複用IO、AIOAI
- Netty FastThreadLocal 實踐NettyASTthread
- Apache Tomcat 7 Configuration BIO NIO AIO APR ThreadPoolApacheTomcatAIthread
- Java核心(五)深入理解BIO、NIO、AIOJavaAI
- nio aio netty區別AINetty
- 阿里面試題BIO和NIO數量問題附答案和程式碼阿里面試題
- BIO、NIO、AIO區別(看不懂你打我)AI
- GraphQL 從入門到實踐
- Netty原始碼分析--NIO(一)Netty原始碼
- 三分鐘秒懂BIO/NIO/AIO區別?AI
- DevOps 從理論到實踐指南dev
- RecyclerView從認識到實踐(1)View
- Netty從入門到禿頭: websocketNettyWeb
- Git 和 GitHub:從入門到實踐2 Git 和 GitHub 基礎配置Github