Netty源碼分析:客戶端連接
先說結論,Netty 客戶端的連接的底層實現最終是藉助於Java NIO SocketChannel來實現,Java NIO SocketChannel作爲客戶端去連接服務端樣式代碼如下:
//客戶端,首先有一個SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//連接
socketChannel.connect(new InetSocketAddress("localhost",8080));
結論已經說完了,或許看完這篇博文,你纔會明白這個結論哈,不急,慢慢看。
博文Netty源碼分析:服務端啓動全過程對服務端的啓動進行了全面的分析,本篇博文將對客戶端如何連接到服務端進行一個分析。
一般情況下,Netty客戶端的啓動代碼類似如下:
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
//new LoggingHandler(LogLevel.INFO),
new EchoClientHandler(firstMessageSize));
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully();
}
前面幾篇博文中對NioEventLoopGroup、Bootstrap已經做過詳細的介紹,這裏不再介紹,本篇博文主要是跟蹤分析ChannelFuture f = b.connect(host, port).sync();
這行代碼主要做了哪些工作。
Bootstrap.java
public ChannelFuture connect(String inetHost, int inetPort) {
return connect(new InetSocketAddress(inetHost, inetPort));
}
public ChannelFuture connect(SocketAddress remoteAddress) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
validate();
return doConnect(remoteAddress, localAddress());
}
該connect函數幹了兩件事情,如下:
1、調用validate()方法進行了相關的校驗
@Override
public Bootstrap validate() {
super.validate();
if (handler() == null) {
throw new IllegalStateException("handler not set");
}
return this;
}
Bootstrap父類AbstractBootstrap的validate()方法如下:
public B validate() {
if (group == null) {
throw new IllegalStateException("group not set");
}
if (channelFactory == null) {
throw new IllegalStateException("channel or channelFactory not set");
}
return (B) this;
}
從兩個validate()函數我們可以得到:就是檢查此Bootstrap對象是否設置了handler、group以及channelFactory這三個對象。
很明顯在本博文最開始的地方所列出客戶端的如下代碼,就是設置了這幾個參數。
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
//new LoggingHandler(LogLevel.INFO),
new EchoClientHandler(firstMessageSize));
}
});
2、調用了doConnect創建連接
private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
final ChannelPromise promise = channel.newPromise();
if (regFuture.isDone()) {
doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
} else {
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
}
});
}
return promise;
}
看過服務端啓動的源碼之後,看到這些方法會發現差不多一樣的趕腳哈。具體可以看博文Netty源碼分析:服務端啓動全過程
這個函數的主要工作有如下幾點:
1、通過initAndRegister()方法得到一個ChannelFuture的實例regFuture。
2、通過regFuture.cause()方法判斷是否在執行initAndRegister方法時產生來異常。如果產生來異常,則直接返回,如果沒有產生異常則進行第3步。
3、通過regFuture.isDone()來判斷initAndRegister方法是否執行完畢,如果執行完畢來返回true,然後調用doConnect0進行連接。如果沒有執行完畢則返回false進行第4步。
4、regFuture會添加一個ChannelFutureListener監聽,當initAndRegister執行完成時,調用operationComplete方法並執行doConnect0進行連接。
第3、4點想幹的事就是一個:調用doConnect0進行連接。
有了服務端啓動源碼分析的經驗,我們就快速的跟下這些函數具體幹了什麼。
1、initAndRegister
final ChannelFuture initAndRegister() {
//第一步:首先利用反射得到Channel的實例,這裏爲NioSocketChannel實例。
final Channel channel = channelFactory().newChannel();
try {
//第二步:然後進行初始化
init(channel);
} catch (Throwable t) {
channel.unsafe().closeForcibly();
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
//第三步:註冊
ChannelFuture regFuture = group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
該函數主要乾了如下三件事:
1、首先利用反射得到Channel的實例,這裏爲NioSocketChannel實例
2、調用init方法初始化第一步所得到的Channel。
3、將第一步所得到的Channel進行註冊。
下面將逐步進行分析。
1、final Channel channel = channelFactory().newChannel()
這行代碼是如何利用反射來得到Channel實例的,在博文Netty源碼分析:服務端啓動全過程有詳細的介紹,由於這裏的Channel是NioSocketChannel這個類,因此這裏主要分析這個類的構造函數。
在看具體的構造函數之前,先看下該類的繼承結構,如下圖所示,用“紅色”框起來的是NioSocketChannel這個Channel與NioServerSocketChannel的相同之處。
下面來看下NioSocketChannel這個類的構造函數。
public NioSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));//注意:這裏的newSocket函數利用provider打開了一個Java NIO SocketChannelImpl。
}
public NioSocketChannel(SocketChannel socket) {
this(null, socket);
}
public NioSocketChannel(Channel parent, SocketChannel socket) {
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
調到這個構造函數之後,接着調用父類AbstractNioByteChannel的構造函數,其中,傳入的參數parent爲null,socket爲剛剛使用newSocket創建的Java NIO SocketChannelImpl實例。
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}
即這裏也什麼也沒有做,而是直接調用父類如下的構造函數,其中傳入的參數除了parent、ch之外,還添加了參數readInterestOp = SelectionKey.OP_READ。
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
ch.configureBlocking(false);
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
if (logger.isWarnEnabled()) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}
}
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}
然後繼續調用父類 AbstractChannel 的構造器:
protected AbstractChannel(Channel parent) {
this.parent = parent;
unsafe = newUnsafe();
pipeline = new DefaultChannelPipeline(this);
}
到這裏,一個完整的NioSocketChannel就初始化完成了,該初始化過程總結如下:
1、 調用NioSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)
打開一個新的 Java NIO SocketChannel。
2、初始化了NioSocketChannel中的屬性SocketChannelConfig config = new NioSocketChannelConfig(this, socket.socket()),該配置類後續會分析;
3、初始化了 AbstractNioChannel中的如下屬性:
1)SelectableChannel ch 被設置爲 Java SocketChannel, 即第一點所返回的SocketChannelImple實例。
2)readInterestOp 被設置爲 SelectionKey.OP_READ
3)SelectableChannel ch 被配置爲非阻塞的 ch.configureBlocking(false)
4、初始化了 AbstractChannel中的如下屬性
1)parent = null
2)unsafe = newUnsafe();//newUnsafe()方法返回的是NioByteUnsafe對象
3) pipeline = new DefaultChannelPipeline(this);//每個Channel都有且僅有一個Pipeline。
2、 init(channel)
init方法與初始化服務端所對應的NioServerSocketChannel相比,此方法更簡單,代碼如下:
void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline();
p.addLast(handler());
final Map<ChannelOption<?>, Object> options = options();
synchronized (options) {
for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
try {
if (!channel.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
logger.warn("Unknown channel option: " + e);
}
} catch (Throwable t) {
logger.warn("Failed to set a channel option: " + channel, t);
}
}
}
final Map<AttributeKey<?>, Object> attrs = attrs();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
該方法裏面的下面兩行代碼主要是將我們自定義的handler加入到pipeline所持有以AbstractChannelHandlerContext爲節點的雙向鏈表中。具體是如何添加的,可以參考博文 Netty源碼分析:ChannelPipeline,該方法的剩餘代碼主要是將options和attrs添加到Channel上。
ChannelPipeline p = channel.pipeline();
p.addLast(handler());
3、ChannelFuture regFuture = group().register(channel);
將channel進行註冊,在博文Netty源碼分析:服務端啓動全過程中有詳細的介紹,這裏不在介紹。
2、doConnect0(regFuture, channel, remoteAddress, localAddress, promise)
該方法的代碼如下:
private static void doConnect0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress remoteAddress, 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()) {
if (localAddress == null) {
channel.connect(remoteAddress, promise);
} else {
channel.connect(remoteAddress, localAddress, promise);
}
promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}
在該方法中,會在channel所關聯到的eventLoop 線程中調用 channel.connect方法,注意這裏的channel是NioSocketChannel實例。
@Override
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return pipeline.connect(remoteAddress, promise);
}
這裏的pipeline是DefaultChannelPipeline實例,繼續看此pipeline的connect方法
@Override
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return tail.connect(remoteAddress, promise);
}
在博文 Netty源碼分析:ChannelPipeline中有關於tail的詳細介紹,這裏回顧下:tail是TailContext的實例,繼承於AbstractChannelHandlerContext,TailContext並沒有實現connect方法,因此這裏調用的是其父類AbstractChannelHandlerContext的connect方法。
@Override
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return connect(remoteAddress, null, promise);
}
@Override
public ChannelFuture connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
//...省略了一些目前不關注的代碼
final AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeConnect(remoteAddress, localAddress, promise);
} else {
safeExecute(executor, new OneTimeTask() {
@Override
public void run() {
next.invokeConnect(remoteAddress, localAddress, promise);
}
}, promise, null);
}
return promise;
}
此函數中的這行代碼:final AbstractChannelHandlerContext next = findContextOutbound();
所完成的任務就是在pipeline所持有的以AbstractChannelHandlerContext爲節點的雙向鏈表中從尾節點tail開始向前尋找第一個outbound=true的handler節點。
private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
在博文 Netty源碼分析:ChannelPipeline中我們分析“在哪裏調用了我們自定義的handler”時介紹了在pipeline所持有的以AbstractChannelHandlerContext爲節點的雙向鏈表中從頭節點head開始向後尋找第一個inbound=true的handler節點,完成此功能的方法爲findContextInbound,具體代碼如下:
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
看過上篇博文之後,我相信我們都知道這個outbound=ture的節點時哪一個?是head節點,爲什麼呢?在博文 Netty源碼分析:ChannelPipeline中我們知道在DefaultChannelPipeline 的構造器中, 會實例化兩個對象: head 和 tail, 並形成了雙向鏈表的頭和尾. head 是 HeadContext 的實例, 它實現了 ChannelOutboundHandler 接口, 即head實例的 outbound = true. 因此在調用上面 findContextOutbound()方法時, 找到的符合outbound=true的節點其實就是 head。
繼續看,在pipelie的雙向鏈表中找到第一個outbound=true的AbstractChannelHandlerContext節點head後,然後調用此節點的invokeConnect方法,該方法的代碼如下,
private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
HeadContext類中的handler()方法代碼如下:
@Override
public ChannelHandler handler() {
return this;
}
該方法返回的是其本身,這是因爲HeadContext由於其繼承AbstractChannelHandlerContext以及實現了ChannelHandler接口使其具有Context和Handler雙重特性。
繼續看,看HeadContext類中的connect方法,代碼如下:
@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
unsafe這個字段是在HeadContext構造函數中被初始化的,如下:
HeadContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, HEAD_NAME, false, true);
unsafe = pipeline.channel().unsafe();
}
而此構造函數中的pipeline.channel().unsafe()這行代碼返回的就是在本博文前面研究NioSocketChannel這個類的構造函數中所初始化的一個實例,如下:
unsafe = newUnsafe();//newUnsafe()方法返回的是NioByteUnsafe對象。
繼續看NioByteUnsafe類中的 connect方法(準確的說此方法是在AbstractNioUnsafe類中)
@Override
public final void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
//...
if (doConnect(remoteAddress, localAddress)) {
fulfillConnectPromise(promise, wasActive);
} else {
//...
}
}
上面只保留了connect的關鍵代碼,相關檢查和連接失敗的代碼省略了,上面這個函數主要是調用了doConnect這個方法,需要注意的是,此方法並不是 AbstractNioUnsafe 的方法, 而是 AbstractNioChannel 的抽象方法. doConnect 方法是在 NioSocketChannel 中實現的。
NioSocketChannel類中的doConnect方法的代碼如下:
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
if (localAddress != null) {
javaChannel().socket().bind(localAddress);
}
boolean success = false;
try {
boolean connected = javaChannel().connect(remoteAddress);
if (!connected) {
selectionKey().interestOps(SelectionKey.OP_CONNECT);
}
success = true;
return connected;
} finally {
if (!success) {
doClose();
}
}
}
上面方法中javaChannel()方法返回的是NioSocketChannel實例初始化時所產生的Java NIO SocketChannel實例(更具體點爲SocketChannelImple實例)。 然後調用此實例的connect方法完成Java NIO層面上的Socket連接。
如果對Java NIO層面的連接以及交互不太清晰,可以看博文Java NIO 之 ServerSocketChannel/SocketChannel ,這裏介紹了 Java NIO 層面的客戶端於服務端之間的連接以及客戶端於服務端之阿金的交互。
總結
Netty中客戶端的連接的底層實現是使用Java NIO 的SocketChannel來完成的。