开始之前,还是先贴一段netty客户端的经典使用姿势,如下:
try {
// 通过无参构造函数,新建一个Bootstrap实例
Bootstrap b = new Bootstrap();
// 设置EventLoop线程组
b.group(new NioEventLoopGroup())
// 设置具体使用的Channel子类
.channel(NioSocketChannel.class)
// 设置tcp的参数
.option(ChannelOption.TCP_NODELAY, true)
// 设置数据处理的handler
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoClientHandler());
}
});
// 建立与服务的的连接
ChannelFuture f = b.connect(HOST, PORT).sync();
// 等待channel关闭
f.channel().closeFuture().sync();
} finally {
// 关闭EventLoop线程组
group.shutdownGracefully();
}
从上述代码可以看出,netty客户端的使用很方便,由于客户端需要配置的参数较多,所以Bootstrap提供了一个无参构造函数,而具体的参数配置,则通过build模式进行各自独立配置。
group配置的就是用来执行I/O操作的线程池,每个线程以EventLoop的形式存在,配置代码如下:
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;
}
做了一些非null的校验,如果传入的group为null,以及如果已经被设置过,则都会抛出异常,校验通过之后,则进行属性赋值。
对于channel的配置,会根据传入的class,构建一个工厂类,然后赋值给channelFactory这个属性,赋值过程与group赋值过程类似。
而工厂类,则会根据需要,通过反射的机制,得到一个channel具体子类的实例,用于建立连接。
public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new BootstrapChannelFactory<C>(channelClass));
}
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 (B) this;
}
option就是配置tcp相关的参数,就是一系列的key-value对。
handler配置的就是客户端想要对I/O过程中的数据的处理逻辑,就是一系列的ChannelHandler构成的列表。
Bootstrap在经过上述一系列配置后,各项准备工作已经就绪,接下来重点分析其connect过程。
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的调用链如上,最终会通过调用doConnect方法进行connect操作,在doConnect之前,会调用validate方法进行校验操作,主要是对group、channelFactory、handler这三个必须配置进行校验,看是否做了配置,代码如下:
public Bootstrap validate() {
super.validate();
if (handler() == null) {
throw new IllegalStateException("handler not set");
}
return this;
}
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;
}
在上述校验通过后,继续看connect真正发生的地方,也就是doConnect方法里做了什么。
private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
/**
* 新建 NioSocketChannel , 并将该channel 注册到一个 eventLoop 上去
* 返回的 ChannelFuture 子类为 DefaultChannelPromise
* 这步的操作都是本机操作,还不涉及到网络操作
*/
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
final ChannelPromise promise = channel.newPromise();
/**
* 根据注册时,返回的channelFuture的状态来决定,进行connect操作
* 这里都是完全异步的,如果注册还没有完成,则会监听注册的状态,在注册完成时,才会进行connect操作
*/
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;
}
这段代码很短,是进行connect操作的入口,主要分为两大步骤,register和connect操作。
1、register就是将新建的NioSocketChannel注册到NioEventLoop上,最终就是将socket注册到一个selector上,
2、connect则是建立与远程服务器的网络连接,我的理解,就是与远程主机完成了3次握手的操作。
从上述这段代码可以看出,register和connect操作都是异步操作,如调用initAndRegister方法后,返回的是一个regFutrure,这是一个异步调用的返回结果,接着对其做了判断,如果其中的异常不为null,说明register过程出错,则直接就讲regFutrure作为整个doConnect方法的返回值,也就是结束了connect操作。判断如下:
if (regFuture.cause() != null) {
return regFuture;
}
如果regFutrure没有异常,则会再判断是否已经done,如果done,则直接调用doConnect0方法进行connect操作,如果没有完成,则在regFutrure上添加了一个ChannelFutureListener,主要逻辑就是在regFuture完成时,调用doConnect0方法,这里需要注意下,在这之前,通过如下代码:
final ChannelPromise promise = channel.newPromise();
新建了一个ChannelPromise的实例promise,并作为doConnect0的第五个入参,然后doConnect方法的返回值也是这个promise。
从这个过程可以看出,register和connect过程都是异步操作,后面具体分析里面的过程时,会更加明确这一点。
这里简单说明下ChannelFuture和ChannelPromise,这两个都是异步执行的方法结果,其中ChannelPromise是ChannelFuture的子类,ChannelFuture只能获取结果,不能对异步返回结果进行操作,而ChannelPromise对其进行了扩展,可以对异步返回结果进行设置等操作。
接下里,先来看register的过程,主要逻辑在initAndRegister方法中,代码如下:
final ChannelFuture initAndRegister() {
// 1、通过channelFactory新建一个NioSocketChannel的实例
final Channel channel = channelFactory().newChannel();
try {
// 2、初始化channel
init(channel);
} catch (Throwable t) {
channel.unsafe().closeForcibly();
return channel.newFailedFuture(t);
}
// 3、对channel进行注册
ChannelFuture regFuture = group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
1、根据Bootstrap的配置,用channelFactory,利用反射新建一个NioSocketChannel的实例;
2、对新建好的NioSocketChannel的实例进行初始化操作,代码如下:
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());
}
}
}
初始化的逻辑很明确,
a、把配置的handler添加到channel的pipeline中,用于后续数据处理;
b、把配置的tcp的参数添加到channel的配置中;
c、把配置的属性添加到channel的属性中。
3、上述初始化完成之后,则进行channel的注册过程,代码如下:
ChannelFuture regFuture = group().register(channel);
其中group方法返回的就是Bootstrap在初始时传入的NioEventLoopGroup对象,其调用register方法,最终是调用了SingleThreadEventLoop的register方法,如下:
public ChannelFuture register(Channel channel) {
return register(channel, new DefaultChannelPromise(channel, this));
}
在register的入参中新建了一个DefaultChannelPromise实例,该实例也就是该register方法后面会返回的ChannelFuture。
public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
if (channel == null) {
throw new NullPointerException("channel");
}
if (promise == null) {
throw new NullPointerException("promise");
}
channel.unsafe().register(this, promise);
return promise;
}
在调用了unsafte的register方法后,就会把这个promise返回。接下来继续看register里做了什么。
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// 1
if (eventLoop == null) {
throw new NullPointerException("eventLoop");
}
// 2
if (isRegistered()) {
promise.setFailure(new IllegalStateException("registered to an event loop already"));
return;
}
// 3
if (!isCompatible(eventLoop)) {
promise.setFailure(
new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
return;
}
// 4
AbstractChannel.this.eventLoop = eventLoop;
// 5
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new OneTimeTask() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
// 6
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}
1、判断eventloop是否为null,如果为null,则抛出异常;
2、判断该channel是否已经被注册过,一个channel只能被注册一次,不能注册到多个eventloop上,所以如果注册,则抛出异常;
3、兼容性判断,看eventloop是否是一个NioEventLoop的实例;
protected boolean isCompatible(EventLoop loop) {
return loop instanceof NioEventLoop;
}
4、将eventloop赋值给channel的eventloop属性;
5、这里就是register实现异步的地方,判断当前执行现场是否就是eventloop的线程,如果是,则进行调用register0方法,如果不是,则将register0方法封装成一个task,提交给eventloop来执行,注意register0的入参就是前面传入的promise,所以可以根据这个promise来判断register是否完成,以及是否成功等;
6、如果注册出现异常,则会做一些收尾工作,如关闭channel,promise中设置fail标志等。
所以到这里,用户执行的register的动作已经完成了,但真正的register操作还没有发生,被提交给eventloop去异步执行了,但是会返回一个ChannelFuture的子类实例promise,可以用来检测注册的完成情况。
再继续跟进到register0里,看看做了什么操作。
private void register0(ChannelPromise promise) {
try {
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
// 1
doRegister();
// 2
registered = true;
// 3
safeSetSuccess(promise);
// 4
pipeline.fireChannelRegistered();
if (isActive()) {
// 5
pipeline.fireChannelActive();
}
} catch (Throwable t) {
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
1、doRegister方法,进行真正的注册,实质就是调用了java nio中的channel的register方法,将其注册到selector上去,这里需要注意的是,注册时,register的第二个参数是设置感兴趣的操作,这里设置的是0,说明没有设置任何感兴趣的操作,这里只是简单的完成了注册的动作,对于感兴趣的操作的设置是在fireChannelActive中设置的,后续会分析到。
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
eventLoop().selectNow();
selected = true;
} else {
throw e;
}
}
}
}
2、注册成功后,会将channel的registered标志设置为true;
3、在promise中设置注册成功的标识,这样在前面doConnect方法中,监听注册情况的那个listener就可以开始进行doConnect0方法的执行了;
4、上述动作完成之后,就会调用pipeline的fireChannelRegistered方法,在pipeline的ChannelHandler链中,依次处理channel注册完成的操作;
5、接着判断该channel是否处于活跃状态,也就是该channel中包含的ch是否处于open和connect的状态,一般情况下,第3步结束后才触发了doConnect0操作,所以一般这里的判断都是false,也就是不会触发fireChannelActive操作,但也不是绝对。
public boolean isActive() {
SocketChannel ch = javaChannel();
return ch.isOpen() && ch.isConnected();
}
上述就是注册的过程,在注册完成之后,就会触发connect操作,来继续看下doConnect0方法中做了什么。
private static void doConnect0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
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());
}
}
});
}
这里做的更直白,直接封装了一个task,然后提交给eventloop去异步执行,然后就返回了。
前面我们分析过,在register完成之后,才会触发doConnect0操作,当这里封装的任务开始执行时,首先就是对regFuture的状态进行判断,看是成功还是失败,成功了,才会继续执行connect操作,调用channel的connect操作,就是调用的该channel的pipeline的connect操作,对于connect是一个用户发起的动作,所以是一个outbound的操作,outbound操作都是从tail开始,传递到head,所以真正的操作是在head的connect方法中。
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return pipeline.connect(remoteAddress, promise);
}
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return tail.connect(remoteAddress, promise);
}
而head的connect方法中,则是直接调用了unsafe的connect方法,继续向下看。
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
unsafe的connect的方法如下:
public void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
// 1、设置channel不能取消标志,并判断channel是否处于open状态
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
try {
// 2、如果connectPromise不为null,则说明已经做过连接,则这里就要抛出异常了
if (connectPromise != null) {
throw new IllegalStateException("connection attempt already made");
}
// 3、判断当前channel是否活跃,也就是要open和connect都完成,才是true,才开始连接,所以wasActive应该是false
boolean wasActive = isActive();
// 4、进行连接操作,下面会详细分析里面内容
if (doConnect(remoteAddress, localAddress)) {
// 5、连接成功,则会触发channelActive事件,以及将selectionKey的监听事件中加入read事件
fulfillConnectPromise(promise, wasActive);
} else {
// 6、如果当前没有连接成功
connectPromise = promise;
requestedRemoteAddress = remoteAddress;
// Schedule connect timeout.
int connectTimeoutMillis = config().getConnectTimeoutMillis();
if (connectTimeoutMillis > 0) {
/**
* 如果超时时间大于0, 则设置一个定时任务, 在超时时间时, 检查连接是否成功,
* 如果还没有连接上, 则会抛出连接超时异常, 这是netty自己做的一个超时检查任务
*/
connectTimeoutFuture = eventLoop().schedule(new OneTimeTask() {
@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);
}
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isCancelled()) {
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
close(voidPromise());
}
}
});
}
} catch (Throwable t) {
if (t instanceof ConnectException) {
Throwable newT = new ConnectException(t.getMessage() + ": " + remoteAddress);
newT.setStackTrace(t.getStackTrace());
t = newT;
}
promise.tryFailure(t);
closeIfClosed();
}
}
4、进行连接操作的具体逻辑如下,如果连接成功了,则返回true,由于channel是非阻塞的,所以暂时没有连接成功的,会注册OP_CONNECT事件,等待其连接成功。
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();
}
}
}
5、如果连接当场就成功了,则会fullFillConnectPromise操作,如下:
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
if (promise == null) {
return;
}
// 1、尝试设置连接成功的标识,让用户调用的地方,知道连接成功了
boolean promiseSet = promise.trySuccess();
// 2、如果在连接之前不处于活跃状态,而现在处于活跃状态了,则触发fireChannelActive操作
if (!wasActive && isActive()) {
pipeline().fireChannelActive();
}
// 3、如果第2步设置失败,则会关闭channel
if (!promiseSet) {
close(voidPromise());
}
}
6、在当前没有连接成功的情况下,netty对这种异步连接的操作,做了一个连接超时的检测,就是设置了一个定时任务,用来判断连接是否成功。
if (connectTimeoutMillis > 0) {
/**
* 如果超时时间大于0, 则设置一个定时任务, 在超时时间时, 检查连接是否成功,
* 如果还没有连接上, 则会抛出连接超时异常, 这是netty自己做的一个超时检查任务
*/
connectTimeoutFuture = eventLoop().schedule(new OneTimeTask() {
@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);
}
在设置netty的客户端时,有一个参数 CONNECT_TIMEOUT_MILLIS 就是用来设置这里的超时时间的,如果设置过小,而网络较差时,则有可能会出现 ConnectTimeoutException 的异常,可以根据网络环境,适当设置该参数的大小。
在channel被注册的selector上去时,并没有设置OP_READ事件,那在哪里设置的哪?
在第5步中,讲到了fireChannelActive这个操作,来继续看看里面做了什么。
public ChannelPipeline fireChannelActive() {
head.fireChannelActive();
if (channel.config().isAutoRead()) {
channel.read();
}
return this;
}
可以看到,先会调用pipeline上的所有ChannelHandler的channelActive方法。
然后会判断是否自动读,该配置默认是true,所以会进入channel的read方法,继续看下去。
public Channel read() {
pipeline.read();
return this;
}
直接调用了pipeline的read方法,由于是一个outbound事件,所以最终是调用head的read方法,进入看看。
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead();
}
public void beginRead() {
// Channel.read() or ChannelHandlerContext.read() was called
readPending = true;
super.beginRead();
}
public void beginRead() {
if (!isActive()) {
return;
}
try {
doBeginRead();
} catch (final Exception e) {
invokeLater(new OneTimeTask() {
@Override
public void run() {
pipeline.fireExceptionCaught(e);
}
});
close(voidPromise());
}
}
经过一层层的调用,最终是调用了doBeginRead方法,实现如下:
protected void doBeginRead() throws Exception {
if (inputShutdown) {
return;
}
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
这里找到了设置感兴趣事件的地方。
selectionKey.interestOps(interestOps | readInterestOp);
至此,netty客户端建立与服务端的连接操作就结束了。
总结如下: