簡介
雖然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/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!