Netty之旅二:口口相傳的高效能Netty到底是什麼?
阿新 • • 發佈:2020-08-25
![d0iosx.png](https://upload-images.jianshu.io/upload_images/5660078-5cc3f85b621388a8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
高清思維導圖原件(`xmind/pdf/jpg`)可以關注公眾號:`一枝花算不算浪漫` 回覆`netty01`即可。
## 前言
上一篇文章講了`NIO`相關的知識點,相比於傳統`IO`,`NIO`已經做得很優雅了,為什麼我們還要使用`Netty`?
上篇文章最後留了很多坑,講了`NIO`使用的弊端,也是為了引出`Netty`而設立的,這篇文章我們就來好好揭開`Netty`的神祕面紗。
本篇文章的目的很簡單,希望看過後你能看懂`Netty`的示例程式碼,針對於簡單的網路通訊,自己也能用`Netty`手寫一個開發應用出來!
## 一個簡單的Netty示例
以下是一個簡單聊天室Server端的程式,程式碼參考自:`http://www.imooc.com/read/82/article/2166`
程式碼有點長,主要核心程式碼是在`main()`方法中,這裡程式碼也希望大家看懂,後面也會一步步剖析。
PS:我是用`mac`系統,直接在終端輸入`telnet 127.0.0.1 8007` 即可啟動一個聊天框,如果提示找不到`telnet`命令,可以通過`brew`進行安裝,具體步驟請自行百度。
```java
/**
* @Description netty簡易聊天室
*
* @Author 一枝花算不算浪漫
* @Date 2020/8/10 6:52 上午
*/
public final class NettyChatServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// 1. EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 2. 服務端引導器
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 3. 設定線bootStrap資訊
serverBootstrap.group(bossGroup, workerGroup)
// 4. 設定ServerSocketChannel的型別
.channel(NioServerSocketChannel.class)
// 5. 設定引數
.option(ChannelOption.SO_BACKLOG, 100)
// 6. 設定ServerSocketChannel對應的Handler,只能設定一個
.handler(new LoggingHandler(LogLevel.INFO))
// 7. 設定SocketChannel對應的Handler
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 可以新增多個子Handler
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new ChatNettyHandler());
}
});
// 8. 繫結埠
ChannelFuture f = serverBootstrap.bind(PORT).sync();
// 9. 等待服務端監聽埠關閉,這裡會阻塞主執行緒
f.channel().closeFuture().sync();
} finally {
// 10. 優雅地關閉兩個執行緒池
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private static class ChatNettyHandler extends SimpleChannelInboundHandler {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("one conn active: " + ctx.channel());
// channel是在ServerBootstrapAcceptor中放到EventLoopGroup中的
ChatHolder.join((SocketChannel) ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String content = new String(bytes, StandardCharsets.UTF_8);
System.out.println(content);
if (content.equals("quit\r\n")) {
ctx.channel().close();
} else {
ChatHolder.propagate((SocketChannel) ctx.channel(), content);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("one conn inactive: " + ctx.channel());
ChatHolder.quit((SocketChannel) ctx.channel());
}
}
private static class ChatHolder {
static final Map USER_MAP = new ConcurrentHashMap<>();
/**
* 加入群聊
*/
static void join(SocketChannel socketChannel) {
// 有人加入就給他分配一個id
String userId = "使用者"+ ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
send(socketChannel, "您的id為:" + userId + "\n\r");
for (SocketChannel channel : USER_MAP.keySet()) {
send(channel, userId + " 加入了群聊" + "\n\r");
}
// 將當前使用者加入到map中
USER_MAP.put(socketChannel, userId);
}
/**
* 退出群聊
*/
static void quit(SocketChannel socketChannel) {
String userId = USER_MAP.get(socketChannel);
send(socketChannel, "您退出了群聊" + "\n\r");
USER_MAP.remove(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + " 退出了群聊" + "\n\r");
}
}
}
/**
* 擴散說話的內容
*/
public static void propagate(SocketChannel socketChannel, String content) {
String userId = USER_MAP.get(socketChannel);
for (SocketChannel channel : USER_MAP.keySet()) {
if (channel != socketChannel) {
send(channel, userId + ": " + content);
}
}
}
/**
* 傳送訊息
*/
static void send(SocketChannel socketChannel, String msg) {
try {
ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
ByteBuf writeBuffer = allocator.buffer(msg.getBytes().length);
writeBuffer.writeCharSequence(msg, Charset.defaultCharset());
socketChannel.writeAndFlush(writeBuffer);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
```
![dkeb0s.png](https://upload-images.jianshu.io/upload_images/5660078-dcb49953adad8127.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
程式碼有點長,執行完的效果如上圖所示,下面所有內容都是圍繞著`如何看懂`以及`如何寫出`這樣的程式碼來展開的,希望你看完 也能輕鬆手寫`Netty`服務端程式碼~。通過簡單demo開發讓大家體驗了`Netty`實現相比`NIO`確實要簡單的多,但優點不限於此,只需要知道選擇Netty就對了。
## Netty核心元件
對應著文章開頭的思維導圖,我們知道`Netty`的核心元件主要有:
- Bootstrap && ServerBootstrap
- EventLoopGroup
- EventLoop
- ByteBuf
- Channel
- ChannelHandler
- ChannelFuture
- ChannelPipeline
- ChannelHandlerContext
類圖如下:
![dk8ZC9.png](https://upload-images.jianshu.io/upload_images/5660078-dcf6d0f8af600df7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
### Bootstrap & ServerBootstrap
一看到`BootStrap`大家就應該想到**啟動類、引導類**這樣的詞彙,之前分析過[EurekaServer專案啟動類時][1]介紹過`EurekaBootstrap`, 他的作用就是上下文初始化、配置初始化。
在`Netty`中我們也有類似的類,`Bootstrap`和`ServerBootstrap`它們都是`Netty`程式的引導類,主要用於配置各種引數,並啟動整個`Netty`服務,我們看下文章開頭的示例程式碼:
```java
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new ChatNettyHandler());
}
});
```
`Bootstrap`和`ServerBootstrap`是針對於`Client`和`Server`端定義的兩套啟動類,區別如下:
- `Bootstrap`是客戶端引導類,而`ServerBootstrap`是服務端引導類。
- `Bootstrap`通常使用`connect()`方法連線到遠端的主機和埠,作為一個`TCP客戶端`。
- `ServerBootstrap`通常使用`bind()`方法繫結本地的埠,等待客戶端來連線。
- `ServerBootstrap`可以處理`Accept`事件,這裡面`childHandler`是用來處理`Channel`請求的,我們可以檢視`chaildHandler()`方法的註解:
![dk884H.png](https://s1.ax1x.com/2020/08/15/dk884H.png)
- `Bootstrap`客戶端引導只需要一個`EventLoopGroup`,但是一個`ServerBootstrap`通常需要兩個(上面的`boosGroup`和`workerGroup`)。
### EventLoopGroup && EventLoop
`EventLoopGroup`及`EventLoop`這兩個類名稱定義的很奇怪,對於初學者來說往往無法通過名稱來了解其中的含義,包括我也是這樣。
`EventLoopGroup` 可以理解為一個執行緒池,對於服務端程式,我們一般會繫結兩個執行緒池,一個用於處理 `Accept` 事件,一個用於處理讀寫事件,看下`EventLoop`系列的類目錄:
![dU4Roj.png](https://upload-images.jianshu.io/upload_images/5660078-20c7c39cb233e867.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
通過上面的類圖,我們才恍然大悟,我的親孃咧,這不就是一個執行緒池嘛?(名字氣的犄角拐彎的真是難認)
`EventLoopGroup`是`EventLoop`的集合,一個`EventLoopGroup` 包含一個或者多個`EventLoop`。我們可以將`EventLoop`看做`EventLoopGroup`執行緒池中的一個個工作執行緒。
至於這裡為什麼要用到兩個執行緒池,具體的其實可以參考`Reactor`設計模式,這裡暫時不做過多的講解。
- 一個 EventLoopGroup 包含一個或多個 EventLoop ,即 EventLoopGroup : EventLoop = 1 : n
- 一個 EventLoop 在它的生命週期內,只能與一個 Thread 繫結,即 EventLoop : Thread = 1 : 1
- 所有有 EventLoop 處理的 I/O 事件都將在它專有的 Thread 上被處理,從而保證執行緒安全,即 Thread : EventLoop = 1 : 1
- 一個 Channel 在它的生命週期內只能註冊到一個 EventLoop 上,即 Channel : EventLoop = n : 1
- 一個 EventLoop 可被分配至一個或多個 Channel ,即 EventLoop : Channel = 1 : n
當一個連線到達時,`Netty` 就會建立一個 `Channel`,然後從 `EventLoopGroup` 中分配一個 `EventLoop` 來給這個 `Channel` 繫結上,在該 `Channel` 的整個生命週期中都是有這個繫結的 `EventLoop` 來服務的。
### ByteBuf
在`Java NIO`中我們有 `ByteBuffer`緩衝池,對於它的操作我們應該印象深刻,往`Buffer`中寫資料時我們需要關注寫入的位置,切換成讀模式時我們還要切換讀寫狀態,不然將會出現大問題。
針對於`NIO`中超級難用的`Buffer`類, `Netty` 提供了`ByteBuf`來替代。`ByteBuf`聲明瞭兩個指標:一個讀指標,一個寫指標,使得讀寫操作進行分離,簡化`buffer`的操作流程。
![dkQocV.png](https://upload-images.jianshu.io/upload_images/5660078-b10ced8238b3b66e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
另外`Netty`提供了發幾種`ByteBuf`的實現以供我們選擇,`ByteBuf`可以分為:
- `Pooled`和`Unpooled` 池化和非池化
- Heap 和 Direct,堆記憶體和堆外記憶體,NIO中建立Buffer也可以指定
- Safe 和 Unsafe,安全和非安全
![dkJ9TU.png](https://upload-images.jianshu.io/upload_images/5660078-fed9e32fd4a0cdf2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
對於這麼多種建立`Buffer`的方式該怎麼選擇呢?`Netty`也為我們處理好了,我們可以直接使用(真是暖男`Ntetty`):
```java
ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
ByteBuf buffer = allocator.buffer(length);
```
使用這種方式,Netty將最大努力的使用池化、Unsafe、對外記憶體的方式為我們建立buffer。
### Channel
提起`Channel`並不陌生,上一篇講`NIO`的三大元件提到過,最常見的就是`java.nio.SocketChannel`和`java.nio.ServerSocketChannel`,他們用於非阻塞的I/0操作。類似於`NIO`的`Channel`,Netty提供了自己的`Channel`和其子類實現,用於非同步I/0操作和其他相關的操作。
在 `Netty` 中, `Channel` 是一個 `Socket` 連線的抽象, 它為使用者提供了關於底層 `Socket` 狀態(是否是連線還是斷開) 以及對 `Socket` 的讀寫等操作。每當 `Netty` 建立了一個連線後, 都會有一個對應的 `Channel` 例項。並且,有父子`channel`的概念。 伺服器連線監聽的`channel` ,也叫 `parent channel`。 對應於每一個 `Socket` 連線的`channel`,也叫 `child channel`。
既然`channel` 是 Netty 抽象出來的網路 I/O 讀寫相關的介面,為什麼不使用` JDK NIO` 原生的 `Channel` 而要另起爐灶呢,主要原因如下:
- `JDK` 的` SocketChannel` 和 `ServersocketChannel `沒有統一的 `Channel` 介面供業務開發者使用,對一於使用者而言,沒有統一的操作檢視,使用起來並不方便。
- `JDK` 的 `SocketChannel `和 `ScrversockctChannel `的主要職責就是網路 I/O 操作,由於他們是` SPI` 類介面,由具體的虛擬機器廠家來提供,所以通過繼承 SPI 功能直接實現 `ServersocketChannel` 和 `SocketChannel` 來擴充套件其工作量和重新` Channel` 功類是差不多的。
- Netty 的 `ChannelPipeline Channel` 需要夠跟 Netty 的整體架構融合在一起,例如 I/O 模型、基的定製模型,以及基於元資料描述配置化的 TCP 引數等,這些` JDK SocketChannel` 和` ServersocketChannel `都沒有提供,需要重新封裝。
- 自定義的 `Channel` ,功實現更加靈活。
基於上述 4 原因,它的設計原理比較簡單, Netty 重新設計了 `Channel` 介面,並且給予了很多不同的實現。但是功能卻比較繁雜,主要的設計理念如下:
- 在 `Channel` 介面層,相關聯的其他操作封裝起來,採用 `Facade` 模式進行統一封裝,將網路 I/O 操作、網路 I/O 統一對外提供。
- `Channel` 介面的定義儘量大而全,統一的檢視,由不同子類實現不同的功能,公共功能在抽象父類中實現,最大程度上實現介面的重用。
- 具體實現採用聚合而非包含的方式,將相關的功類聚合在 `Channel `中,由 `Channel` 統一負責分配和排程,功能實現更加靈活。
`Channel `的實現類非常多,繼承關係複雜,從學習的角度我們抽取最重要的兩個 `NioServerSocketChannel `和 `NioSocketChannel`。
服務端 `NioServerSocketChannel `的繼承關係類圖如下:
![dUn8G4.png](https://upload-images.jianshu.io/upload_images/5660078-a82b11a120a58c85.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
客戶端 `NioSocketChannel `的繼承關係類圖如下:
![dUnJz9.png](https://upload-images.jianshu.io/upload_images/5660078-321c8b4f8e0dda6f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
後面文章原始碼系列會具體分析,這裡就不進一步闡述分析了。
### ChannelHandler
`ChannelHandler` 是`Netty`中最常用的元件。`ChannelHandler` 主要用來處理各種事件,這裡的事件很廣泛,比如可以是連線、資料接收、異常、資料轉換等。
`ChannelHandler` 有兩個核心子類 `ChannelInboundHandler` 和 `ChannelOutboundHandler`,其中 `ChannelInboundHandler` 用於接收、處理入站( `Inbound` )的資料和事件,而 `ChannelOutboundHandler` 則相反,用於接收、處理出站( `Outbound` )的資料和事件。
![dkJAp9.png](https://upload-images.jianshu.io/upload_images/5660078-b76bf9284af32276.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#### ChannelInboundHandler
`ChannelInboundHandler`處理入站資料以及各種狀態變化,當`Channel`狀態發生改變會呼叫`ChannelInboundHandler`中的一些生命週期方法.這些方法與`Channel`的生命密切相關。
入站資料,就是進入`socket`的資料。下面展示一些該介面的生命週期`API`:
![dUntMR.png](https://upload-images.jianshu.io/upload_images/5660078-016fadc9b5211ee0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
當某個 `ChannelInboundHandler`的實現重寫 `channelRead()`方法時,它將負責顯式地釋放與池化的 `ByteBuf` 例項相關的記憶體。 Netty 為此提供了一個實用方法`ReferenceCountUtil.release()`。
```java
@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.release(msg);
}
}
```
這種方式還挺繁瑣的,Netty提供了一個`SimpleChannelInboundHandler`,重寫`channelRead0()`方法,就可以在呼叫過程中會自動釋放資源.
```java
public class SimpleDiscardHandler
extends SimpleChannelInboundHandler