這邊博客分兩個部分,先介紹下IO線程模型,然後介紹下Netty的模型加上一個簡單的demo
1. IO線程模型的介紹
IO線程模型分爲三大類
1.傳統阻塞式模型 2.Reactor模型(反應堆模型) 3.Proactor模型(前攝器)
1.1 傳統阻塞模型
一個連接佔用一個線程,當大量併發的時候會造成資源的浪費,而且連接建立後,容易阻塞的讀或者寫的狀態
1.2 Reactor模型
Reactor模式是基於事件驅動開發的,核心組成部分包括Reactor和線程池,其中Reactor負責監聽和分配事件,線程池負責處理事件,而根據Reactor的數量和線程池的數量,又將Reactor分爲三種模型:
單線程模型 (單Reactor單線程)
多線程模型 (單Reactor多線程)
主從多線程模型 (多Reactor多線程)
單線程模型
好比一個接待員,一個服務員
1. select是I/O複用模型介紹的標準網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求 2. Reactor對象通過select監控客戶端請求事件,收到事件後通過dispatch進行分發 3. 如果是建立連接請求事件,則由Acceptor通過accept處理連接請求,然後創建一個Handler對象處理連接完成後的後續業務處理 4. 如果不是建立連接事件,則Reactor會分發調用連接對應的Handler來響應 5. Handler會完成 read -> 業務處理 -> send 的完整業務流程
結合實例:服務器端用一個線程通過多路複用搞定所有的IO操作(包括連接,讀、寫等),編碼簡單清晰明瞭,但是如果客戶端連接數量較多,將無法支撐。
優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成
缺點:性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障
場景:客戶端有限,業務處理快,比如redis
多線程模型
好比就是一個接待員,多個服務員
1. Reactor對象通過select監控客戶端請求事件, 收到事件後,通過dispatch進行分發 2. 如果建立連接請求, 則由Acceptor通過accept處理連接請求, 然後創建一個Handler對象處理完成連接後的各種事件 3. 如果不是連接請求,則由Reactor分發調用連接對應的Handler來處理4. Handler只負責響應事件,不做具體的業務處理, 通過read讀取數據後,會分發給後面的worker線程池的某個線程處理業務 5. worker線程池會分配獨立線程完成真正的業務,並將結果返回給handler 6. handler收到響應後,通過send將結果返回給client
單Reactor承當所有事件的監聽和響應,而當我們的服務端遇到大量的客戶端同時進行連接,或者在請求連接時執行一些耗時操作,比如身份認證,權限檢查等,這種瞬時的高併發就容易成爲性能瓶頸
下圖是對應的業務邏輯處理部分,將任務交給線程池處理, 資料來自:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
優點:可以充分的利用多核 cpu 的處理能力
缺點:多線程數據共享和訪問比較複雜, Reactor處理所有的事件的監聽和響應,在單線程運行, 在高併發場景容易出現性能瓶頸
主從多線程模型(主流的方式)
好比多個接待員,多個服務員
1. Reactor主線程MainReactor對象通過select監聽連接事件, 收到事件後,通過Acceptor處理連接事件 2. 當Acceptor處理連接事件後,MainReactor將連接分配給SubReactor 3. Subreactor將連接加入到連接隊列進行監聽,並創建Handler進行各種事件處理 4. 當有新事件發生時, Subreactor就會調用對應的Handler處理 5. Handler通過read讀取數據,分發給後面的Worker線程處理 6. worker線程池分配獨立的worker線程進行業務處理,並返回結果7. Handler收到響應的結果後,再通過send將結果返回給client 8. Reactor主線程可以對應多個Reactor子線程, 即MainRecator可以關聯多個SubReactor
優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成後續的業務處理。父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
缺點:編程複雜度較高
Nginx / Netty / Memcached都是使用的這個模式
1.3 Proactor模型
Reactor先建立連接,等待事件發生,然後讓實現準備好的handler去處理,後者來實際讀寫,它是同步非阻塞的線程模型.如果IO改爲異步交給操作系統來完成,則可以進一步提高效率,這就是異步網絡模型 Proactor
Reactor讀寫在Handler裏面完成, 而Proactor讀寫在內核中完成
編程複雜性,由於異步操作流程的事件的初始化和事件完成在時間和空間上都是相互分離的,因此開發異步應用程序更加複雜。應用程序還可能因爲反向的流控而變得更加難以Debug;
內存使用,緩衝區在讀或寫操作的時間段內必須保持住,可能造成持續的不確定性,並且每個併發操作都要求有獨立的緩存,相比Reactor模式,在Socket已經準備好讀或寫前,是不要求開闢緩存的;
2. Netty模型介紹
1. BossGroup線程維護Selector, 只關注Accecpt 2. 當接收到Accept事件,獲取到對應的SocketChannel, 封裝成NIOScoketChannel並註冊到Worker線程(事件循環), 並進行維護 3. 當Worker線程監聽到selector中通道發生自己感興趣的事件後,就進行處理(就由handler), 注意handler已經加入到通道
1. Netty抽象出兩組線程池BossGroup專門負責接收客戶端的連接,WorkerGroup專門負責網絡的讀寫 2. BossGroup和WorkerGroup類型都是NioEventLoopGroup 3. NioEventLoopGroup相當於一個事件循環組, 這個組中含有多個事件循環 ,每一個事件循環是NioEventLoop 4. NioEventLoop表示一個不斷循環的執行處理任務的線程, 每個NioEventLoop都有一個selector , 用於監聽綁定在其上的socket的網絡通訊 5. NioEventLoopGroup可以有多個線程, 即可以含有多個NioEventLoop 6. 每個Boss NioEventLoop循環執行的步驟有3步 - 輪詢accept事件 - 處理accept事件 , 與client建立連接 , 生成NioScocketChannel , 並將其註冊到某個worker NioEventLoop上的selector
- 處理任務隊列的任務 , 即runAllTasks 7. 每個Worker NioEventLoop循環執行的步驟 - 輪詢read / write事件 - 處理i/o事件, 即read / write事件,在對應NioScocketChannel處理 - 處理任務隊列的任務 , 即runAllTasks 8) 每個Worker NioEventLoop處理業務時,會使用pipeline(管道), pipeline中包含了channel , 即通過pipeline可以獲取到對應通道, 管道中維護了很多的處理器
3. 基於Netty的demo
3.1 TCP簡單的demo
簡單介紹下Netty的NioEventLoopGroup的結構:
1. Netty抽象出兩組線程池,BossGroup專門負責接收客戶端連接,WorkerGroup專門負責網絡讀寫操作
2. NioEventLoop表示一個不斷循環執行處理任務的線程,每個NioEventLoop都有一個selector,用於監聽綁定在其上的socket網絡通道
3. NioEventLoop 內部採用串行化設計,從消息的讀取->解碼->處理->編碼->發送,始終由IO線程NioEventLoop負責
- NioEventLoopGroup下包含多個NioEventLoop
- 每個NioEventLoop中包含有一個Selector,一個taskQueue
- 每個NioEventLoop的Selector上可以註冊監聽多個NioChannel
- 每個NioChannel只會綁定在唯一的NioEventLoop 上
- 每個NioChannel都綁定有一個自己的ChannelPipeline
BrianServer.java
package com.kawa.io.netty.simple; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import lombok.extern.slf4j.Slf4j; @Slf4j public class BrianServer { public static void main(String[] args) { // create the bossGroup and workerGroup // bossGroup only handle the connect request EventLoopGroup bossGroup = new NioEventLoopGroup(1); // workerGroup handle the business request, thread pool size = cup * 2 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // ServerBootstrap application start class ServerBootstrap bootstrap = new ServerBootstrap(); // set the parent group and child group bootstrap.group(bossGroup, workerGroup) // NioServerSocketChannel use as server channel .channel(NioServerSocketChannel.class) // set .option(ChannelOption.SO_BACKLOG, 128) // set keep alive connect .childOption(ChannelOption.SO_KEEPALIVE, true) // create a SocketChannel // set Handler to pipeline of workerGroup's EventLoop .childHandler(new ChannelInitializer<SocketChannel>() { // set Handler for pipeline @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new BrianServerHandler()); } }); // start the server bind port and sync create ChannelFuture ChannelFuture cf = bootstrap.bind(9001).sync(); log.info("---------- BrianServer is ready ----------"); // listen the close channel cf.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
BrianServerHandler.java 服務端自定義Handler
package com.kawa.io.netty.simple; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.CharsetUtil; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @Slf4j public class BrianServerHandler extends ChannelInboundHandlerAdapter { private ConcurrentHashMap<String, String> storage = new ConcurrentHashMap<>(); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info(">>>>>>>>>> channelActive current thread: {}", Thread.currentThread().getName()); ctx.channel().eventLoop().execute(()-> { // save to storage storage.put(ctx.channel().remoteAddress().toString(), "Y"); }); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { Channel channel = ctx.channel(); // convert the msg to ByteBuf ByteBuf buf = (ByteBuf) msg; log.info(">>>>>>>>>> get msg from client:{}, msg:{}", channel.remoteAddress(), getMsg(buf)); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { sendMsg(ctx," get the msg"); } // when hit the Exception can close the related channel @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error(">>>>>>>>>> BrianServerHandler error: {}", cause.getMessage()); ctx.channel().eventLoop().schedule(()-> { // update the storage storage.put(ctx.channel().remoteAddress().toString(), "N"); }, 20L, TimeUnit.MILLISECONDS); ctx.close(); } private void sendMsg(ChannelHandlerContext ctx, String message){ ctx.writeAndFlush(Unpooled.copiedBuffer(message, CharsetUtil.UTF_8)); } private String getMsg(ByteBuf buf){ return buf.toString(CharsetUtil.UTF_8); } }
BrianClient.java
package com.kawa.io.netty.simple; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import lombok.extern.slf4j.Slf4j; @Slf4j public class BrianClient { public static void main(String[] args) { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { // add BrianClientHandler ch.pipeline().addLast(new BrianClientHandler()); } }); // start the client and listen the server port 9001 ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9001).sync(); log.info("---------- BrianClient is ready ----------"); // listen the close channel channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { group.shutdownGracefully(); } } }
BrianClientHandler.java 客戶端自定義Handler
package com.kawa.io.netty.simple; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.CharsetUtil; import lombok.extern.slf4j.Slf4j; @Slf4j public class BrianClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info(">>>>>>>>>> BrianClientHandler channelActive "); sendMsg(ctx, "BrianClientHandler channelActive"); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; log.info(">>>>>>>>>> server: {}{}", ctx.channel().remoteAddress(), getMsg(buf)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error(">>>>>>>>>> BrianClientHandler error: {}", cause.getMessage()); ctx.close(); } private void sendMsg(ChannelHandlerContext ctx, String message){ ctx.writeAndFlush(Unpooled.copiedBuffer(message, CharsetUtil.UTF_8)); } private String getMsg(ByteBuf buf){ return buf.toString(CharsetUtil.UTF_8); } }
整個demo很簡單,就是開啓服務器端等帶客戶端連接和打印客戶端的發送的消息,以及響應客戶端,所以這裏就不截圖測試日誌了
3.2 HTTP簡單的demo
在寫HTTP demo前先講講Netty的異步模型
3.2.1 Netty的異步模型
1. 異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的組件在完成後,通過狀態、通知和回調來通知調用者 2. Netty中的I/O操作是異步的,包括Bind、Write、Connect等操作會簡單的返回一個ChannelFuture 3. 調用者並不能立刻獲得結果,而是通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果 4. Netty的異步模型是建立在future和callback的之上的。callback就是回調。重點說Future它的核心思想是:
假設一個方法fun,計算過程可能非常耗時,等待fun返回顯然不合適。那麼可以在調用fun的時候,立馬返回一個Future,
後續可以通過Future去監控方法fun的處理過程(即:Future-Listener機制)
Future說明
1. 表示異步的執行結果, 可以通過它提供的方法來檢測執行是否完成,比如檢索計算等 2. ChannelFuture是一個接口 : public interface ChannelFuture extends Future<Void> 我們可以添加監聽器,當監聽的事件發生時,就會通知到監聽器
在使用Netty進行編程時,攔截操作和轉換出入站數據只需要您提供callback或利用future即可。這使得鏈式操作簡單、高效, 並有利於編寫可重用的、通用的代碼。Netty框架的目標就是讓你的業務邏輯從網絡基礎應用編碼中分離出來、解脫出來
鏈式操作示意圖:
Future-Listener機制
1.當Future對象剛剛創建時,處於非完成狀態,調用者可以通過返回的ChannelFuture來獲取操作執行的狀態,註冊監聽函數來執行完成後的操作 2.常見有如下操作 通過isDone方法來判斷當前操作是否完成 通過isSuccess方法來判斷已完成的當前操作是否成功 通過getCause方法來獲取已完成的當前操作失敗的原因 通過isCancelled方法來判斷已完成的當前操作是否被取消 通過addListener方法來註冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;如果Future對象已完成,則通知指定的監聽器
相比傳統阻塞IO,執行IO操作後線程會被阻塞住,直到操作完成;異步處理的好處是不會造成線程阻塞,線程在IO操作期間可以執行別的程序,在高併發情形下會更穩定和更高的吞吐量
3.2.2 HTTP demo
BrianHttpServer.java
package com.kawa.io.netty.http; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import lombok.extern.slf4j.Slf4j; @Slf4j public class BrianHttpServer { public static void main(String[] args) { EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new BrianServerInitializer()); ChannelFuture channelFuture = serverBootstrap.bind(9999).sync(); log.info("---------- BrianHttpServer is ready ----------"); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
BrianServerInitializer.java 設置handler
package com.kawa.io.netty.http; import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpServerCodec; import lombok.extern.slf4j.Slf4j; @Slf4j public class BrianServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { // add handler ch.pipeline() // add Netty Encoder-Decoder .addLast("httpServerCodec", new HttpServerCodec()) // add customized handler .addLast("brianHttpServerHandler", new BrianHttpServerHandler()); } }
BrianHttpServerHandler.java 地定義handler處理 /kawa 的http請求
package com.kawa.io.netty.http; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; import lombok.extern.slf4j.Slf4j; import java.net.URI; @Slf4j public class BrianHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> { // read the client send data @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { // check the msg is HttpRequest if (msg instanceof HttpRequest) { HttpRequest request = (HttpRequest) msg; URI uri = new URI(request.uri()); if ("/kawa".equals(uri.getPath())) { log.info(">>>>>>>>>> client: {}", ctx.channel().remoteAddress()); log.info(">>>>>>>>>> type: {}", msg.getClass().getName()); log.info(">>>>>>>>>> pipeline: {}", ctx.pipeline().getClass().getName() + ctx.pipeline().hashCode()); log.info(">>>>>>>>>> handler: {}", getClass().getName() + this.hashCode()); sendMsg(ctx, "save the request to storage"); } else if ("/favicon.ico".equals(uri.getPath())) { sendMsg(ctx, "load the favicon.ico"); } else { log.info(">>>>>>>>>> client: {} type: {}", ctx.channel().remoteAddress(), msg.getClass()); sendMsg(ctx, "404 not found"); } } } private void sendMsg(ChannelHandlerContext ctx, String msg) { // send the http protocol response String template = "{\"message\":\"%s\"}"; ByteBuf content = Unpooled.copiedBuffer(String.format(template, msg), CharsetUtil.UTF_8); // create a HttpResponse FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content); httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes()); // send ctx.writeAndFlush(httpResponse); } }
啓動服務測試http://localhost:9999/kawa
後臺打印的日誌
INFO [main] 2021-09-25 22:27:55.662 c.kawa.io.netty.http.BrianHttpServer - ---------- BrianHttpServer is ready ---------- DEBUG [nioEventLoopGroup-3-2] 2021-09-25 22:28:14.241 io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkAccessible: true DEBUG [nioEventLoopGroup-3-2] 2021-09-25 22:28:14.242 io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkBounds: true DEBUG [nioEventLoopGroup-3-2] 2021-09-25 22:28:14.243 i.n.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@7db6b201 DEBUG [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.260 io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 4096 DEBUG [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.261 io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2 DEBUG [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.261 io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16 DEBUG [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.261 io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8 DEBUG [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.261 io.netty.util.Recycler - -Dio.netty.recycler.delayedQueue.ratio: 8 INFO [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.297 c.k.i.n.http.BrianHttpServerHandler - >>>>>>>>>> client: /0:0:0:0:0:0:0:1:36500 INFO [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.298 c.k.i.n.http.BrianHttpServerHandler - >>>>>>>>>> type: io.netty.handler.codec.http.DefaultHttpRequest INFO [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.307 c.k.i.n.http.BrianHttpServerHandler - >>>>>>>>>> pipeline: io.netty.channel.DefaultChannelPipeline855934580 INFO [nioEventLoopGroup-3-1] 2021-09-25 22:28:14.308 c.k.i.n.http.BrianHttpServerHandler - >>>>>>>>>> handler: com.kawa.io.netty.http.BrianHttpServerHandler325803268