【Netty】(5)原始碼 Bootstrap
【Netty】5 原始碼 Bootstrap
上一篇講了AbstractBootstrap,為這篇做了個鋪墊。
一、概述
Bootstrap 是 Netty 提供的一個便利的工廠類, 我們可以通過它來完成 Netty 的客戶端或伺服器端的 Netty 初始化.
Bootstrap
: 用於客戶端,只需要一個單獨的Channel,來與服務端進行資料互動,對應server端的子Channel。
作用職責
:EventLoop初始化,channel的註冊過程 ,關於pipeline的初始化,handler的新增過程,客戶端連線分析。
Netty客戶端原始碼部分
EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) // 註冊執行緒池 .channel(NioSocketChannel.class) // 使用NioSocketChannel來作為連線用的channel類 .handler(new ChannelInitializer<SocketChannel>() { // 繫結連線初始化器 @Override protected void initChannel(SocketChannel ch) throws Exception { //這裡放入自定義助手類 ch.pipeline().addLast(new EchoClientHandler()); } }); ChannelFuture cf = b.connect(host, port).sync(); // 非同步連線伺服器 cf.channel().closeFuture().sync(); // 非同步等待關閉連線channel } finally { group.shutdownGracefully().sync(); // 釋放執行緒池資源 } }
從上面的客戶端程式碼雖然簡單, 但是卻展示了 Netty 客戶端初始化時所需的所有內容:
1. EventLoopGroup: 不論是伺服器端還是客戶端, 都必須指定 EventLoopGroup. 在這個例子中,
指定了 NioEventLoopGroup, 表示一個 NIO 的EventLoopGroup.
2. ChannelType: 指定 Channel 的型別. 因為是客戶端, 因此使用了 NioSocketChannel.
3. Handler: 設定資料的處理器.
4. 這裡的option,提供了一系列的TCP引數
下面我們深入程式碼, 看一下客戶端通過 Bootstrap 啟動後, 都做了哪些工作.
二、原始碼分析
1、group(group)
/** * 直接呼叫父類AbstractBootstrap的方法 */ public B group(EventLoopGroup group) { if (group == null) { throw new NullPointerException("group"); } if (this.group != null) { throw new IllegalStateException("group set already"); } this.group = group; return self(); }
直接呼叫父類的方法 ,說明該EventLoopGroup,作為客戶端 Connector 執行緒,負責註冊監聽連線操作位,用於判斷非同步連線結果。
2、channel(NioServerSocketChannel.class)
在 Netty 中, Channel是一個Socket的抽象, 它為使用者提供了關於 Socket 狀態(是否是連線還是斷開) 以及對 Socket 的讀寫等操作. 每當 Netty 建立了一個連線後, 都會有一個對應的 Channel 例項。
2.1原始碼
/**
* 同樣也是直接呼叫父類AbstractBootstrap的方法
*/
public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
我們再來看下ReflectiveChannelFactory類
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {
private final Class<? extends T> clazz;
/**
* 通過建構函式 傳入 clazz
*/
public ReflectiveChannelFactory(Class<? extends T> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
this.clazz = clazz;
}
/**
* 只用這一個方法 通過傳入不同的Channel.class 建立不同的Channel 物件。
* newChannel() 什麼時候呼叫呢 仔細追原始碼 發現是在繫結 IP 和 埠的 doResolveAndConnect方法裡會呼叫
*/
@Override
public T newChannel() {
try {
return clazz.getConstructor().newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
在看channelFactory(new ReflectiveChannelFactory
/**
* 建立好Channel後,返回物件Bootstrap本身
*/
@Deprecated
public B channelFactory(ChannelFactory<? extends C> channelFactory) {
if (channelFactory == null) {
throw new NullPointerException("channelFactory");
}
if (this.channelFactory != null) {
throw new IllegalStateException("channelFactory set already");
}
this.channelFactory = channelFactory;
return self();
}
因此對於我們這個例子中的客戶端的 Bootstrap 而言, 生成的的 Channel 例項就是 NioSocketChannel。
2.2 Channel 型別
除了 TCP 協議以外, Netty 還支援很多其他的連線協議, 並且每種協議還有 NIO(非同步 IO) 和 OIO(Old-IO, 即傳統的阻塞 IO) 版本的區別. 不同協議不同的阻塞型別的連線都有不同的 Channel 型別與之對應下面是一些常用的 Channel 型別:
- NioSocketChannel, 代表非同步的客戶端 TCP Socket 連線.
- NioServerSocketChannel, 非同步的伺服器端 TCP Socket 連線.
- NioDatagramChannel, 非同步的 UDP 連線
- NioSctpChannel, 非同步的客戶端 Sctp 連線.
- NioSctpServerChannel, 非同步的 Sctp 伺服器端連線.
- OioSocketChannel, 同步的客戶端 TCP Socket 連線.
- OioServerSocketChannel, 同步的伺服器端 TCP Socket 連線.
- OioDatagramChannel, 同步的 UDP 連線
- OioSctpChannel, 同步的 Sctp 伺服器端連線.
- OioSctpServerChannel, 同步的客戶端 TCP Socket 連線.
3、handler(ChannelHandler handler)
Netty 的一個強大和靈活之處就是基於 Pipeline 的自定義 handler 機制
. 基於此, 我們可以像新增外掛一樣自由組合各種各樣的 handler 來完成業務邏輯. 例如我們需要處理 HTTP 資料, 那麼就可以在 pipeline 前新增一個 Http 的編解碼的 Handler, 然後接著新增我們自己的業務邏輯的 handler, 這樣網路上的資料流就向通過一個管道一樣, 從不同的 handler 中流過並進行編解碼, 最終在到達我們自定義的 handler 中。
/**
* 同樣也是 直接呼叫父類 AbstractBootstrap 的方法
*/
public B handler(ChannelHandler handler) {
if (handler == null) {
throw new NullPointerException("handler");
}
this.handler = handler;
return self();
}
不過我們看到程式碼 一般都是這樣寫的
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoClientHandler());
}
})
那是因為Bootstrap.handler 方法接收一個 ChannelHandler, 而我們傳遞的是一個 派生於 ChannelInitializer 的匿名類, 它正好也實現了 ChannelHandler 介面. 我們來看一下, ChannelInitializer 類部分程式碼:
/**
* ChannelInboundHandlerAdapter 父類的父類 最終會繼承 ChannelHandler
* 那麼ChannelInitializer 也就是 ChannelHandler的 子類
*/
public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {
private static final InternalLogger logger
=InternalLoggerFactory.getInstance(ChannelInitializer.class);
/**
* 這裡只有這一個抽象類 所以我們只需重寫這一個方法就可以了
*/
protected abstract void initChannel(C ch) throws Exception;
@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
initChannel((C) ctx.channel());
ctx.pipeline().remove(this);
ctx.fireChannelRegistered();
}
}
ChannelInitializer 是一個抽象類, 它有一個抽象的方法 initChannel
, 我們正是實現了這個方法, 並新增的自定義的 handler 的. 那麼 initChannel 是哪裡被呼叫的呢?
答案是 ChannelInitializer.channelRegistered 方法中。
我們來關注一下 channelRegistered 方法. 從上面的原始碼中, 我們可以看到, 在 channelRegistered 方法中, 會呼叫 initChannel 方法, 將自定義的 handler 新增到 ChannelPipeline 中, 然後呼叫 ctx.pipeline().remove(this) 將自己從 ChannelPipeline 中刪除. 上面的分析過程, 可以用如下圖片展示:
一開始, ChannelPipeline 中只有三個 handler, head, tail 和我們新增的 ChannelInitializer.
接著 initChannel 方法呼叫後, 添加了自定義的 handler
最後將 ChannelInitializer 刪除
4、ChannelPipeline物件
/**
* 我們在initChannel抽象方法的實現方法中 通過 SocketChannel獲得 ChannelPipeline物件
*/
ChannelPipeline p = ch.pipeline();
p.addLast(newEchoClientHandler());
在例項化一個 Channel 時, 會伴隨著一個 ChannelPipeline 的例項化
, 並且此 Channel 會與這個 ChannelPipeline 相互關聯, 這一點可以通過NioSocketChannel 的父類 AbstractChannel 的構造器:
protected AbstractChannel(Channel parent) {
this.parent = parent;
unsafe = newUnsafe();
//這個可以看出
pipeline = new DefaultChannelPipeline(this);
}
當例項化一個 Channel(這裡以 EchoClient 為例, 那麼 Channel 就是 NioSocketChannel), 其 pipeline 欄位就是我們新建立的 DefaultChannelPipeline 物件, 那麼我們就來看一下 DefaultChannelPipeline 的構造方法。
public DefaultChannelPipeline(AbstractChannel channel) {
if (channel == null) {
throw new NullPointerException("channel");
}
this.channel = channel;
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
我們呼叫 DefaultChannelPipeline 的構造器, 傳入了一個 channel, 而這個 channel 其實就是我們例項化的 NioSocketChannel, DefaultChannelPipeline 會將這個 NioSocketChannel 物件儲存在channel 欄位中。DefaultChannelPipeline 中, 還有兩個特殊的欄位, 即head
和tail
, 而這兩個欄位是一個雙向連結串列的頭和尾
. 其實在 DefaultChannelPipeline 中, 維護了一個以 AbstractChannelHandlerContext 為節點的雙向連結串列, 這個連結串列是 Netty 實現 Pipeline 機制的關鍵。
5、.connect(host, port)
經過上面的各種分析後, 我們大致瞭解了 Netty 初始化時, 所做的工作, 接下來 分析一下客戶端是如何發起 TCP 連線的。
/**
* 1、 這裡 終於是Bootstrap 自己的方法了。 傳入IP 地址 和 埠號
*/
public ChannelFuture connect(String inetHost, int inetPort) {
//通過InetSocketAddress 建構函式 繫結 IP地址+埠號
return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
/**
* 2、上面呼叫該方法 ,該方法在呼叫 doResolveAndConnect方法
*/
public ChannelFuture connect(SocketAddress remoteAddress) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
validate();
return doResolveAndConnect(remoteAddress, config.localAddress());
}
/**
* 3、這步 例項化 Channer
*/
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
//注意 這裡 initAndRegister()方法就是例項化 Channer 的方法 上面說過 真正獲取Channer 物件 是在這步獲取的
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
// 這裡省略的 很大一部分邏輯判斷的程式碼
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
}
/**
* 3.1 這裡 就開始 調 newChannel() 方法 也就建立了 Channel 物件
*/
final ChannelFuture initAndRegister() {
Channel channel = null;
channel = channelFactory.newChannel();
return regFuture;
}
/**
* 4、在看doResolveAndConnect0方法
* 這一步還是對一些 引數資料 進行校驗 省略了校驗程式碼
*/
private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
final SocketAddress localAddress, final ChannelPromise promise) {
// 獲取 當前 EventLoop執行緒
final EventLoop eventLoop = channel.eventLoop();
final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);
final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);
//這一步 才是 連線的關鍵
doConnect(resolveFuture.getNow(), localAddress, promise);
return promise;
}
接下來看重要的方法,在 connect 中, 會進行一些引數檢查後, 最終呼叫的是 doConnect 方法,有關doConnect之後接下來原始碼,等自己對Netty瞭解更細緻之後 ,再來寫吧。
這裡推薦一個博主,有關Netty原始碼分析的蠻好的:原始碼之下無祕密 ── 做最好的 Netty 原始碼分析教程
如果一個人充滿快樂,正面的思想,那麼好的人事物就會和他共鳴,而且被他吸引過來。同樣,一個人老帶悲傷,倒黴的事情也會跟過來。
——在自己心情低落的時候,告誡自己不要把負能量帶給別人。(大校13)