簡介
雖然netty很強大,但是使用netty來構建程式卻是很簡單,只需要掌握特定的netty套路就可以寫出強大的netty程式。每個netty程式都需要一個Bootstrap,什麼是Bootstrap呢?Bootstrap翻譯成中文來說就是鞋拔子,在計算機世界中,Bootstrap指的是載入程式,通過Bootstrap可以輕鬆構建和啟動程式。
在netty中有兩種Bootstrap:客戶端的Bootstrap和伺服器端的ServerBootstrap。兩者有什麼不同呢?netty中這兩種Bootstrap到底是怎麼工作的呢? 一起來看看吧。
Bootstrap和ServerBootstrap的聯絡
首先看一下Bootstrap和ServerBootstrap這兩個類的繼承關係,如下圖所示:
可以看到Bootstrap和ServerBootstrap都是繼承自AbstractBootstrap,而AbstractBootstrap則是實現了Cloneable介面。
AbstractBootstrap
有細心的同學可能會問了,上面圖中還有一個Channel,channel跟AbstractBootstrap有什麼關係呢?
我們來看下AbstractBootstrap的定義:
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable
AbstractBootstrap接受兩個泛型引數,一個是B繼承自AbstractBootstrap,一個是C繼承自Channel。
我們先來觀察一下一個簡單的Bootstrap啟動需要哪些元素:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FirstServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 繫結埠並開始接收連線
ChannelFuture f = b.bind(port).sync();
// 等待server socket關閉
f.channel().closeFuture().sync();
上面的程式碼是一個最基本也是最標準的netty伺服器端的啟動程式碼。可以看到和Bootstrap相關的元素有這樣幾個:
- EventLoopGroup,主要用來進行channel的註冊和遍歷。
- channel或者ChannelFactory,用來指定Bootstrap中使用的channel的型別。
- ChannelHandler,用來指定具體channel中訊息的處理邏輯。
- ChannelOptions,表示使用的channel對應的屬性資訊。
- SocketAddress,bootstrap啟動是繫結的ip和埠資訊。
目前看來和Bootstrap相關的就是這5個值,而AbstractBootstrap的建構函式中也就定義了這些屬性的賦值:
AbstractBootstrap(AbstractBootstrap<B, C> bootstrap) {
group = bootstrap.group;
channelFactory = bootstrap.channelFactory;
handler = bootstrap.handler;
localAddress = bootstrap.localAddress;
synchronized (bootstrap.options) {
options.putAll(bootstrap.options);
}
attrs.putAll(bootstrap.attrs);
}
示例程式碼中的group,channel,option等方法實際上都是向這些屬性中賦值,並沒有做太多的業務操作。
注意,AbstractBootstrap中只存在一個group屬性,所以兩個group屬性是在ServerBootstrap中新增的擴充套件屬性。
在Bootstrap中,channel其實是有兩種賦值方法,一種是直接傳入channel,另外一種方法是傳入ChannelFactory。兩者的本質都是一樣的,我們看下channel是怎麼轉換成為ChannelFactory的:
public B channel(Class<? extends C> channelClass) {
return channelFactory(new ReflectiveChannelFactory<C>(
ObjectUtil.checkNotNull(channelClass, "channelClass")
));
}
channelClass被封裝在一個ReflectiveChannelFactory中,最終還是設定的channelFactory屬性。
AbstractBootstrap中真正啟動服務的方法就是bind,bind方法傳入的是一個SocketAddress,返回的是ChannelFuture,很明顯,bind方法中會建立一個channel。我們來看一下bind方法的具體實現:
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
在doBind方法中,首先呼叫initAndRegister方法去初始化和註冊一個channel。
channel是通過channelFactory的newChannel方法來建立的:
channel = channelFactory.newChannel();
接著呼叫初始化channel的init方法。這個init方法在AbstractBootstrap中並沒有實現,需要在具體的實現類中實現。
有了channel之後,通過呼叫EventLoopGroup的register方法將channel註冊到 EventLoop中,並將註冊生成的ChannelFuture返回。
然後通過判斷返回的regFuture的狀態,來判斷channel是否註冊成功,如果註冊成功,最後呼叫doBind0方法,完成最後的繫結工作:
private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (regFuture.isSuccess()) {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}
因為eventLoop本身是一個Executor,所以可以執行一個具體的命令的,在它的execute方法中,傳入了一個新的Runnable物件,在其中的run方法中執行了channel.bind方法,將channel跟SocketAddress進行繫結。
到此,Bootstrap的bind方法執行完畢。
我們再來回顧一下bind方法的基本流程:
- 通過ChannelFactory建立一個channel。
- 將channel註冊到Bootstrap中的EventLoopGroup中。
- 如果channel註冊成功,則呼叫EventLoopGroup的execute方法,將channel和SocketAddress進行繫結。
是不是很清晰?
講完AbstractBootstrap,接下來,我們再繼續探討一下Bootstrap和ServerBootstrap。
Bootstrap和ServerBootstrap
首先來看下Bootstrap,Bootstrap主要使用在客戶端使用,或者UDP協議中。
先來看下Bootstrap的定義:
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel>
Bootstrap和AbstractBootstrap相比,主要多了一個屬性和一個方法。
多的一個屬性是resolver:
private static final AddressResolverGroup<?> DEFAULT_RESOLVER = DefaultAddressResolverGroup.INSTANCE;
private volatile AddressResolverGroup<SocketAddress> resolver =
(AddressResolverGroup<SocketAddress>) DEFAULT_RESOLVER;
AddressResolverGroup裡面有一個IdentityHashMap,它的key是EventExecutor,value是AddressResolver:
private final Map<EventExecutor, AddressResolver<T>> resolvers =
new IdentityHashMap<EventExecutor, AddressResolver<T>>();
實際上AddressResolverGroup維護了一個EventExecutor和AddressResolver的對映關係。
AddressResolver主要用來解析遠端的SocketAddress的地址。因為遠端的SocketAddress可能並不是一個IP地址,所以需要使用AddressResolver解析一下。
這裡的EventExecutor實際上就是channel註冊的EventLoop。
另外Bootstrap作為一個客戶端的應用,它需要連線到伺服器端,所以Bootstrap類中多了一個connect到遠端SocketAddress的方法:
public ChannelFuture connect(SocketAddress remoteAddress) {
ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
validate();
return doResolveAndConnect(remoteAddress, config.localAddress());
}
connect方法和bind方法的邏輯類似,只是多了一個resolver的resolve過程。
解析完畢之後,會呼叫doConnect方法,進行真正的連線:
private static void doConnect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
final Channel channel = connectPromise.channel();
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (localAddress == null) {
channel.connect(remoteAddress, connectPromise);
} else {
channel.connect(remoteAddress, localAddress, connectPromise);
}
connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
});
}
可以看到doConnect方法和doBind方法很類似,都是通過當前channel註冊的eventLoop來執行channel的connect或許bind方法。
再看一下ServerBootstrap的定義:
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel>
因為是ServerBootstrap用在伺服器端,所以不選Bootstrap那樣去解析SocketAddress,所以沒有resolver屬性。
但是對應伺服器端來說,可以使用parent EventLoopGroup來接受連線,然後使用child EventLoopGroup來執行具體的命令。所以在ServerBootstrap中多了一個childGroup和對應的childHandler:
private volatile EventLoopGroup childGroup;
private volatile ChannelHandler childHandler;
因為ServerBootstrap有兩個group,所以ServerBootstrap包含一個含有兩個EventLoopGroup的group方法:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
還記得bind方法需要實現的init方法嗎? 我們看下ServerBootstrap中init的具體邏輯:
void init(Channel channel) {
setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, newAttributesArray());
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions);
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
首先是設定channel的一些屬性,然後通過channel.pipeline方法獲得channel對應的pipeline,然後向pipeline中新增channelHandler。
這些都是常規操作,我們要注意的是最後通過channel註冊到的eventLoop,將ServerBootstrapAcceptor加入到了pipeline中。
很明顯ServerBootstrapAcceptor本身應該是一個ChannelHandler,它的主要作用就是用來接受連線:
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter
我們來看一下它的channelRead方法:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);
try {
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
因為server端接受的是客戶端channel的connect操作,所以對應的channelRead中的物件實際上是一個channel。這裡把這個接受到的channel稱作child。通過給這個child channel新增childHandler,childOptions和childAttrs,一個能夠處理child channel請求的邏輯就形成了。
最後將child channel註冊到childGroup中,至此整個ServerBootstrapAcceptor接受channel的任務就完成了。
這裡最妙的部分就是將客戶端的channel通過server端的channel傳到server端,然後在server端為child channel配備handler進行具體的業務處理,非常巧妙。
總結
通過具體分析AbstractBootstrap,Bootstrap和ServerBootstrap的結構和實現邏輯,相信大家對netty服務的啟動流程有了大概的認識,後面我們會詳細講解netty中的channel和非常重要的eventLoop。
本文已收錄於 http://www.flydean.com/03-1-netty-boots…-serverbootstrap/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!