講解)
目錄一、BIO / NIO / AIO 簡(jiǎn)單對(duì)比Java NIO 三大組件二、Netty 核心架構(gòu)與線程模型重中之重主從 Reactor 模型NioEventLoop三、Channel、Pipeline、ChannelHandler核心三件套1. Channel2. ChannelPipeline 流水線3. ChannelHandler 處理器ByteBufNetty 封裝的緩沖區(qū)替代 JDK ByteBuffer四、編解碼器解決粘包半包五、Netty 重要概念1. ChannelFuture2. ChannelOption / ChildOption3. IdleStateHandler 空閑檢測(cè)業(yè)務(wù)心跳4. 零拷貝 Zero?Copy六、Netty 執(zhí)行完整流程服務(wù)端接收消息全過(guò)程七、常見(jiàn)坑面試高頻八、Netty 的 Reactor 面試高頻問(wèn)題九、簡(jiǎn)單完整示例服務(wù)端十、Netty 適合什么場(chǎng)景Netty 是基于 Java NIO 的高性能、異步事件驅(qū)動(dòng)的網(wǎng)絡(luò)通信框架封裝了 Java 原生 NIO 的復(fù)雜 API用于快速開發(fā)高并發(fā)的 TCP/UDP 網(wǎng)絡(luò)服務(wù)端、客戶端。主流中間件底層網(wǎng)絡(luò)全部用 NettyRocketMQ、Dubbo、Elasticsearch、Zookeeper、Spring Cloud Gateway、Redis Java 客戶端。Java 網(wǎng)絡(luò)演進(jìn)BIO → NIO(Java原生) → Netty(封裝NIO)一、BIO / NIO / AIO 簡(jiǎn)單對(duì)比模型IO 類型線程模型缺點(diǎn)BIO阻塞 IO1 連接 1 線程高并發(fā)下線程爆炸上下文切換重NIO(Java)非阻塞同步單線程 Selector 輪詢?cè)?API 繁瑣BUG 多 (空輪詢 bug)AIO異步非阻塞操作系統(tǒng)回調(diào)Windows 好Linux 內(nèi)核支持差實(shí)際很少用Netty 底層在 Linux 上使用 NIOEpollWindows 上會(huì)自動(dòng)切換 AIO。生產(chǎn)環(huán)境幾乎都是 NIO 模式。Java NIO 三大組件Buffer 緩沖區(qū)數(shù)據(jù)讀寫容器ByteBufferChannel 通道雙向讀寫SocketChannel、ServerSocketChannelSelector 選擇器多路復(fù)用器一個(gè)線程監(jiān)聽(tīng)大量 Channel 事件連接、讀、寫實(shí)現(xiàn)少量線程處理上萬(wàn)連接。原生 NIO 痛點(diǎn)ByteBuffer API 難用需要 flip、rewind、clear容易內(nèi)存溢出Selector 空輪詢 BUG (JDK bug)CPU 100%沒(méi)有編解碼器、斷連處理、心跳、半包粘包全部自己手寫線程模型、異常處理需要自己實(shí)現(xiàn) Netty 就是把這些坑全部封裝好。二、Netty 核心架構(gòu)與線程模型重中之重Netty 核心線程模型Reactor 反應(yīng)器模式Netty 實(shí)現(xiàn)了三種 Reactor單線程 Reactor不生產(chǎn)用一個(gè)線程干接受連接 處理 IO多線程 Reactor一個(gè) accept 線程N(yùn) 個(gè) IO 線程處理讀寫主從 ReactorNetty 默認(rèn)【生產(chǎn)常用】主從 Reactor 模型Boss Group主 Reactor負(fù)責(zé)接收 TCP 連接只做 accept不處理業(yè)務(wù)讀寫。一般 1 個(gè)線程足夠。Worker Group從 Reactor處理已建立連接的所有 IO 事件read、write、編解碼、觸發(fā) handlerN 個(gè)線程。NioEventLoopGroup 就是線程組NioEventLoop 就是單個(gè)線程。java// 服務(wù)端經(jīng)典初始化代碼 // boss線程組接收連接worker線程組處理socket讀寫 NioEventLoopGroup bossGroup new NioEventLoopGroup(1); NioEventLoopGroup workerGroup new NioEventLoopGroup(); ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 指定NIO通道實(shí)現(xiàn) .option(ChannelOption.SO_BACKLOG,128) // tcp半連接隊(duì)列 .childOption(ChannelOption.SO_KEEPALIVE,true) // tcp心跳 .childHandler(new ChannelInitializerNioSocketChannel() { Override protected void initChannel(NioSocketChannel ch) { // 流水線添加處理器 ch.pipeline().addLast(new MyHandler()); } }); ChannelFuture future bootstrap.bind(8888).sync(); future.channel().closeFuture().sync();NioEventLoop一個(gè)NioEventLoop對(duì)應(yīng)一個(gè) Java 線程 一個(gè) Selector一個(gè) Worker NioEventLoop 可以綁定多個(gè) SocketChannel多路復(fù)用監(jiān)聽(tīng) IO 事件一個(gè) SocketChannel 永遠(yuǎn)只會(huì)綁定到同一個(gè) NioEventLoop 線程所以同一個(gè) channel 的 handler 執(zhí)行是單線程不需要同步鎖非常關(guān)鍵的特性任務(wù)隊(duì)列每個(gè) NioEventLoop 有任務(wù)隊(duì)列支持普通任務(wù)、定時(shí)任務(wù)。注意不要在 Netty IO 線程執(zhí)行耗時(shí)業(yè)務(wù)數(shù)據(jù)庫(kù)查詢、http 調(diào)用會(huì)阻塞整個(gè)線程所有綁定這個(gè)線程的連接全部卡死 耗時(shí)業(yè)務(wù)丟到自定義業(yè)務(wù)線程池執(zhí)行。三、Channel、Pipeline、ChannelHandler核心三件套1. Channel代表一條 Socket 連接封裝底層 Java NIO SocketChannel。 常用方法writeAndFlush()寫消息并刷到內(nèi)核緩沖區(qū)close()關(guān)閉連接channel().isActive()判斷連接是否存活2. ChannelPipeline 流水線每個(gè) Channel 內(nèi)部持有一個(gè)ChannelPipeline是Handler 的雙向鏈表。 IO 事件會(huì)沿著 pipeline 依次流轉(zhuǎn)。inbound 入站事件外部 → 服務(wù)端連接建立、讀數(shù)據(jù)從 head→tail 執(zhí)行 outbound 出站事件服務(wù)端 → 外部寫數(shù)據(jù)、關(guān)閉從 tail→head 執(zhí)行HeadContext -- Handler1 -- Handler2 -- TailContext3. ChannelHandler 處理器分為兩類ChannelInboundHandler 入站處理器處理接收過(guò)來(lái)的數(shù)據(jù)channelRead()讀取報(bào)文ChannelOutboundHandler 出站處理器處理向外發(fā)送的數(shù)據(jù)write()常用適配器簡(jiǎn)化開發(fā)ChannelInboundHandlerAdapterChannelOutboundHandlerAdapterSimpleChannelInboundHandler自動(dòng)釋放 ByteBuf泛型直接拿到解碼后的對(duì)象業(yè)務(wù) handler 最常用ByteBufNetty 封裝的緩沖區(qū)替代 JDK ByteBufferNetty 自己實(shí)現(xiàn)的緩沖區(qū)解決原生 ByteBuffer 的痛點(diǎn)讀寫指針?lè)蛛xreaderIndex、writerIndex不用 flip 切換支持池化內(nèi)存PooledByteBuf默認(rèn)開啟復(fù)用直接內(nèi)存減少 GC支持復(fù)合緩沖區(qū) CompositeByteBuf組合多個(gè)緩沖區(qū)不拷貝堆內(nèi)存 (HeapByteBuf) / 直接內(nèi)存 (DirectByteBuf零拷貝不走 JVM 堆)??內(nèi)存釋放Netty 引用計(jì)數(shù)retain()/release()DirectByteBuf 不釋放會(huì)直接內(nèi)存泄漏 SimpleChannelInboundHandler 會(huì)自動(dòng) release普通 InboundHandler 拿到 ByteBuf處理完必須手動(dòng) release。四、編解碼器解決粘包半包TCP 是流協(xié)議沒(méi)有消息邊界會(huì)出現(xiàn)粘包、半包多條報(bào)文粘在一起或者一條報(bào)文被拆成多次到達(dá)。Netty 內(nèi)置開箱即用解碼器FixedLengthFrameDecoder固定長(zhǎng)度幀DelimiterBasedFrameDecoder分隔符分割報(bào)文比如換行符LengthFieldBasedFrameDecoder【最常用】長(zhǎng)度域解碼器協(xié)議頭部存消息長(zhǎng)度支持自定義偏移RPC、MQ 大量使用。解碼器屬于 InboundHandler編碼器屬于 OutboundHandler。 編碼業(yè)務(wù)對(duì)象 → ByteBuf解碼ByteBuf → 業(yè)務(wù)對(duì)象。示例Protobuf、JSON 都可以配合 LengthFieldBasedFrameDecoder 做私有二進(jìn)制協(xié)議。五、Netty 重要概念1. ChannelFutureNetty 所有 IO 操作全是異步bind、connect、writeAndFlush 全部返回 ChannelFuture不會(huì)阻塞。.sync()阻塞等待操作完成.addListener()異步回調(diào)推薦不阻塞線程java// 異步回調(diào)寫法 channel.writeAndFlush(msg).addListener(future - { if(future.isSuccess()){ System.out.println(發(fā)送成功); }else{ future.cause().printStackTrace(); } });2. ChannelOption / ChildOptionOption作用于服務(wù)端 ServerSocketChannel接受連接的那個(gè) socketChildOption作用于每一條客戶端 SocketChannel常用參數(shù)SO_BACKLOGTCP 半連接隊(duì)列大小SO_KEEPALIVETCP 層面的空閑探測(cè)不推薦業(yè)務(wù)心跳粒度粗TCP_NODELAY關(guān)閉 Nagle 算法小報(bào)文立刻發(fā)送低延遲系統(tǒng)開啟3. IdleStateHandler 空閑檢測(cè)業(yè)務(wù)心跳TCP keepalive 是內(nèi)核層業(yè)務(wù)感知差。Netty 提供空閑處理器放在 pipeline檢測(cè)讀空閑、寫空閑觸發(fā)事件我們發(fā)送心跳包超時(shí)關(guān)閉僵死連接。java// 讀空閑5秒寫空閑8秒讀寫空閑10秒觸發(fā)IdleStateEvent ch.pipeline().addLast(new IdleStateHandler(5,8,10));4. 零拷貝 Zero?CopyNetty 的零拷貝不是操作系統(tǒng) mmap 那種分幾層FileRegion文件傳輸直接把文件從磁盤發(fā)送到網(wǎng)卡不拷貝到 JVM 內(nèi)存CompositeByteBuf邏輯合并多個(gè)緩沖區(qū)不做內(nèi)存復(fù)制Direct Buffer使用操作系統(tǒng)直接內(nèi)存JVM 堆和 socket 之間少一次拷貝六、Netty 執(zhí)行完整流程服務(wù)端接收消息全過(guò)程BossGroup NioEventLoop 的 Selector 監(jiān)聽(tīng) OP_ACCEPT 事件有客戶端連接進(jìn)來(lái)accept 得到 SocketChannel將 SocketChannel 注冊(cè)到 WorkerGroup 其中一個(gè) NioEventLoop 的 Selector 上監(jiān)聽(tīng) OP_READ 事件客戶端發(fā)送數(shù)據(jù)Worker 線程 Selector 觸發(fā) OP_READ讀到字節(jié)到 ByteBuf進(jìn)入 pipeline從 head 開始執(zhí)行各個(gè) InboundHandler首先經(jīng)過(guò)解碼器LengthFieldBasedFrameDecoder拆包把 ByteBuf 轉(zhuǎn)成業(yè)務(wù) POJO 對(duì)象執(zhí)行業(yè)務(wù) Handler channelRead () 方法如果業(yè)務(wù)要返回響應(yīng)調(diào)用 writeAndFlush事件反向走 OutboundHandler編碼器編碼最終發(fā)送到 socket 緩沖區(qū)write () 只是寫入緩沖區(qū)不會(huì)真正發(fā)送必須調(diào)用 flush 才會(huì)刷出writeAndFlush 合并兩個(gè)操作。七、常見(jiàn)坑面試高頻禁止在 IO 線程執(zhí)行耗時(shí)操作數(shù)據(jù)庫(kù)、RPC、復(fù)雜計(jì)算會(huì)阻塞 NioEventLoop 線程同線程所有連接卡死。解決方案把任務(wù)提交到自定義業(yè)務(wù)線程池。DirectByteBuf 內(nèi)存泄漏直接內(nèi)存不在堆GC 不會(huì)自動(dòng)回收必須 release開啟-Dio.netty.leakDetection.levelPARANOID檢測(cè)內(nèi)存泄漏。粘包半包不要裸用 ByteBuf 接收業(yè)務(wù)報(bào)文必須加解碼器。不要混用同步 sync ()IO 線程內(nèi)調(diào)用 sync () 造成死鎖IO 線程中絕對(duì)不能調(diào)用 future.sync ()要用 addListener 異步回調(diào)。NioEventLoop 綁定 Channel 后channel 所有 handler 都在這個(gè)線程執(zhí)行所以同一個(gè) channel 內(nèi)變量不需要加 synchronized跨多個(gè) channel 共享變量仍然需要鎖。TCP 斷開不一定觸發(fā) channelRead一定要捕獲channelInactive連接斷開事件清理會(huì)話。八、Netty 的 Reactor 面試高頻問(wèn)題QBossGroup 線程數(shù)為什么一般設(shè)置 1 ServerSocketChannel 只有一個(gè)只負(fù)責(zé) accept 接受連接單線程足夠多線程反而會(huì)競(jìng)爭(zhēng) accept 鎖沒(méi)有收益。QWorkerGroup 默認(rèn)線程數(shù)CPU核心數(shù)*2Netty 默認(rèn)構(gòu)造器就是這個(gè)值。QNetty 怎么解決 JDK NIO Selector 空輪詢 bug 檢測(cè) Selector.select () 返回 0 但沒(méi)有 IO 事件達(dá)到閾值就重建新 Selector把所有 channel 遷移過(guò)去。QwriteAndFlush 發(fā)送消息是立刻發(fā)送給客戶端嗎 不是寫到 Netty 的發(fā)送緩沖區(qū)隊(duì)列由 NioEventLoop 在合適時(shí)機(jī)調(diào)用底層 socket write 系統(tǒng)調(diào)用刷入操作系統(tǒng)內(nèi)核 socket 緩沖區(qū)內(nèi)核緩沖區(qū)再發(fā)給網(wǎng)絡(luò)。九、簡(jiǎn)單完整示例服務(wù)端javapublic class NettyServerDemo { public static void main(String[] args) throws InterruptedException { NioEventLoopGroup boss new NioEventLoopGroup(1); NioEventLoopGroup worker new NioEventLoopGroup(); try { ServerBootstrap b new ServerBootstrap(); b.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerNioSocketChannel() { Override protected void initChannel(NioSocketChannel ch) { ChannelPipeline p ch.pipeline(); // 簡(jiǎn)單按換行符分割報(bào)文 p.addLast(new DelimiterBasedFrameDecoder(1024, Delimiters.lineDelimiter())); p.addLast(new StringDecoder()); p.addLast(new StringEncoder()); // 業(yè)務(wù)handler p.addLast(new SimpleChannelInboundHandlerString() { Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(收到客戶端消息 msg); ctx.writeAndFlush(服務(wù)端回執(zhí) msg \r\n); } Override public void channelInactive(ChannelHandlerContext ctx) { System.out.println(客戶端斷開連接); } }); } }); ChannelFuture f b.bind(9000).sync(); f.channel().closeFuture().sync(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } }十、Netty 適合什么場(chǎng)景? 長(zhǎng)連接服務(wù)、RPC 框架、消息中間件、網(wǎng)關(guān)、IM 聊天、私有二進(jìn)制協(xié)議 ? 簡(jiǎn)單短連接 http 接口直接用 SpringBoot web 即可沒(méi)必要手寫 Netty如果你需要我可以繼續(xù)手寫 Netty 客戶端完整代碼LengthFieldBasedFrameDecoder 詳細(xì)圖解協(xié)議頭長(zhǎng)度域模擬半包粘包演示Netty 高頻面試題整理Netty 源碼核心流程梳理NioEventLoop.run 循環(huán)