Netty之旅二:口口相傳的高性能Netty到底是什麼?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ad/adc61b03ce4460399a0adb2b07e8a74b.png","alt":"d0iosx.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"高清思維導圖原件("},{"type":"codeinline","content":[{"type":"text","text":"xmind/pdf/jpg"}]},{"type":"text","text":")可以關注公衆號:"},{"type":"codeinline","content":[{"type":"text","text":"一枝花算不算浪漫"}]},{"type":"text","text":" 回覆"},{"type":"codeinline","content":[{"type":"text","text":"netty01"}]},{"type":"text","text":"即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一篇文章講了"},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"相關的知識點,相比於傳統"},{"type":"codeinline","content":[{"type":"text","text":"IO"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"已經做得很優雅了,爲什麼我們還要使用"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上篇文章最後留了很多坑,講了"},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"使用的弊端,也是爲了引出"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"而設立的,這篇文章我們就來好好揭開"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"的神祕面紗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本篇文章的目的很簡單,希望看過後你能看懂"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"的示例代碼,針對於簡單的網絡通信,自己也能用"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"手寫一個開發應用出來!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一個簡單的Netty示例"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是一個簡單聊天室Server端的程序,代碼參考自:"},{"type":"codeinline","content":[{"type":"text","text":"http://www.imooc.com/read/82/article/2166"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼有點長,主要核心代碼是在"},{"type":"codeinline","content":[{"type":"text","text":"main()"}]},{"type":"text","text":"方法中,這裏代碼也希望大家看懂,後面也會一步步剖析。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PS:我是用"},{"type":"codeinline","content":[{"type":"text","text":"mac"}]},{"type":"text","text":"系統,直接在終端輸入"},{"type":"codeinline","content":[{"type":"text","text":"telnet 127.0.0.1 8007"}]},{"type":"text","text":" 即可啓動一個聊天框,如果提示找不到"},{"type":"codeinline","content":[{"type":"text","text":"telnet"}]},{"type":"text","text":"命令,可以通過"},{"type":"codeinline","content":[{"type":"text","text":"brew"}]},{"type":"text","text":"進行安裝,具體步驟請自行百度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * @Description netty簡易聊天室\n *\n * @Author 一枝花算不算浪漫\n * @Date 2020/8/10 6:52 上午\n */\npublic final class NettyChatServer {\n\n static final int PORT = Integer.parseInt(System.getProperty(\"port\", \"8007\"));\n\n public static void main(String[] args) throws Exception {\n // 1. EventLoopGroup\n EventLoopGroup bossGroup = new NioEventLoopGroup(1);\n EventLoopGroup workerGroup = new NioEventLoopGroup();\n try {\n // 2. 服務端引導器\n ServerBootstrap serverBootstrap = new ServerBootstrap();\n // 3. 設置線bootStrap信息\n serverBootstrap.group(bossGroup, workerGroup)\n // 4. 設置ServerSocketChannel的類型\n .channel(NioServerSocketChannel.class)\n // 5. 設置參數\n .option(ChannelOption.SO_BACKLOG, 100)\n // 6. 設置ServerSocketChannel對應的Handler,只能設置一個\n .handler(new LoggingHandler(LogLevel.INFO))\n // 7. 設置SocketChannel對應的Handler\n .childHandler(new ChannelInitializer() {\n @Override\n public void initChannel(SocketChannel ch) throws Exception {\n ChannelPipeline p = ch.pipeline();\n // 可以添加多個子Handler\n p.addLast(new LoggingHandler(LogLevel.INFO));\n p.addLast(new ChatNettyHandler());\n }\n });\n\n // 8. 綁定端口\n ChannelFuture f = serverBootstrap.bind(PORT).sync();\n // 9. 等待服務端監聽端口關閉,這裏會阻塞主線程\n f.channel().closeFuture().sync();\n } finally {\n // 10. 優雅地關閉兩個線程池\n bossGroup.shutdownGracefully();\n workerGroup.shutdownGracefully();\n }\n }\n\n private static class ChatNettyHandler extends SimpleChannelInboundHandler {\n @Override\n public void channelActive(ChannelHandlerContext ctx) {\n System.out.println(\"one conn active: \" + ctx.channel());\n // channel是在ServerBootstrapAcceptor中放到EventLoopGroup中的\n ChatHolder.join((SocketChannel) ctx.channel());\n }\n\n @Override\n protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {\n byte[] bytes = new byte[byteBuf.readableBytes()];\n byteBuf.readBytes(bytes);\n String content = new String(bytes, StandardCharsets.UTF_8);\n System.out.println(content);\n\n if (content.equals(\"quit\\r\\n\")) {\n ctx.channel().close();\n } else {\n ChatHolder.propagate((SocketChannel) ctx.channel(), content);\n }\n }\n\n @Override\n public void channelInactive(ChannelHandlerContext ctx) {\n System.out.println(\"one conn inactive: \" + ctx.channel());\n ChatHolder.quit((SocketChannel) ctx.channel());\n }\n }\n\n private static class ChatHolder {\n static final Map USER_MAP = new ConcurrentHashMap<>();\n\n /**\n * 加入羣聊\n */\n static void join(SocketChannel socketChannel) {\n // 有人加入就給他分配一個id\n String userId = \"用戶\"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);\n send(socketChannel, \"您的id爲:\" + userId + \"\\n\\r\");\n\n for (SocketChannel channel : USER_MAP.keySet()) {\n send(channel, userId + \" 加入了羣聊\" + \"\\n\\r\");\n }\n\n // 將當前用戶加入到map中\n USER_MAP.put(socketChannel, userId);\n }\n\n /**\n * 退出羣聊\n */\n static void quit(SocketChannel socketChannel) {\n String userId = USER_MAP.get(socketChannel);\n send(socketChannel, \"您退出了羣聊\" + \"\\n\\r\");\n USER_MAP.remove(socketChannel);\n\n for (SocketChannel channel : USER_MAP.keySet()) {\n if (channel != socketChannel) {\n send(channel, userId + \" 退出了羣聊\" + \"\\n\\r\");\n }\n }\n }\n\n /**\n * 擴散說話的內容\n */\n public static void propagate(SocketChannel socketChannel, String content) {\n String userId = USER_MAP.get(socketChannel);\n for (SocketChannel channel : USER_MAP.keySet()) {\n if (channel != socketChannel) {\n send(channel, userId + \": \" + content);\n }\n }\n }\n\n /**\n * 發送消息\n */\n static void send(SocketChannel socketChannel, String msg) {\n try {\n ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;\n ByteBuf writeBuffer = allocator.buffer(msg.getBytes().length);\n writeBuffer.writeCharSequence(msg, Charset.defaultCharset());\n socketChannel.writeAndFlush(writeBuffer);\n } catch (Exception e) {\n e.printStackTrace();\n }\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a6/a6b3e3d6d3df87bea45eccf2f02e3b7b.png","alt":"dkeb0s.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼有點長,執行完的效果如上圖所示,下面所有內容都是圍繞着"},{"type":"codeinline","content":[{"type":"text","text":"如何看懂"}]},{"type":"text","text":"以及"},{"type":"codeinline","content":[{"type":"text","text":"如何寫出"}]},{"type":"text","text":"這樣的代碼來展開的,希望你看完 也能輕鬆手寫"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"服務端代碼~。通過簡單demo開發讓大家體驗了"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"實現相比"},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"確實要簡單的多,但優點不限於此,只需要知道選擇Netty就對了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Netty核心組件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對應着文章開頭的思維導圖,我們知道"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"的核心組件主要有:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Bootstrap && ServerBootstrap"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"EventLoopGroup"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"EventLoop"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ByteBuf"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Channel"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ChannelHandler"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ChannelFuture"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ChannelPipeline"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ChannelHandlerContext"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類圖如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d2/d26a6eeaf925fe55e05d449a1f427f48.png","alt":"dk8ZC9.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Bootstrap & ServerBootstrap"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一看到"},{"type":"codeinline","content":[{"type":"text","text":"BootStrap"}]},{"type":"text","text":"大家就應該想到"},{"type":"text","marks":[{"type":"strong"}],"text":"啓動類、引導類"},{"type":"text","text":"這樣的詞彙,之前分析過[EurekaServer項目啓動類時][1]介紹過"},{"type":"codeinline","content":[{"type":"text","text":"EurekaBootstrap"}]},{"type":"text","text":", 他的作用就是上下文初始化、配置初始化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"中我們也有類似的類,"},{"type":"codeinline","content":[{"type":"text","text":"Bootstrap"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"它們都是"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"程序的引導類,主要用於配置各種參數,並啓動整個"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"服務,我們看下文章開頭的示例代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ServerBootstrap serverBootstrap = new ServerBootstrap();\nserverBootstrap.group(bossGroup, workerGroup) \n .channel(NioServerSocketChannel.class)\n .option(ChannelOption.SO_BACKLOG, 100)\n .handler(new LoggingHandler(LogLevel.INFO))\n .childHandler(new ChannelInitializer() {\n @Override\n public void initChannel(SocketChannel ch) throws Exception {\n ChannelPipeline p = ch.pipeline();\n p.addLast(new LoggingHandler(LogLevel.INFO));\n p.addLast(new ChatNettyHandler());\n }\n });"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Bootstrap"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"是針對於"},{"type":"codeinline","content":[{"type":"text","text":"Client"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Server"}]},{"type":"text","text":"端定義的兩套啓動類,區別如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Bootstrap"}]},{"type":"text","text":"是客戶端引導類,而"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"是服務端引導類。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Bootstrap"}]},{"type":"text","text":"通常使用"},{"type":"codeinline","content":[{"type":"text","text":"connect()"}]},{"type":"text","text":"方法連接到遠程的主機和端口,作爲一個"},{"type":"codeinline","content":[{"type":"text","text":"TCP客戶端"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"通常使用"},{"type":"codeinline","content":[{"type":"text","text":"bind()"}]},{"type":"text","text":"方法綁定本地的端口,等待客戶端來連接。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"可以處理"},{"type":"codeinline","content":[{"type":"text","text":"Accept"}]},{"type":"text","text":"事件,這裏面"},{"type":"codeinline","content":[{"type":"text","text":"childHandler"}]},{"type":"text","text":"是用來處理"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"請求的,我們可以查看"},{"type":"codeinline","content":[{"type":"text","text":"chaildHandler()"}]},{"type":"text","text":"方法的註解:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/26/26ef7f0cbf0694c41594c355df2cdbe7.png","alt":"dk884H.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Bootstrap"}]},{"type":"text","text":"客戶端引導只需要一個"},{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":",但是一個"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"通常需要兩個(上面的"},{"type":"codeinline","content":[{"type":"text","text":"boosGroup"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"workerGroup"}]},{"type":"text","text":")。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"EventLoopGroup && EventLoop"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":"及"},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":"這兩個類名稱定義的很奇怪,對於初學者來說往往無法通過名稱來了解其中的含義,包括我也是這樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":" 可以理解爲一個線程池,對於服務端程序,我們一般會綁定兩個線程池,一個用於處理 "},{"type":"codeinline","content":[{"type":"text","text":"Accept"}]},{"type":"text","text":" 事件,一個用於處理讀寫事件,看下"},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":"系列的類目錄:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/95/95db84fe7d25dee1eb926fa98f163b96.png","alt":"dU4Roj.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上面的類圖,我們才恍然大悟,我的親孃咧,這不就是一個線程池嘛?(名字氣的犄角拐彎的真是難認)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":"是"},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":"的集合,一個"},{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":" 包含一個或者多個"},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":"。我們可以將"},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":"看做"},{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":"線程池中的一個個工作線程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至於這裏爲什麼要用到兩個線程池,具體的其實可以參考"},{"type":"codeinline","content":[{"type":"text","text":"Reactor"}]},{"type":"text","text":"設計模式,這裏暫時不做過多的講解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 EventLoopGroup 包含一個或多個 EventLoop ,即 EventLoopGroup : EventLoop = 1 : n "}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 EventLoop 在它的生命週期內,只能與一個 Thread 綁定,即 EventLoop : Thread = 1 : 1 "}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有有 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理,從而保證線程安全,即 Thread : EventLoop = 1 : 1"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 Channel 在它的生命週期內只能註冊到一個 EventLoop 上,即 Channel : EventLoop = n : 1 "}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 EventLoop 可被分配至一個或多個 Channel ,即 EventLoop : Channel = 1 : n "}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當一個連接到達時,"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":" 就會創建一個 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":",然後從 "},{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":" 中分配一個 "},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":" 來給這個 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 綁定上,在該 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 的整個生命週期中都是有這個綁定的 "},{"type":"codeinline","content":[{"type":"text","text":"EventLoop"}]},{"type":"text","text":" 來服務的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"ByteBuf"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"Java NIO"}]},{"type":"text","text":"中我們有 "},{"type":"codeinline","content":[{"type":"text","text":"ByteBuffer"}]},{"type":"text","text":"緩衝池,對於它的操作我們應該印象深刻,往"},{"type":"codeinline","content":[{"type":"text","text":"Buffer"}]},{"type":"text","text":"中寫數據時我們需要關注寫入的位置,切換成讀模式時我們還要切換讀寫狀態,不然將會出現大問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對於"},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"中超級難用的"},{"type":"codeinline","content":[{"type":"text","text":"Buffer"}]},{"type":"text","text":"類, "},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":" 提供了"},{"type":"codeinline","content":[{"type":"text","text":"ByteBuf"}]},{"type":"text","text":"來替代。"},{"type":"codeinline","content":[{"type":"text","text":"ByteBuf"}]},{"type":"text","text":"聲明瞭兩個指針:一個讀指針,一個寫指針,使得讀寫操作進行分離,簡化"},{"type":"codeinline","content":[{"type":"text","text":"buffer"}]},{"type":"text","text":"的操作流程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6f/6f7af1693e734d32bd7eee790ee5f9f3.png","alt":"dkQocV.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"提供了發幾種"},{"type":"codeinline","content":[{"type":"text","text":"ByteBuf"}]},{"type":"text","text":"的實現以供我們選擇,"},{"type":"codeinline","content":[{"type":"text","text":"ByteBuf"}]},{"type":"text","text":"可以分爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Pooled"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Unpooled"}]},{"type":"text","text":" 池化和非池化"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Heap 和 Direct,堆內存和堆外內存,NIO中創建Buffer也可以指定"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Safe 和 Unsafe,安全和非安全"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/96/962ee1464dfe1982f16ace358a5b3032.png","alt":"dkJ9TU.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於這麼多種創建"},{"type":"codeinline","content":[{"type":"text","text":"Buffer"}]},{"type":"text","text":"的方式該怎麼選擇呢?"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"也爲我們處理好了,我們可以直接使用(真是暖男"},{"type":"codeinline","content":[{"type":"text","text":"Ntetty"}]},{"type":"text","text":"):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;\nByteBuf buffer = allocator.buffer(length);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用這種方式,Netty將最大努力的使用池化、Unsafe、對外內存的方式爲我們創建buffer。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Channel"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提起"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"並不陌生,上一篇講"},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"的三大組件提到過,最常見的就是"},{"type":"codeinline","content":[{"type":"text","text":"java.nio.SocketChannel"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"java.nio.ServerSocketChannel"}]},{"type":"text","text":",他們用於非阻塞的I/0操作。類似於"},{"type":"codeinline","content":[{"type":"text","text":"NIO"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":",Netty提供了自己的"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"和其子類實現,用於異步I/0操作和其他相關的操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":" 中, "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 是一個 "},{"type":"codeinline","content":[{"type":"text","text":"Socket"}]},{"type":"text","text":" 連接的抽象, 它爲用戶提供了關於底層 "},{"type":"codeinline","content":[{"type":"text","text":"Socket"}]},{"type":"text","text":" 狀態(是否是連接還是斷開) 以及對 "},{"type":"codeinline","content":[{"type":"text","text":"Socket"}]},{"type":"text","text":" 的讀寫等操作。每當 "},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":" 建立了一個連接後, 都會有一個對應的 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 實例。並且,有父子"},{"type":"codeinline","content":[{"type":"text","text":"channel"}]},{"type":"text","text":"的概念。 服務器連接監聽的"},{"type":"codeinline","content":[{"type":"text","text":"channel"}]},{"type":"text","text":" ,也叫 "},{"type":"codeinline","content":[{"type":"text","text":"parent channel"}]},{"type":"text","text":"。 對應於每一個 "},{"type":"codeinline","content":[{"type":"text","text":"Socket"}]},{"type":"text","text":" 連接的"},{"type":"codeinline","content":[{"type":"text","text":"channel"}]},{"type":"text","text":",也叫 "},{"type":"codeinline","content":[{"type":"text","text":"child channel"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然"},{"type":"codeinline","content":[{"type":"text","text":"channel"}]},{"type":"text","text":" 是 Netty 抽象出來的網絡 I/O 讀寫相關的接口,爲什麼不使用"},{"type":"codeinline","content":[{"type":"text","text":" JDK NIO"}]},{"type":"text","text":" 原生的 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 而要另起爐竈呢,主要原因如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"JDK"}]},{"type":"text","text":" 的"},{"type":"codeinline","content":[{"type":"text","text":" SocketChannel"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"ServersocketChannel "}]},{"type":"text","text":"沒有統一的 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 接口供業務開發者使用,對一於用戶而言,沒有統一的操作視圖,使用起來並不方便。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"JDK"}]},{"type":"text","text":" 的 "},{"type":"codeinline","content":[{"type":"text","text":"SocketChannel "}]},{"type":"text","text":"和 "},{"type":"codeinline","content":[{"type":"text","text":"ScrversockctChannel "}]},{"type":"text","text":"的主要職責就是網絡 I/O 操作,由於他們是"},{"type":"codeinline","content":[{"type":"text","text":" SPI"}]},{"type":"text","text":" 類接口,由具體的虛擬機廠家來提供,所以通過繼承 SPI 功能直接實現 "},{"type":"codeinline","content":[{"type":"text","text":"ServersocketChannel"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"SocketChannel"}]},{"type":"text","text":" 來擴展其工作量和重新"},{"type":"codeinline","content":[{"type":"text","text":" Channel"}]},{"type":"text","text":" 功類是差不多的。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Netty 的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline Channel"}]},{"type":"text","text":" 需要夠跟 Netty 的整體架構融合在一起,例如 I/O 模型、基的定製模型,以及基於元數據描述配置化的 TCP 參數等,這些"},{"type":"codeinline","content":[{"type":"text","text":" JDK SocketChannel"}]},{"type":"text","text":" 和"},{"type":"codeinline","content":[{"type":"text","text":" ServersocketChannel "}]},{"type":"text","text":"都沒有提供,需要重新封裝。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自定義的 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" ,功實現更加靈活。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於上述 4 原因,它的設計原理比較簡單, Netty 重新設計了 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 接口,並且給予了很多不同的實現。但是功能卻比較繁雜,主要的設計理念如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 接口層,相關聯的其他操作封裝起來,採用 "},{"type":"codeinline","content":[{"type":"text","text":"Facade"}]},{"type":"text","text":" 模式進行統一封裝,將網絡 I/O 操作、網絡 I/O 統一對外提供。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 接口的定義儘量大而全,統一的視圖,由不同子類實現不同的功能,公共功能在抽象父類中實現,最大程度上實現接口的重用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體實現採用聚合而非包含的方式,將相關的功類聚合在 "},{"type":"codeinline","content":[{"type":"text","text":"Channel "}]},{"type":"text","text":"中,由 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 統一負責分配和調度,功能實現更加靈活。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Channel "}]},{"type":"text","text":"的實現類非常多,繼承關係複雜,從學習的角度我們抽取最重要的兩個 "},{"type":"codeinline","content":[{"type":"text","text":"NioServerSocketChannel "}]},{"type":"text","text":"和 "},{"type":"codeinline","content":[{"type":"text","text":"NioSocketChannel"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務端 "},{"type":"codeinline","content":[{"type":"text","text":"NioServerSocketChannel "}]},{"type":"text","text":"的繼承關係類圖如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cc/cccc172c897f3a87d1983669f8129040.png","alt":"dUn8G4.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端 "},{"type":"codeinline","content":[{"type":"text","text":"NioSocketChannel "}]},{"type":"text","text":"的繼承關係類圖如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/45/45e0a0fd80e8c3fcc75aeaa151498eab.png","alt":"dUnJz9.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後面文章源碼系列會具體分析,這裏就不進一步闡述分析了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"ChannelHandler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 是"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"中最常用的組件。"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 主要用來處理各種事件,這裏的事件很廣泛,比如可以是連接、數據接收、異常、數據轉換等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 有兩個核心子類 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":",其中 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":" 用於接收、處理入站( "},{"type":"codeinline","content":[{"type":"text","text":"Inbound"}]},{"type":"text","text":" )的數據和事件,而 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":" 則相反,用於接收、處理出站( "},{"type":"codeinline","content":[{"type":"text","text":"Outbound"}]},{"type":"text","text":" )的數據和事件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c9/c90bd6c99066e919de06a68e16b1edaf.png","alt":"dkJAp9.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":"處理入站數據以及各種狀態變化,當"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"狀態發生改變會調用"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":"中的一些生命週期方法.這些方法與"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"的生命密切相關。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"入站數據,就是進入"},{"type":"codeinline","content":[{"type":"text","text":"socket"}]},{"type":"text","text":"的數據。下面展示一些該接口的生命週期"},{"type":"codeinline","content":[{"type":"text","text":"API"}]},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/28cc3cad531c2ba344ff849b13617ffb.png","alt":"dUntMR.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當某個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":"的實現重寫 "},{"type":"codeinline","content":[{"type":"text","text":"channelRead()"}]},{"type":"text","text":"方法時,它將負責顯式地釋放與池化的 "},{"type":"codeinline","content":[{"type":"text","text":"ByteBuf"}]},{"type":"text","text":" 實例相關的內存。 Netty 爲此提供了一個實用方法"},{"type":"codeinline","content":[{"type":"text","text":"ReferenceCountUtil.release()"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Sharable\npublic class DiscardHandler extends ChannelInboundHandlerAdapter {\n\t@Override\n\tpublic void channelRead(ChannelHandlerContext ctx, Object msg) {\n\t\tReferenceCountUtil.release(msg);\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方式還挺繁瑣的,Netty提供了一個"},{"type":"codeinline","content":[{"type":"text","text":"SimpleChannelInboundHandler"}]},{"type":"text","text":",重寫"},{"type":"codeinline","content":[{"type":"text","text":"channelRead0()"}]},{"type":"text","text":"方法,就可以在調用過程中會自動釋放資源."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class SimpleDiscardHandler\n\textends SimpleChannelInboundHandler {\n\t@Override\n\tpublic void channelRead0(ChannelHandlerContext ctx,\n\t\t\t\t\t\t\t\t\tObject msg) {\n\t\t\t// 不用調用ReferenceCountUtil.release(msg)也會釋放資源\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出站操作和數據將由 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":" 處理。它的方法將被 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"、 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"以及 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":" 調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":" 的一個強大的功能是可以按需推遲操作或者事件,這使得可以通過一些複雜的方法來處理請求。例如, 如果到遠程節點的寫入被暫停了, 那麼你可以推遲沖刷操作並在稍後繼續。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b0/b05c8aea260f08545680e22df5154fb3.png","alt":"d0PxbT.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelPromise"}]},{"type":"text","text":"與"},{"type":"codeinline","content":[{"type":"text","text":"ChannelFuture"}]},{"type":"text","text":": "},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":"中的大部分方法都需要一個"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPromise"}]},{"type":"text","text":"參數, 以便在操作完成時得到通知。 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPromise"}]},{"type":"text","text":"是"},{"type":"codeinline","content":[{"type":"text","text":"ChannelFuture"}]},{"type":"text","text":"的一個子類,其定義了一些可寫的方法,如"},{"type":"codeinline","content":[{"type":"text","text":"setSuccess()"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"setFailure()"}]},{"type":"text","text":",從而使"},{"type":"codeinline","content":[{"type":"text","text":"ChannelFuture"}]},{"type":"text","text":"不可變。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"ChannelHandlerAdapter"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerAdapter"}]},{"type":"text","text":"顧名思義,就是"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":"的適配器。你需要知道什麼是適配器模式,假設有一個A接口,我們需要A的"},{"type":"codeinline","content":[{"type":"text","text":"subclass"}]},{"type":"text","text":"實現功能,但是B類中正好有我們需要的功能,不想複製粘貼B中的方法和屬性了,那麼可以寫一個適配器類"},{"type":"codeinline","content":[{"type":"text","text":"Adpter"}]},{"type":"text","text":"繼承B實現A,這樣一來"},{"type":"codeinline","content":[{"type":"text","text":"Adapter"}]},{"type":"text","text":"是A的子類並且能直接使用B中的方法,這種模式就是適配器模式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就比如Netty中的"},{"type":"codeinline","content":[{"type":"text","text":"SslHandler"}]},{"type":"text","text":"類,想使用"},{"type":"codeinline","content":[{"type":"text","text":"ByteToMessageDecoder"}]},{"type":"text","text":"中的方法進行解碼,但是必須是"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":"子類對象才能加入到"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"中,通過如下簽名和其實現細節("},{"type":"codeinline","content":[{"type":"text","text":"SslHandler"}]},{"type":"text","text":"實現細節就不貼了)就能夠作爲一個"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":"去處理消息了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class SslHandler extends ByteToMessageDecoder implements ChannelOutboundHandler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerAdapter"}]},{"type":"text","text":"提供了一些實用方法"},{"type":"codeinline","content":[{"type":"text","text":"isSharable()"}]},{"type":"text","text":"如果其對應的實現被標註爲"},{"type":"codeinline","content":[{"type":"text","text":" Sharable"}]},{"type":"text","text":", 那麼這個方法將返回 "},{"type":"codeinline","content":[{"type":"text","text":"true"}]},{"type":"text","text":", 表示它可以被添加到多個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"中 。如果想在自己的"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":"中使用這些適配器類,只需要擴展他們,重寫那些想要自定義的方法即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一個新創建的 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 都將會被分配一個新的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"。這項關聯是永久性的; "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 既不能附加另外一個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":",也不能分離其當前的。在 Netty 組件的生命週期中,這是一項固定的操作,不需要開發人員的任何干預。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Netty 的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 爲處理器提供了基本的抽象, 目前你可以認爲每個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 的實例都類似於一種爲了響應特定事件而被執行的回調。從應用程序開發人員的角度來看, 它充當了所有處理入站和出站數據的應用程序邏輯的攔截載體。"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"提供了 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 鏈的容器,並定義了用於在該鏈上傳播入站和出站事件流的 "},{"type":"codeinline","content":[{"type":"text","text":"API"}]},{"type":"text","text":"。當 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 被創建時,它會被自動地分配到它專屬的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 安裝到 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 中的過程如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer"}]},{"type":"text","text":"的實現被註冊到了"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"中"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer.initChannel()"}]},{"type":"text","text":"方法被調用時,"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer"}]},{"type":"text","text":"將在 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"中安裝一組自定義的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer"}]},{"type":"text","text":" 將它自己從 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"中移除"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5f/5f21e347da2e556727b0e9071acff630.png","alt":"dkJuTO.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖所示:這是一個同時具有入站和出站 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"的佈局,並且印證了我們之前的關於 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"主要由一系列的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 所組成的說法。 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"還提供了通過 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 本身傳播事件的方法。如果一個入站事件被觸發,它將被從 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"的頭部開始一直被傳播到 Channel Pipeline 的尾端。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能會說, 從事件途經 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"的角度來看, "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"的頭部和尾端取決於該事件是入站的還是出站的。然而 Netty 總是將 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"的入站口(圖 的左側)作爲頭部,而將出站口(該圖的右側)作爲尾端。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當你完成了通過調用 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline.add*()"}]},{"type":"text","text":"方法將入站處理器( "},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":")和 出 站 處 理 器 ( "},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":" ) 混 合 添 加 到 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"之 後 , 每 一 個"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 從頭部到尾端的順序位置正如同我們方纔所定義它們的一樣。因此,如果你將圖 6-3 中的處理器( "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":")從左到右進行編號,那麼第一個被入站事件看到的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 將是1,而第一個被出站事件看到的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler "}]},{"type":"text","text":"將是 5。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 傳播事件時,它會測試 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 中的下一個 Channel\u0002Handler 的類型是否和事件的運動方向相匹配。如果不匹配, "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 將跳過該"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 並前進到下一個,直到它找到和該事件所期望的方向相匹配的爲止。 (當然, "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler "}]},{"type":"text","text":"也可以同時實現"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler "}]},{"type":"text","text":"接口和 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":" 接口。)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"修改`ChannelPipeline`"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修改指的是添加或刪除"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":",見代碼示例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ChannelPipeline pipeline = ..;\nFirstHandler firstHandler = new FirstHandler();\n// 先添加一個Handler到ChannelPipeline中\npipeline.addLast(\"handler1\", firstHandler);\n// 這個Handler放在了first,意味着放在了handler1之前\npipeline.addFirst(\"handler2\", new SecondHandler());\n// 這個Handler被放到了last,意味着在handler1之後\npipeline.addLast(\"handler3\", new ThirdHandler());\n...\n// 通過名稱刪除\npipeline.remove(\"handler3\");\n// 通過對象刪除\npipeline.remove(firstHandler);\n// 名稱\"handler2\"替換成名稱\"handler4\",並切handler2的實例替換成了handler4的實例\npipeline.replace(\"handler2\", \"handler4\", new ForthHandler());"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"`ChannelPipeline`的出入站`API`"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"入站"},{"type":"codeinline","content":[{"type":"text","text":"API"}]},{"type":"text","text":"所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[圖片上傳失敗...(image-6037f5-1598167949595)]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出站"},{"type":"codeinline","content":[{"type":"text","text":"API"}]},{"type":"text","text":"所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/28da1096ff71d689961eec6e829a8927.png","alt":"dUndZ6.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 這個組件上面所講的大致只需要記住這三點即可:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 保存了與 "},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":" 相關聯的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"可以根據需要,通過添加或者刪除 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 來動態地修改"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline "}]},{"type":"text","text":"有着豐富的"},{"type":"codeinline","content":[{"type":"text","text":" API "}]},{"type":"text","text":"用以被調用,以響應入站和出站事件"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 被添加到 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 時,它將會被分配一個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":" ,它代表了 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 之間的綁定。"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":" 的主要功能是管理它所關聯的"},{"type":"codeinline","content":[{"type":"text","text":" ChannelHandler "}]},{"type":"text","text":"和在同一個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 中的其他"},{"type":"codeinline","content":[{"type":"text","text":" ChannelHandler "}]},{"type":"text","text":"之間的交互。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果調用"},{"type":"codeinline","content":[{"type":"text","text":"Channel"}]},{"type":"text","text":"或"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"上的方法,會沿着整個"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"傳播,如果調用"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":"上的相同方法,則會從對應的當前"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":"進行傳播。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext API"}]},{"type":"text","text":"如下表所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d3/d347a5be50e254d13d74187c41208f99.png","alt":"dUn0IO.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":"之間的關聯(綁定)是永遠不會改變的,所以緩存對它的引用是安全的;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如同在本節開頭所解釋的一樣,相對於其他類的同名方法,"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":"的方法將產生更短的事件流, 應該儘可能地利用這個特性來獲得最大的性能。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"與`ChannelHandler`、`ChannelPipeline`的關聯使用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/70/70f89459251dbf306fff919b2ac9865f.png","alt":"dUnDiD.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":"訪問"},{"type":"codeinline","content":[{"type":"text","text":"channel"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ChannelHandlerContext ctx = ..;\n// 獲取channel引用\nChannel channel = ctx.channel();\n// 通過channel寫入緩衝區\nchannel.write(Unpooled.copiedBuffer(\"Netty in Action\",\nCharsetUtil.UTF_8));"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":"訪問"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ChannelHandlerContext ctx = ..;\n// 獲取ChannelHandlerContext\nChannelPipeline pipeline = ctx.pipeline();\n// 通過ChannelPipeline寫入緩衝區\npipeline.write(Unpooled.copiedBuffer(\"Netty in Action\",\nCharsetUtil.UTF_8));"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f6/f6ead3edd59b01aa410d127a78af049b.png","alt":"dUnrJe.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有時候我們不想從頭傳遞數據,想跳過幾個"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":",從某個"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":"開始傳遞數據.我們必須獲取目標"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":"之前的"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":"關聯的"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ChannelHandlerContext ctx = ..;\n// 直接通過ChannelHandlerContext寫數據,發送到下一個handler\nctx.write(Unpooled.copiedBuffer(\"Netty in Action\", CharsetUtil.UTF_8));"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/19/192fdf08689d827b023df922c8763fdb.png","alt":"dUnyzd.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好了,"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":"的基本使用應該掌握了,但是你真的理解"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Channelhandler"}]},{"type":"text","text":"之間的關係了嗎?不理解也沒關係,因爲源碼以後會幫你理解的更爲深刻。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"核心組件之間的關係"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 "},{"type":"codeinline","content":[{"type":"text","text":"Channel "}]},{"type":"text","text":"對應一個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":" 包含一條雙向的 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext "}]},{"type":"text","text":"鏈"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandlerContext "}]},{"type":"text","text":"中包含一個"},{"type":"codeinline","content":[{"type":"text","text":" ChannelHandler"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 "},{"type":"codeinline","content":[{"type":"text","text":"Channel "}]},{"type":"text","text":"會綁定到一個"},{"type":"codeinline","content":[{"type":"text","text":" EventLoop "}]},{"type":"text","text":"上"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 "},{"type":"codeinline","content":[{"type":"text","text":"NioEventLoop"}]},{"type":"text","text":" 維護了一個 "},{"type":"codeinline","content":[{"type":"text","text":"Selector("}]},{"type":"text","text":"使用的是 Java 原生的 Selector)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 "},{"type":"codeinline","content":[{"type":"text","text":"NioEventLoop"}]},{"type":"text","text":" 相當於一個線程"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"粘包拆包問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"粘包拆包問題是處於網絡比較底層的問題,在數據鏈路層、網絡層以及傳輸層都有可能發生。我們日常的網絡應用開發大都在傳輸層進行,由於"},{"type":"codeinline","content":[{"type":"text","text":"UDP"}]},{"type":"text","text":"有消息保護邊界,不會發生粘包拆包問題,而因此粘包拆包問題只發生在"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"協議中。具體講"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"是個”流\"協議,只有流的概念,沒有包的概念,對於業務上層數據的具體含義和邊界並不瞭解,它只會根據"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"緩衝區的實際情況進行包的劃分。所以在業務上認爲,一個完整的包可能會被"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"拆分成多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"粘包和拆包問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"問題舉例說明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面針對客戶端分別發送了兩個數據表"},{"type":"codeinline","content":[{"type":"text","text":"Packet1"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Packet2"}]},{"type":"text","text":"給服務端的時候,"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"粘包和拆包會出現的情況進行列舉說明:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(1)第一種情況,服務端分兩次正常收到兩個獨立數據包,即沒有發生拆包和粘包的現象;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e3/e32e61e7206cc71fd3c3621242dc11d1.png","alt":"dUncQA.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(2)第二種情況,接收端只收到一個數據包,由於"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"是不會出現丟包的,所以這一個數據包中包含了客戶端發送的兩個數據包的信息,這種現象即爲粘包。這種情況由於接收端不知道這兩個數據包的界限,所以對於服務接收端來說很難處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/49/49c72709433ea14b640261f310393789.png","alt":"dUn2Lt.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(3)第三種情況,服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的"},{"type":"codeinline","content":[{"type":"text","text":"Packet1"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Packet2"}]},{"type":"text","text":"包的部分內容,第二次讀取到了"},{"type":"codeinline","content":[{"type":"text","text":"Packet2"}]},{"type":"text","text":"的剩餘內容,這被稱爲TCP拆包;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ed/ed8f44afda8c09ec366b8e7a30415811.png","alt":"d0Pq8s.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(4)第四種情況,服務端分兩次讀取到了兩個數據包,第一次讀取到了部分的"},{"type":"codeinline","content":[{"type":"text","text":"Packet1"}]},{"type":"text","text":"內容,第二次讀取到了"},{"type":"codeinline","content":[{"type":"text","text":"Packet1"}]},{"type":"text","text":"剩餘內容和"},{"type":"codeinline","content":[{"type":"text","text":"Packet2"}]},{"type":"text","text":"的整包。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c8/c83da38266314c346e54bb2535e48457.png","alt":"dUn5FS.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果此時服務端TCP接收滑窗非常小,而數據包"},{"type":"codeinline","content":[{"type":"text","text":"Packet1"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"Packet2"}]},{"type":"text","text":"比較大,很有可能服務端需要分多次才能將兩個包接收完全,期間發生多次拆包。以上列舉情況的背後原因分別如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"應用程序寫入的數據大於套接字緩衝區大小,這將會發生拆包。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"應用程序寫入數據小於套接字緩衝區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘包。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"進行"},{"type":"codeinline","content":[{"type":"text","text":"MSS"}]},{"type":"text","text":"(最大報文長度)大小的"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"分段,當"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"報文長度-"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"頭部長度>"},{"type":"codeinline","content":[{"type":"text","text":"MSS"}]},{"type":"text","text":"的時候將發生拆包。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"接收方法不及時讀取套接字緩衝區數據,這將發生粘包。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"*"},{"type":"text","marks":[{"type":"italic"}],"text":"如何基於Netty處理粘包、拆包問題"},{"type":"text","text":"*"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於底層的"},{"type":"codeinline","content":[{"type":"text","text":"TCP"}]},{"type":"text","text":"無法理解上層的業務數據,所以在底層是無法保證數據包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案,可以歸納如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":" 消息定長,例如每個報文的大小爲固定長度200字節,如果不夠,空位補空格;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在包尾增加回車換行符進行分割,例如"},{"type":"codeinline","content":[{"type":"text","text":"FTP"}]},{"type":"text","text":"協議;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"將消息分爲消息頭和消息體,消息頭中包含表示消息總長度的字段,通常設計思路爲消息頭的第一個字段使用"},{"type":"codeinline","content":[{"type":"text","text":"int32"}]},{"type":"text","text":"來表示消息的總長度;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"更復雜的應用層協議。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 之前Netty示例中其實並沒有考慮讀半包問題,這在功能測試往往沒有問題,但是一旦請求數過多或者發送大報文之後,就會存在該問題。如果代碼沒有考慮,往往就會出現解碼錯位或者錯誤,導致程序不能正常工作,下面看看Netty是如何根據主流的解決方案進行抽象實現來幫忙解決這一問題的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下表所示,Netty爲了找出消息的邊界,採用封幀方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 方式 | 解碼 | 編碼 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| ------------------ | ------------------------------ | ---------------------- |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 固定長度 | "},{"type":"codeinline","content":[{"type":"text","text":"FixedLengthFrameDecoder"}]},{"type":"text","text":" | 簡單 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 分隔符 | "},{"type":"codeinline","content":[{"type":"text","text":"DelimiterBasedFrameDecoder"}]},{"type":"text","text":" | 簡單 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 專門的 length 字段 | "},{"type":"codeinline","content":[{"type":"text","text":"LengthFieldBasedFrameDecoder"}]},{"type":"text","text":" | "},{"type":"codeinline","content":[{"type":"text","text":"LengthFieldPrepender"}]},{"type":"text","text":" |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意到,Netty提供了對應的解碼器來解決對應的問題,有了這些解碼器,用戶不需要自己對讀取的報文進行人工解碼,也不需要考慮TCP的粘包和半包問題。爲什麼這麼說呢?下面列舉一個包尾增加分隔符的例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"import io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.ChannelInboundHandlerAdapter;\nimport io.netty.util.CharsetUtil;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * @Author: wuxiaofei\n * @Date: 2020/8/15 0015 19:15\n * @Version: 1.0\n * @Description:入站處理器\n */\[email protected]\npublic class DelimiterServerHandler extends ChannelInboundHandlerAdapter {\n\n private AtomicInteger counter = new AtomicInteger(0);\n private AtomicInteger completeCounter = new AtomicInteger(0);\n\n /*** 服務端讀取到網絡數據後的處理*/\n @Override\n public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n ByteBuf in = (ByteBuf)msg;\n String request = in.toString(CharsetUtil.UTF_8);\n System.out.println(\"Server Accept[\"+request\n +\"] and the counter is:\"+counter.incrementAndGet());\n String resp = \"Hello,\"+request+\". Welcome to Netty World!\"\n + DelimiterEchoServer.DELIMITER_SYMBOL;\n ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));\n }\n\n /*** 服務端讀取完成網絡數據後的處理*/\n @Override\n public void channelReadComplete(ChannelHandlerContext ctx)\n throws Exception {\n ctx.fireChannelReadComplete();\n System.out.println(\"the ReadComplete count is \"\n +completeCounter.incrementAndGet());\n }\n\n /*** 發生異常後的處理*/\n @Override\n public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {\n cause.printStackTrace();\n ctx.close();\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"import io.netty.bootstrap.ServerBootstrap;\nimport io.netty.buffer.ByteBuf;\nimport io.netty.buffer.Unpooled;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioServerSocketChannel;\nimport io.netty.handler.codec.DelimiterBasedFrameDecoder;\n\nimport java.net.InetSocketAddress;\n\n/**\n * @Author: wuxiaofei\n * @Date: 2020/8/15 0015 19:17\n * @Version: 1.0\n * @Description:服務端\n */\npublic class DelimiterEchoServer {\n\n public static final String DELIMITER_SYMBOL = \"@~\";\n public static final int PORT = 9997;\n\n public static void main(String[] args) throws InterruptedException {\n DelimiterEchoServer delimiterEchoServer = new DelimiterEchoServer();\n System.out.println(\"服務器即將啓動\");\n delimiterEchoServer.start();\n }\n\n public void start() throws InterruptedException {\n final DelimiterServerHandler serverHandler = new DelimiterServerHandler();\n EventLoopGroup group = new NioEventLoopGroup();/*線程組*/\n try {\n ServerBootstrap b = new ServerBootstrap();/*服務端啓動必須*/\n b.group(group)/*將線程組傳入*/\n .channel(NioServerSocketChannel.class)/*指定使用NIO進行網絡傳輸*/\n .localAddress(new InetSocketAddress(PORT))/*指定服務器監聽端口*/\n /*服務端每接收到一個連接請求,就會新啓一個socket通信,也就是channel,\n 所以下面這段代碼的作用就是爲這個子channel增加handle*/\n .childHandler(new ChannelInitializerImp());\n ChannelFuture f = b.bind().sync();/*異步綁定到服務器,sync()會阻塞直到完成*/\n System.out.println(\"服務器啓動完成,等待客戶端的連接和數據.....\");\n f.channel().closeFuture().sync();/*阻塞直到服務器的channel關閉*/\n } finally {\n group.shutdownGracefully().sync();/*優雅關閉線程組*/\n }\n }\n\n private static class ChannelInitializerImp extends ChannelInitializer {\n\n @Override\n protected void initChannel(Channel ch) throws Exception {\n ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_SYMBOL\n .getBytes());\n //服務端收到數據包後經過DelimiterBasedFrameDecoder即分隔符基礎框架解碼器解碼爲一個個帶有分隔符的數據包。\n ch.pipeline().addLast( new DelimiterBasedFrameDecoder(1024,\n delimiter));\n ch.pipeline().addLast(new DelimiterServerHandler());\n }\n }\n\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"添加到"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"DelimiterBasedFrameDecoder"}]},{"type":"text","text":"用於對使用分隔符結尾的消息進行自動解碼,當然還有沒有用到的"},{"type":"codeinline","content":[{"type":"text","text":"FixedLengthFrameDecoder"}]},{"type":"text","text":"用於對固定長度的消息進行自動解碼等解碼器。正如上門的代碼使用案例,有了Netty提供的幾碼器可以輕鬆地完成對很多消息的自動解碼,而且不需要考慮TCP粘包/拆包導致的讀半包問題,極大地提升了開發效率。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Netty示例代碼詳解"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相信看完上面的鋪墊,你對Netty編碼有了一定的瞭解了,下面再來整體梳理一遍吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/07/079a93fe6e922842756f73c1626e4fd1.png","alt":"dVp7yn.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、設置"},{"type":"codeinline","content":[{"type":"text","text":"EventLoopGroup"}]},{"type":"text","text":"線程組("},{"type":"codeinline","content":[{"type":"text","text":"Reactor"}]},{"type":"text","text":"線程組)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"EventLoopGroup bossGroup = new NioEventLoopGroup(1);\nEventLoopGroup workerGroup = new NioEventLoopGroup();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面我們說過"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"中使用"},{"type":"codeinline","content":[{"type":"text","text":"Reactor"}]},{"type":"text","text":"模式,"},{"type":"codeinline","content":[{"type":"text","text":"bossGroup"}]},{"type":"text","text":"表示服務器連接監聽線程組,專門接受 "},{"type":"codeinline","content":[{"type":"text","text":"Accept"}]},{"type":"text","text":" 新的客戶端"},{"type":"codeinline","content":[{"type":"text","text":"client"}]},{"type":"text","text":" 連接。另一個"},{"type":"codeinline","content":[{"type":"text","text":"workerGroup"}]},{"type":"text","text":"表示處理每一連接的數據收發的線程組,來處理消息的讀寫事件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、服務端引導器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ServerBootstrap serverBootstrap = new ServerBootstrap();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"集成所有配置,用來啓動"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"服務端。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、設置"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"信息"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"serverBootstrap.group(bossGroup, workerGroup);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將兩個線程組設置到"},{"type":"codeinline","content":[{"type":"text","text":"ServerBootstrap"}]},{"type":"text","text":"中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、設置"},{"type":"codeinline","content":[{"type":"text","text":"ServerSocketChannel"}]},{"type":"text","text":"類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"serverBootstrap.channel(NioServerSocketChannel.class);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置通道的"},{"type":"codeinline","content":[{"type":"text","text":"IO"}]},{"type":"text","text":"類型,"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"不止支持"},{"type":"codeinline","content":[{"type":"text","text":"Java NIO"}]},{"type":"text","text":",也支持阻塞式"},{"type":"codeinline","content":[{"type":"text","text":"IO"}]},{"type":"text","text":",例如"},{"type":"codeinline","content":[{"type":"text","text":"OIO"}]},{"type":"text","marks":[{"type":"strong"}],"text":"OioServerSocketChannel.class)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5、設置參數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"serverBootstrap.option(ChannelOption.SO_BACKLOG, 100);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過"},{"type":"codeinline","content":[{"type":"text","text":"option()"}]},{"type":"text","text":"方法可以設置很多參數,這裏"},{"type":"codeinline","content":[{"type":"text","text":"SO_BACKLOG"}]},{"type":"text","text":"標識服務端接受連接的隊列長度,如果隊列已滿,客戶端連接將被拒絕。默認值,"},{"type":"codeinline","content":[{"type":"text","text":"Windows"}]},{"type":"text","text":"爲200,其他爲128,這裏設置的是100。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6、設置"},{"type":"codeinline","content":[{"type":"text","text":"Handler"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置 "},{"type":"codeinline","content":[{"type":"text","text":"ServerSocketChannel"}]},{"type":"text","text":"對應的"},{"type":"codeinline","content":[{"type":"text","text":"Handler"}]},{"type":"text","text":",這裏只能設置一個,它會在"},{"type":"codeinline","content":[{"type":"text","text":"SocketChannel"}]},{"type":"text","text":"建立起來之前執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"7、設置子Handler"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"serverBootstrap.childHandler(new ChannelInitializer() {\n @Override\n public void initChannel(SocketChannel ch) throws Exception {\n ChannelPipeline p = ch.pipeline();\n p.addLast(new LoggingHandler(LogLevel.INFO));\n p.addLast(new ChatNettyHandler());\n }\n});"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"中提供了一種可以設置多個"},{"type":"codeinline","content":[{"type":"text","text":"Handler"}]},{"type":"text","text":"的途徑,即使用"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer"}]},{"type":"text","text":"方式。"},{"type":"codeinline","content":[{"type":"text","text":"ChannelPipeline"}]},{"type":"text","text":"是"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"處理請求的責任鏈,這是一個"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":"的鏈表,而"},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":"就是用來處理網絡請求的內容的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一個"},{"type":"codeinline","content":[{"type":"text","text":"channel"}]},{"type":"text","text":",都有一個處理器流水線。裝配"},{"type":"codeinline","content":[{"type":"text","text":"child channel"}]},{"type":"text","text":"流水線,調用"},{"type":"codeinline","content":[{"type":"text","text":"childHandler()"}]},{"type":"text","text":"方法,傳遞一個"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer"}]},{"type":"text","text":" 的實例。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 "},{"type":"codeinline","content":[{"type":"text","text":"child channel"}]},{"type":"text","text":" 創建成功,開始通道初始化的時候,在bootstrap啓動器中配置的"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInitializer"}]},{"type":"text","text":" 實例就會被調用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個時候,才真正的執行去執行 "},{"type":"codeinline","content":[{"type":"text","text":"initChannel"}]},{"type":"text","text":" 初始化方法,開始通道流水線裝配。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流水線裝配,主要是在流水線"},{"type":"codeinline","content":[{"type":"text","text":"pipeline"}]},{"type":"text","text":"的後面,增加負責數據讀寫、處理業務邏輯的"},{"type":"codeinline","content":[{"type":"text","text":"handler"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"處理器 "},{"type":"codeinline","content":[{"type":"text","text":"ChannelHandler"}]},{"type":"text","text":" 用來處理網絡請求內容,有"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":"兩種,"},{"type":"codeinline","content":[{"type":"text","text":"ChannlPipeline"}]},{"type":"text","text":"會從頭到尾順序調用"},{"type":"codeinline","content":[{"type":"text","text":"ChannelInboundHandler"}]},{"type":"text","text":"處理網絡請求內容,從尾到頭調用"},{"type":"codeinline","content":[{"type":"text","text":"ChannelOutboundHandler"}]},{"type":"text","text":"處理網絡請求內容"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"8、綁定端口號"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ChannelFuture f = serverBootstrap.bind(PORT).sync();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綁定端口號"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"9、等待服務端端口號關閉"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"f.channel().closeFuture().sync();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"等待服務端監聽端口關閉,"},{"type":"codeinline","content":[{"type":"text","text":"sync()"}]},{"type":"text","text":"會阻塞主線程,內部調用的是 "},{"type":"codeinline","content":[{"type":"text","text":"Object"}]},{"type":"text","text":" 的 "},{"type":"codeinline","content":[{"type":"text","text":"wait()"}]},{"type":"text","text":"方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"10、關閉EventLoopGroup線程組"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"bossGroup.shutdownGracefully();\nworkerGroup.shutdownGracefully();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這篇文章主要是從一個"},{"type":"codeinline","content":[{"type":"text","text":"demo"}]},{"type":"text","text":"作爲引子,然後介紹了"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"的包結構、"},{"type":"codeinline","content":[{"type":"text","text":"Reactor"}]},{"type":"text","text":"模型、編程規範等等,目的很簡單,希望你能夠讀懂這段"},{"type":"codeinline","content":[{"type":"text","text":"demo"}]},{"type":"text","text":"並寫出來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後面開始繼續"},{"type":"codeinline","content":[{"type":"text","text":"Netty"}]},{"type":"text","text":"源碼解析部分,敬請期待。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考資料"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"《Netty in Action》書籍"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"慕課Netty專欄"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"掘金閃電俠Netty小冊"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"芋道源碼Netty專欄"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"Github[fork from krcys]"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"感謝Netty專欄作者們優秀的文章內容~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1]:https://juejin.im/post/6844903972843552776"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章