1、Netty客户端创建流程分析
Netty为了向使用者屏蔽NlO通信的底层细节,在和用户交互的边界做了封装,目的就是为了减少用户开发工作量,降低开发难度。 Bootstrap是 Socket客户端创建工具类,用户通过 Bootstrap可以方便地创建 Netty的客户端并发起异步TCP连接操作。
1.1、Netty客户端创建时序图
1.2、Netty客户端创建流程分析
步骤1:用户线程创建 Bootstrap实例,通过APl设置创建客户端相关的参数,异步发起客户端连接。
步骤2:创建处理客户端连接、IO读写的 Reactor线程组 NioEventLoopGroup。可以通过构造函数指定IO线程的个数,默认为CPU内核数的2倍;
步骤3:通过 Bootstrap的 ChannelFactory和用户指定的 Channel类型创建用于客户端连接的 NioSocketChannel,它的功能类似于 JDK NIO类库提供的 SocketChannel
步骤4:创建默认的 ChannelHandlerPipeline,用于调度和执行网络事件;
步骤5:异步发起TCP连接,判断连接是否成功。如果成功,则直接将 NioSocketChannel注册到多路复用器上,监听读操作位,用于数据报读取和消息发送:如果没有立即连接成功,则注册连接监听位到多路复用器,等待连接结果;
步骤6:注册对应的网络监听状态位到多路复用器;
步骤7:由多路复用器在1O现场中轮询各 Channel,处理连接结果;
步骤8:如果连接成功,设置 Future结果,发送连接成功事件,触发 ChannelPipeline执行;
步骤9:由 ChannelPipeline调度执行系统和用户的 Channelhandler,执行业务逻辑。
2、Netty客户端创建源码分析
2.1、客户端连接辅助类 Bootstrap
Bootstrap是 Netty提供的客户端连接工具类,主要用于简化客户端的创建,下面我们对它的主要API进行讲解。
设置I/O线程组:在前面的章节我们介绍过,非阻塞IO的特点就是一个多路复用器可以同时处理成百上干条链路,这就意味着使用NIO模式一个线程可以处理多个TCP连接。考虑到O线程的处理性能,大多数NO框架都采用线程池的方式处理IO读写, Netty也不例外。客户端相对于服务端,只需要一个处理IO读写的线程组即可,因为 Bootstrap提供了设置IO线程组的接口,代码如下。
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 (B) this;
}
由于Netty的NlO线程组默认采用 EventLoopGroup接口,因此线程组参数使用EventLoopGroup。TCP参数设置接口:无论是异步NIO,还是同步BIO,创建客户端套接字的时候通常都会设置连接参数,例如接收和发送缓冲区大小、连接超时时间等。 Bootstrap也提供了客户端TCP参数设置接口,代码如下。
public <T> B option(ChannelOption<T> option, T value) {
if (option == null) {
throw new NullPointerException("option");
}
if (value == null) {
synchronized (options) {
options.remove(option);
}
} else {
synchronized (options) {
options.put(option, value);
}
}
return (B) this;
}
Netty提供的主要TCP参数如下。
(1) SO_TIMEOUT:控制读取操作将阻塞多少毫秒。如果返回值为0,计时器就被禁止了,该线程将无限期阻塞;
(2) SO_SNDBUF:套接字使用的发送缓冲区大小;
(3) SO_RCVBUF:套接字使用的接收缓冲区大小;
(4) SO_REUSEADDR:用于决定如果网络上仍然有数据向旧的 Server Socket传输数据,是否允许新的 Server Socket绑定到与旧的 ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口;
(5) CONNECT_TIMEOUT_MILLIS:客户端连接超时时间,由于NIO原生的客户端并不提供设置连接超时的接口,因此, Netty采用的是自定义连接超时定时器负责检测和超时控制;
(6) TCP_NODELAY:激活或禁止 TCP_NODELAY套接字选项,它决定是否使用 Nagle算法。如果是时延敏感型的应用,建议关闭 Nagle算法。
channel接口:用于指定客户端使用的 channel接口,对于TCP客户端连接,默认使用 NioSocketChannel,代码如下。
public Bootstrap channel(Class<? extends Channel> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new BootstrapChannelFactory<Channel>(channelClass));
}
BootstrapChannelFactory利用 channelClass类型信息,通过反射机制创建NioSocketChanne对象设置 Handler接口: Bootstrap为了简化 Handler的编排,提供了 Channelinitializer,它继承了 ChannelHandlerAdapter,当TCP链路注册成功之后,调用 initHannel接口,用于设置用户 Channelhandler。它的代码如下。
@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ChannelPipeline pipeline = ctx.pipeline();
boolean success = false;
try {
initChannel((C) ctx.channel());
pipeline.remove(this);
ctx.fireChannelRegistered();
success = true;
} catch (Throwable t) {
logger.warn("Failed to initialize a channel. Closing: " + ctx.channel(), t);
} finally {
if (pipeline.context(this) != null) {
pipeline.remove(this);
}
if (!success) {
ctx.close();
}
}
}
其中 initChanne为抽象接口,用户可以在此方法中设置 Channelhandler,代码如下。
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_"
ch.pipeline().addLast(new EchoClientHandler());
}
});
最后一个比较重要的接口就是发起客户端连接,代码如下。
ChannelFuture f= b.connect(host, port). sync();
由于客户端连接方法比较复杂,下个小节对此进行详细讲解。
2.2、客户端连接操作
首先要创建和初始化 NioSocketChannel,代码如下。
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;
}
从 NioEventLoopGroup中获取 NioEventLoop,然后使用其作为参数创建NioSocketChannel,代码如下。
@Override
Channel createChannel() {
EventLoop eventLoop = group().next();
return channelFactory().newChannel(eventLoop);
}
初始化 Channel之后,将其注册到 Selector上,代码如下。
ChannelPromise regFuture = channel.newPromise();
channel.unsafe().register(regEuture);
链路创建成功之后,发起异步的TCP连接,代码如下。
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());
}
}
});
}
由上述代码可以看出,从 doConnect0操作开始,连接操作切换到了 Netty的NIO线程 NioEventLoop中进行,此时客户端返回,连接操作异步执行。doConnect0最终调用 Headhandler的 connect方法,代码如下。
@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
AbstractNioUnsafe的 connect操作如下。
boolean wasActive = isActive();
if (doConnect(remoteAddress, localAddress)) {
fulfillConnectPromise(promise, wasActive);
} else {
//后续代码省略
}
需要注意的是, SocketChannel执行 connect()操作后有以下三种结果
(1)连接成功,返回True;
(2)暂时没有连接上,服务端没有返回ACK应答,连接结果不确定,返回 False;
3)连接失败,直接抛出O异常。
如果是第二种结果,需要将 NioSocketChannel中的 selectionKey设置为OP_CONNECT,监听连接结果异步连接返回之后,需要判断连接结果,如果连接成功,则触发 ChannelActive事件,代码如下。
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
// trySuccess() will return false if a user cancelled the connection attempt.
boolean promiseSet = promise.trySuccess();
// Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
// because what happened is what happened.
if (!wasActive && isActive()) {
pipeline().fireChannelActive();
}
// If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
if (!promiseSet) {
close(voidPromise());
}
}
ChannelActive事件处理在前面章节已经详细说明,最终会将 NioSocketChannel中的selectionKey设置为 SelectionKey.OP_READ,用于监听网络读操作。
如果没有立即连接上服务端,则注册 SelectionKey.OP_CONNECT到多路复用器,代码如下
@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();
}
}
}
2.3、异步连接结果通知
NioEventLoop的 Selector轮询客户端连接 Channel,当服务端返回握手应答之后,对连接结果进行判断,代码如下。
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
下面对 finishConnect方法进行分析,代码如下。
boolean wasActive = isActive();
doFinishConnect();
fulfillConnectPromise(connectPromise, wasActive);
dofinishConnect用于判断JDK的 SocketChannel的连接结果,如果返回true表示连接成功,其他值或者发生异常表示连接失败
@Override
protected void doFinishConnect() throws Exception {
if (!javaChannel().finishConnect()) {
throw new Error();
}
}
连接成功之后,调用 fulfilIConnectPromise方法,触发链路激活事件,该事件由 ChannelPipeline进行传播,代码如下。
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
// trySuccess() will return false if a user cancelled the connection attempt.
boolean promiseSet = promise.trySuccess();
// Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
// because what happened is what happened.
if (!wasActive && isActive()) {
pipeline().fireChannelActive();
}
// If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
if (!promiseSet) {
close(voidPromise());
}
}
前面章节已经对 fireChannelActive方法进行过讲解,主要用于修改网络监听位为读操作。
2.4、客户端连接超时机制
对于 SocketChannel接口,JDK并没有提供连接超时机制,需要NIO框架或者用户自己扩展实现。Netty利用定时器提供了客户端连接超时控制功能,下面我们对该功能进行详细讲解。
首先,用户在创建 Netty客户端的时候,可以通过 ChannelOption.CONNECT_TIMEOUT_MILLIS配置项设置连接超时时间,代码如下。
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(Channeloption.CONNECT_TIMEOUT_MILLIS, 3000);
发起连接的同时,启动连接超时检测定时器,代码如下。
// Schedule connect timeout.
int connectTimeoutMillis = config().getConnectTimeoutMillis();
if (connectTimeoutMillis > 0) {
connectTimeoutFuture = eventLoop().schedule(new Runnable() {
@Override
public void run() {
ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
ConnectTimeoutException cause =
new ConnectTimeoutException("connection timed out: " + remoteAddress);
if (connectPromise != null && connectPromise.tryFailure(cause)) {
close(voidPromise());
}
}
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}
一旦超时定时器执行,说明客户端连接超时,构造连接超时异常,将异常结果设置到connectPromise中,同时关闭客户端连接,释放句柄。如果在连接超时之前获取到连接结果,则删除连接超时定时器,防止其被触发,代码如下。
@Override
public void finishConnect() {
// Note this method is invoked by the event loop only if the connection attempt was
// neither cancelled nor timed out.
assert eventLoop().inEventLoop();
assert connectPromise != null;
try {
boolean wasActive = isActive();
doFinishConnect();
fulfillConnectPromise(connectPromise, wasActive);
} catch (Throwable t) {
if (t instanceof ConnectException) {
Throwable newT = new ConnectException(t.getMessage() + ": " + requestedRemoteAddress);
newT.setStackTrace(t.getStackTrace());
t = newT;
}
// Use tryFailure() instead of setFailure() to avoid the race against cancel().
connectPromise.tryFailure(t);
closeIfClosed();
} finally {
//无论连接是否成功,只要获取到连接结果,之后就删除连接超时定时器
// Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
// See https://github.com/netty/netty/issues/1770
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
}
}