第一個Netty實踐DiscardServer
建立Netty專案
建立一個快速開始的Maven專案,匯入Netty4.0版本的依賴(我的JDK是1.8,官方建議1.6以上)。Netty依賴如下:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
複製程式碼
這裡使用了門面日誌框架Slf4j,所以需要引入slf4j和日誌實現框架logback。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
複製程式碼
服務端程式
先上程式碼,程式碼裡已有詳細註釋,具體的介紹後面再說。
public class NettyDiscardServer {
private static final Logger logger = LoggerFactory.getLogger(NettyDiscardServer.class);
final int port;
// 建立一個伺服器端的啟動器
ServerBootstrap sb = new ServerBootstrap();
NettyDiscardServer(int port) {
this.port = port;
}
public void runServer() {
// 建立反應器執行緒組
// 開啟一個執行緒(其實就是建立一個反應器類的物件)用於父通道,負責新連線的監聽
EventLoopGroup boss = new NioEventLoopGroup(1);
// 使用不帶引數的建構函式,預設執行緒數是CPU核數 * 2
// 開啟一個執行緒池用於子通道,負責通道的IO事件的處理
EventLoopGroup workers = new NioEventLoopGroup();
try {
// 設定反應器的執行緒組,一個父執行緒組,一個子執行緒組
sb.group(boss, workers)
// 設定NIO型別的通道
.channel(NioServerSocketChannel.class)
// 設定服務端監聽的埠
.localAddress("127.0.0.1", port)
// 設定通道的引數
// SO_KEEPALIVE指開啟TCP心跳檢測
.option(ChannelOption.SO_KEEPALIVE, true)
// Netty的記憶體在堆外直接記憶體上分配,可避免位元組緩衝區的二次拷貝
.option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT)
// 裝配子通道流水線
.childHandler(new ChannelInitializer<SocketChannel>() {
// 當有新連線到達時會建立一個通道
protected void initChannel(SocketChannel ch) {
// 向子通道流水線新增第一個Handler處理器
ch.pipeline().addLast(new NettyDiscardHandler());
// 向子通道流水線新增第二個Handler處理器
ch.pipeline().addLast(new NettyDiscardHandler1());
}
});
// 開始繫結伺服器
// 呼叫sync同步方法阻塞直到繫結成功
ChannelFuture channel = sb.bind().sync();
logger.info("伺服器啟動成功,監聽埠:" + channel.channel().localAddress());
// 伺服器監聽通道會一直等待 通道關閉的非同步任務結束
ChannelFuture close = channel.channel().closeFuture();
// 同步關閉
close.sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放所有使用的執行緒,釋放所有資源(包括關閉通道和選擇器)
boss.shutdownGracefully();
workers.shutdownGracefully();
}
}
public static void main(String[] args) {
new NettyDiscardServer(8888).runServer();
}
}
複製程式碼
業務處理器
在Netty的反應器模式中,所有的業務處理都在Handler處理器中完成。這裡寫一個簡單的入站處理(讀取訊息不回覆),以及演示簡單的通道流水線處理。這裡兩個類我都是做為上面伺服器類的內部類實現的。
// 繼承netty預設實現的入站處理器,在內部實現自己的業務邏輯
static class NettyDiscardHandler extends ChannelInboundHandlerAdapter {
// 讀取緩衝區中的資料
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 將資料轉為ByteBuf(類似Java NIO中的ByteBuffer)類來處理
ByteBuf buf = (ByteBuf) msg;
try {
logger.info("收到訊息 : ");
// 迴圈讀取緩衝區中的字元
while (buf.isReadable()) {
System.out.print((char) buf.readByte());
}
System.out.println();
} finally {
// 通過 通道處理器上下文 將訊息傳播到下一個處理器節點
ctx.fireChannelRead(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 遇到異常時關閉連線
cause.printStackTrace();
ctx.close();
}
}
static class NettyDiscardHandler1 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
logger.info("我是第二個處理器");
// 拋棄訊息
ReferenceCountUtil.release(msg);
}
}
複製程式碼
簡單的客戶端程式
實現一個簡單的想伺服器傳送訊息的客戶端。
public class NettyDiscardClient {
public static void main(String[] args) throws IOException {
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
channel.configureBlocking(false);
// 自旋,等待連線完成
while (!channel.finishConnect());
// 向服務端傳送一個訊息
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("hello world!".getBytes());
buf.flip();
channel.write(buf);
channel.close();
}
}
複製程式碼
執行伺服器和客戶端。其中可以看到訊息被兩個處理器處理了,這就是通道的流水線處理。
[main] INFO discardServer.NettyDiscardServer - 伺服器啟動成功,監聽埠:/127.0.0.1:8888
[nioEventLoopGroup-3-1] INFO discardServer.NettyDiscardServer - 收到訊息 : hello world!
[nioEventLoopGroup-3-1] INFO discardServer.NettyDiscardServer - 我是第二個處理器
複製程式碼
伺服器啟動程式碼分析
Netty提供了一個非常便利的工廠類Bootstrap,可以用它來方便的完成netty元件的組裝和配置。
父子通道
在Netty中,將有接收關係的NioServerSocketChannel和NioSocketChannel,叫做父子通道。
- NioServerSocketChannel負責伺服器端新連線的監聽和接收,叫做父通道。
- NioSocketChannel負責通道間資料的傳輸,叫做子通道。
EventLoopGroup執行緒組
在Netty中,一個EventLoop就是一個反應器。由於Netty是多執行緒版本的反應器模式,所以使用EventLoopGroup來實現多執行緒版本的反應器。
EventLoopGroup一般常用兩個建構函式,一個是帶引數的(用於指定執行緒數),一個是不帶引數的。下面來看一下內部原始碼。
// 帶引數,直接指定執行緒數
public NioEventLoopGroup(int nThreads) {
this(nThreads, (Executor) null);
}
// 不帶引數,內部呼叫帶引數的建構函式,傳入的引數為0
public NioEventLoopGroup() {
this(0);
}
複製程式碼
對於無參的建構函式,傳入的執行緒數居然是0,帶著疑問繼續往原始碼裡鑽。經過層層呼叫,最後找到了,是呼叫了父類MultithreadEventLoopGroup的建構函式。
protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
}
複製程式碼
可以發現,如果傳入的執行緒數為0,則把預設的DEFAULT_EVENT_LOOP_THREADS賦值給執行緒數nThreads。這個DEFAULT_EVENT_LOOP_THREADS是多少呢,在父類中找找看。
static {
// 將CPU核數 * 2 作為預設執行緒數
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
// 如果日誌開啟了debug模式,則列印出開啟的執行緒數
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
複製程式碼
通過原始碼可以發現,如果傳入一個無參的建構函式,則預設開始CPU核數 * 2個EventLoop執行緒。
另外,開篇的伺服器端程式碼中,建立了兩個執行緒組例項。其中一個用於負責新連線的監聽和連線,查詢父通道的IO事件(這裡用帶引數的建構函式建立了一個執行緒)。還有一個執行緒組用於負責查詢所有子通道的IO事件,並執行Handler處理器中的業務處理。
Bootstrap的啟動流程
Bootstrap可以方便的組裝和配置Netty的元件。下面介紹一下流程。
- 建立一個伺服器端的啟動器ServerBootstrap。
ServerBootstrap sb = new ServerBootstrap();
複製程式碼
- 建立反應器執行緒組,並賦值給ServerBootstrap。
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup workers = new NioEventLoopGroup();
sb.group(boss, workers); // 對應的方法原型 ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
複製程式碼
- 設定通道的IO型別,這裡伺服器端一般使用NioServerSocketChannel,也就是Java NIO型別。
sb.channel(NioServerSocketChannel.class) // 對應的方法原型 B channel(Class<? extends C> channelClass)
複製程式碼
- 設定監聽的地址和埠。
sb.localAddress("127.0.0.1", port) // 對應的方法原型 B localAddress(String inetHost, int inetPort)
複製程式碼
- 設定傳輸通道的配置選項,這裡是給父通道接收連線通道設定一些選項。對於ChannelOption常量的分析。參考Netty支援的ChannelOption分析
sb.option(ChannelOption.SO_KEEPALIVE, true)
sb.option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT) // 對應的方法原型 B option(ChannelOption<T> option, T value)
複製程式碼
- 裝配子通道的Pipeline流水線,通過呼叫childHandler方法,傳入一個ChannelInitializer通道初始化類的例項(這個例項的泛型需要與前面的啟動器中設定的通道型別對應)。在父通道成功接收一個連線,並建立成功一個子通道後,就會初始化子通道,這個例項就會被呼叫。在這個例項中有一個initChannel初始化方法,用於向子通道流水線新增業務處理器。
sb.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyDiscardHandler());
ch.pipeline().addLast(new NettyDiscardHandler1());
}
});
複製程式碼
- 繫結伺服器新連線的監聽埠。內部通過呼叫一個doBind(final SocketAddress localAddress)建立一個之前設定的新連線通道型別例項,並將這個通道例項和地址埠繫結。這裡呼叫了同步阻塞方法,阻塞到地址埠繫結成功為止,伺服器正式啟動啦。
ChannelFuture channel = sb.bind().sync();
複製程式碼
- 自我阻塞,直到通道關閉。當通道關閉時,closeFuture例項的sync方法會返回。
// 獲取一個closeFuture例項
ChannelFuture close = channel.channel().closeFuture();
// 自我阻塞,直到通道關閉,就返回。
close.sync();
複製程式碼
- 關閉EventLoopGroup。同時會關閉內部的反應器執行緒以及選擇器和所有子通道。並釋放掉底層的資源。
boss.shutdownGracefully();
workers.shutdownGracefully();
複製程式碼
Netty中的反應器模式
反應器模式中的IO處理流程
在Reactor模式中的IO處理流程圖如下: [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-XWq76zbM-1575900038358)(i.loli.net/2019/12/09/…)]
- 通道註冊:首先需要將通道註冊到選擇器中,通道中就緒的IO事件才能被選擇器查詢到。 2.查詢選擇:一個反應器對應一個執行緒,不斷的輪詢查詢選擇器中的IO就緒事件。
- 事件分發:如果查詢到就緒的IO事件,就分發給繫結在對應通道上的處理器去處理。
- 業務處理:完成真正的IO操作和業務處理。
Netty中的Channel元件
Netty中沒有使用Java NIO的Channel,而是實現了自己的Channel通道元件。由於Netty還能處理OIO,所以也封裝了對應OIO通道元件。
但是在Netty中使用最多的還是NioSocketChannel(非同步非阻塞TCP Socket傳輸通道),NioServerSocketChannel(非同步非阻塞TCP Socket伺服器端監聽通道)。
其中有個好玩的通道就是EmbeddedChannel(嵌入式通道),這個通道主要是用於單元測試的(它可以讓開發人員在開發中方便的模擬入站與出站的操作,而不進行底層實際的傳輸)。因為Netty實際開發中主要就是開發業務處理器,但是對於業務處理器的測試需要用到客戶端和伺服器的操作,每次測試都要開啟兩個程式測試很麻煩,所以就有了EmbeddedChannel。除了不進行傳輸之外,其它處理流程和真實的傳輸通道的是一樣的。
其中主要的API如下:
名稱 | 說明 |
---|---|
writeInbound (Object... msgs) | 向通道中寫入inbound入站資料,模擬通道收到客戶端的資料 |
readInbound() | 從EmbeddedChannel通道中讀取入站資料,也就是返回入站處理器處理完的資料,如果沒有資料,返回NULL |
writeOutbound (Object... msgs) | 向通道中寫入outbound出站資料,模擬通道傳送資料到客戶端。 |
readOutbound() | 從EmbeddedChannel通道中讀取出站資料,類似readInbound。 |
flush() | 結束EmbeddedChannel,它會呼叫通道的close方法,並釋放所有資料的底層資源。 |
Netty中的Reactor反應器
Netty中的反應器有多個實現類,對應於Channel通道。NioSocketChannel對應的反應器類是NioEventLoop。
NioEventLoop有兩個重要成員,一個是Thread執行緒類的成員,一個是Java NIO選擇器的成員。
一個NioEventLoop擁有一個Thread執行緒,負責一個Java NIO選擇器的IO事件輪詢。但是一個NioEventLoop反應器可以對應多個NioSocketChannel通道。
Netty中的Handler處理器
Netty中有兩大類的Handler處理器。他們都繼承與ChannelHandler介面。
- ChannelInboundHandler入站處理器,最常用的channelRead用於從通道中讀取資料並處理。對應的Netty預設實現為ChannelInboundHandlerAdapter入站處理介面卡。
- 是ChannelIOutboundHandler出站處理器,最常用的channelWrite用於把資料寫入通道中。對應的Netty預設實現為ChannelOutboundHandlerAdapter出站處理介面卡。
Handler處理器的生命週期:新增處理器到流水線 -> 註冊到流水線中 -> 通道啟用後回撥 -> 入站方法回撥 -> 底層連線關閉 -> 移除註冊 -> 從流水線中刪除。
Netty的Pipeline流水線
在Netty中通道和處理器例項的關係為一對多。這裡就用到了通道流水線(ChannelPipeline)。一個通道(有一個pipeline成員)擁有一個通道流水線。
通道流水線ChannelPipeline基於責任鏈模式設計的,內部是一個雙向連結串列結構,每一個節點對應一個ChannelHandlerContext處理器上下文物件(處理器新增到流水線時,會建立一個通道處理器上下文,裡面包裹了一個ChannelHandler處理器物件,並關聯著ChannelPipeline流水線。用於控制流水線上處理器的傳播),可以支援動態的增加和刪除處理器上下文物件。
在通道流水線中,IO事件按照既定的順序處理。入站處理器的執行次序為從前往後,而出站處理器的次序為從後往前。