Netty學習筆記(Reactor線程模型)
NIO selector 多路複用Reactor線程模型
在文章《Scalable IO in Java》中,介紹了使用NIO與Reactor模式相結合來實現高伸縮性網絡的模型。在這個模型中,mianReactor負責處理client的請求;subReactor可以有多個,每個subReactor都會在一個獨立線程中執行。
模型實現部分代碼講解
創建mainReactor和subReactor
/**
* 初始化線程組
*/
private void newGroup() throws IOException {
// 創建IO線程,負責處理客戶端連接以後socketChannel的IO讀寫
for (int i = 0; i < subReactorThreads.length; i++) {
subReactorThreads[i] = new ReactorThread() {
@Override
public void handler(SelectableChannel channel) throws IOException {
// work線程只負責處理IO處理,不處理accept事件
SocketChannel ch = (SocketChannel) channel;
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (ch.isOpen() && ch.read(requestBuffer) != -1) {
// 長連接情況下,需要手動判斷數據有沒有讀取結束 (此處做一個簡單的判斷: 超過0字節就認爲請求結束了)
if (requestBuffer.position() > 0) break;
}
if (requestBuffer.position() == 0) return; // 如果沒數據了, 則不繼續後面的處理
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
System.out.println(Thread.currentThread().getName() + "收到數據,來自:" + ch.getRemoteAddress());
// TODO 業務操作 數據庫、接口...
workPool.submit(() -> {
});
// 響應結果 200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
ch.write(buffer);
}
}
};
}
// 創建mainReactor線程, 只負責處理serverSocketChannel
for (int i = 0; i < mainReactorThreads.length; i++) {
mainReactorThreads[i] = new ReactorThread() {
AtomicInteger incr = new AtomicInteger(0);
@Override
public void handler(SelectableChannel channel) throws Exception {
// 只做請求分發,不做具體的數據讀取
ServerSocketChannel ch = (ServerSocketChannel) channel;
SocketChannel socketChannel = ch.accept();
socketChannel.configureBlocking(false);
// 收到連接建立的通知之後,分發給I/O線程繼續去讀取數據
int index = incr.getAndIncrement() % subReactorThreads.length;
ReactorThread workEventLoop = subReactorThreads[index];
workEventLoop.doStart();
SelectionKey selectionKey = workEventLoop.register(socketChannel);
selectionKey.interestOps(SelectionKey.OP_READ);
System.out.println(Thread.currentThread().getName() + "收到新連接 : " + socketChannel.getRemoteAddress());
}
};
}
}
ReactorThread類中封裝了selector.select()等事件輪詢的代碼,上面的代碼中創建了mainReactor線程和subReactor線程, mainReactor線程只負責處理serverSocketChannel,subReactor線程負責IO處理。當mainReactor收到連接建立的通知之後,分發給I/O線程繼續去讀取數據。
創建serverSocketChannel,註冊到mainReactor線程上的selector上
/**
* 初始化channel,並且綁定一個eventLoop線程
*
* @throws IOException IO異常
*/
private void initAndRegister() throws Exception {
// 1、 創建ServerSocketChannel
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 2、 將serverSocketChannel註冊到selector
int index = new Random().nextInt(mainReactorThreads.length);
mainReactorThreads[index].doStart();
SelectionKey selectionKey = mainReactorThreads[index].register(serverSocketChannel);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
}
Netty線程模型
爲了讓NIO處理更好地利用多線程特性,Netty實現了Reactor線程模型。
Netty模塊組件
NioEventLoopGroup
NioEventLoopGroup,主要管理 EventLoop 的生命週期,可以理解爲一個線程池,內部維護了一組線程,每個線程(NioEventLoop)負責處理多個 Channel 上的事件,而一個 Channel 只對應於一個線程。
NioEventLoop
NioEventLoop 中維護了一個線程和任務隊列,支持異步提交執行任務,線程啓動時會調用 NioEventLoop 的 run 方法,執行 I/O 任務和非 I/O 任務:
I/O 任務,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法觸發。
非 IO 任務,添加到 taskQueue 中的任務,如 register0、bind0 等任務,由 runAllTasks 方法觸發。
Selector
Netty 基於 Selector 對象實現 I/O 多路複用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件。
當向一個 Selector 中註冊 Channel 後,Selector 內部的機制就可以自動不斷地查詢(Select) 這些註冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel 。
Channel
Netty的Channel可以理解爲對NIO中Channel的增強和拓展,增加了很多屬性和方法。
Netty工作原理
Netty服務端典型代碼如下
public static void main(String[] args) throws Exception {
// Configure the server.
// 創建EventLoopGroup accept線程組 NioEventLoop
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 創建EventLoopGroup I/O線程組
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
// 服務端啓動引導工具類
ServerBootstrap b = new ServerBootstrap();
// 配置服務端處理的reactor線程組以及服務端的其他配置
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.DEBUG)).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
// 通過bind啓動服務
ChannelFuture f = b.bind(PORT).sync();
// 阻塞主線程,知道網絡服務被關閉
f.channel().closeFuture().sync();
} finally {
// 關閉線程組
bossGroup.shutdownGracefully();
workerGroup2.shutdownGracefully();
}
}
服務端代碼基本過程描述如下:
1)初始化創建 2 個 NioEventLoopGroup:其中 boosGroup 用於 accept 連接建立事件並分發請求,workerGroup 用於處理 I/O 讀寫事件和業務邏輯。
2)基於 ServerBootstrap(服務端啓動引導類):配置 EventLoopGroup、Channel 類型,連接參數、配置入站、出站事件 handler。
3)綁定端口:開始工作。
結合上面介紹的 Netty Reactor 模型,介紹服務端 Netty 的工作架構圖如下:
服務端包含 1 個 Boss NioEventLoopGroup(bossGroup) 和 1 個 Worker NioEventLoopGroup(workGroup)。
NioEventLoopGroup 相當於 1 個事件循環組,這個組裏包含多個事件循環 NioEventLoop,每個 NioEventLoop 包含 1 個 Selector 和 1 個事件循環線程。
每個 Boss NioEventLoop 循環執行的任務包含 3 步:
1)輪詢 Accept 事件;
2)處理 Accept I/O 事件,與 Client 建立連接,生成 NioSocketChannel,並將 NioSocketChannel 註冊到某個 Worker NioEventLoop 的 Selector 上;
3)處理任務隊列中的任務,runAllTasks。任務隊列中的任務包括用戶調用 eventloop.execute 或 schedule 執行的任務,或者其他線程提交到該 eventloop 的任務。
每個 Worker NioEventLoop 循環執行的任務包含 3 步:
1)輪詢 Read、Write 事件;
2)處理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可讀、可寫事件發生時進行處理;
3)處理任務隊列中的任務,runAllTasks。
個人小結
Netty實現了Reactor線程模型,Netty服務端包含 1 個 bossGroup 和 1 個 workGroup,其中 boosGroup 中的 NioEventLoop 負責 accept 連接建立事件並分發請求,workerGroup 中的多個 NioEventLoop 用於處理 I/O 讀寫事件和業務邏輯。
參考資料
[1].Scalable IO in Java http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
[2].Netty源碼解讀(四)Netty與Reactor模式 https://yq.aliyun.com/articles/25425?spm=a2c4e.11153940.0.0.34ee5e71hGNfnN
[3].Netty 4.x學習(三):線程模型詳解 http://www.52im.net/thread-98-1-1.html