Netty源码解析—客户端启动

Bootstrap示例

public final class EchoClient {

    static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
static final int SIZE = Integer.parseInt(System.getProperty("size", "256")); public static void main(String[] args) throws Exception {
// Configure SSL.git
// 配置 SSL
final SslContext sslCtx;
if (SSL) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
} // Configure the client.
// 创建一个 EventLoopGroup 对象
EventLoopGroup group = new NioEventLoopGroup();
try {
// 创建 Bootstrap 对象
Bootstrap b = new Bootstrap();
b.group(group) // 设置使用的 EventLoopGroup
.channel(NioSocketChannel.class) // 设置要被实例化的为 NioSocketChannel 类
.option(ChannelOption.TCP_NODELAY, true) // 设置 NioSocketChannel 的可选项
.handler(new ChannelInitializer<SocketChannel>() { // 设置 NioSocketChannel 的处理器
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoClientHandler());
}
}); // 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.
// 优雅关闭一个 EventLoopGroup 对象
group.shutdownGracefully();
}
}
}

我们进入到启动客户端的地方connect(...)

	public ChannelFuture connect(SocketAddress remoteAddress) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
// 校验必要参数
validate();
// 解析远程地址,并进行连接
return doResolveAndConnect(remoteAddress, config.localAddress());
}

我们进入到doResolveAndConnect()方法

    /**
* @see #connect()
*/
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
// 初始化并注册一个 Channel 对象,因为注册是异步的过程,所以返回一个 ChannelFuture 对象。
final ChannelFuture regFuture = initAndRegister();
//...
if (regFuture.isDone()) {
//...
// 解析远程地址,并进行连接
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
} else {
//...
//如果异步注册对应的 ChanelFuture 未完成,则调用下面方法,添加监听器
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
//...
// 解析远程地址,并进行连接
doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
//...
}
});
return promise;
}
//...
}

省略掉部分代码,我们可以知道这个方法主要做了两件事

  1. 注册一个Channel对象
  2. 解析地址并连接

我们直接开始撸initAndRegister()

    final ChannelFuture initAndRegister() {
//...
//反射初始化一个NioSocketChannel
channel = channelFactory.newChannel();
//设置参数
init(channel);
//...
//注册channel
ChannelFuture regFuture = config().group().register(channel);
//...
return regFuture;
}

我们到NioSocketChannel

    public NioSocketChannel(SelectorProvider provider) {
this(newSocket(provider));
} private static SocketChannel newSocket(SelectorProvider provider) {
try {
//相当于java NIO的SocketChannel.open();
return provider.openSocketChannel();
} catch (IOException e) {
throw new ChannelException("Failed to open a socket.", e);
}
} public NioSocketChannel(Channel parent, SocketChannel socket) {
//调用父类构造方法
super(parent, socket);
//初始化config属性,创建NioSocketChannelConfig对象
config = new NioSocketChannelConfig(this, socket.socket());
}

我们再回到init(Channel channel)方法,初始化Channel配置

@Override
void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline(); // 添加处理器到 pipeline 中
p.addLast(config.handler()); // 初始化 Channel 的可选项集合
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
} // 初始化 Channel 的属性集合
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}

至此initAndRegister() 方法已经走完,回到我们的doResolveAndConnect()方法中,继续调用doResolveAndConnect0(),解析远程地址,并进行连接

    private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
final SocketAddress localAddress, final ChannelPromise promise) {
try {
final EventLoop eventLoop = channel.eventLoop();
final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop); if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
// Resolver has no idea about what to do with the specified remote address or it's resolved already.
doConnect(remoteAddress, localAddress, promise);
return promise;
}
// 解析远程地址
final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress); if (resolveFuture.isDone()) {
// 解析远程地址失败,关闭 Channel ,并回调通知 promise 异常
final Throwable resolveFailureCause = resolveFuture.cause(); if (resolveFailureCause != null) {
// Failed to resolve immediately
channel.close();
promise.setFailure(resolveFailureCause);
} else {
// Succeeded to resolve immediately; cached? (or did a blocking lookup)
// 连接远程地址
doConnect(resolveFuture.getNow(), localAddress, promise);
}
return promise;
} // Wait until the name resolution is finished.
resolveFuture.addListener(new FutureListener<SocketAddress>() {
@Override
public void operationComplete(Future<SocketAddress> future) throws Exception {
// 解析远程地址失败,关闭 Channel ,并回调通知 promise 异常
if (future.cause() != null) {
channel.close();
promise.setFailure(future.cause());
// 解析远程地址成功,连接远程地址
} else {
doConnect(future.getNow(), localAddress, promise);
}
}
});
} catch (Throwable cause) {
// 发生异常,并回调通知 promise 异常
promise.tryFailure(cause);
}
return promise;
}

我们走到doConnect()方法,执行Channel连接远程地址的逻辑.

    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);
}
});
}

然后调用connect()方法一直断点到AbstractNioUnsafe#connect

       AbstractNioUnsafe.java
@Override
public final void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
} try {
// 目前有正在连接远程地址的 ChannelPromise ,则直接抛出异常,禁止同时发起多个连接。
if (connectPromise != null) {
// Already a connect in process.
throw new ConnectionPendingException();
}
// 记录 Channel 是否激活
boolean wasActive = isActive();
// 执行连接远程地址
if (doConnect(remoteAddress, localAddress)) {
fulfillConnectPromise(promise, wasActive);
} else {
// 记录 connectPromise
connectPromise = promise;
// 记录 requestedRemoteAddress
requestedRemoteAddress = remoteAddress; // 使用 EventLoop 发起定时任务,监听连接远程地址超时。若连接超时,则回调通知 connectPromise 超时异常。
// 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);
} // 添加监听器,监听连接远程地址取消。
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isCancelled()) {
// 取消定时任务
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
// 置空 connectPromise
connectPromise = null;
close(voidPromise());
}
}
});
}
} catch (Throwable t) {
// 回调通知 promise 发生异常
promise.tryFailure(annotateConnectException(t, remoteAddress));
closeIfClosed();
}
}

调用isActive()方法,获得Channel是否激活,判断SocketChannel是否处于打开,并且连接的状态,此时,一般返回false

    @Override
public boolean isActive() {
SocketChannel ch = javaChannel();
return ch.isOpen() && ch.isConnected();
}

调用doConnect()方法

   NioSocketChannel.java
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
//绑定本地地址
if (localAddress != null) {
doBind0(localAddress);
}
boolean success = false;
try {
//连接远程地址
boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
// 若未连接完成,则关注连接( OP_CONNECT )事件
if (!connected) {
selectionKey().interestOps(SelectionKey.OP_CONNECT);
}
// 标记执行是否成功
success = true;
return connected;
} finally {
// 执行失败,则关闭 Channel
if (!success) {
doClose();
}
}
}

一般情况下NIO client是不需要绑定本地地址的. 所以我们跟踪调用链AbstractChannelHandlerContext可知,localAddress传进来的是null

    AbstractChannelHandlerContext.java
@Override
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return connect(remoteAddress, null, promise);
}

所以走到boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);

若连接没有完成时,我们调用selectionKey().interestOps(SelectionKey.OP_CONNECT);添加事件SelectionKey.OP_CONNECT,也就是说连接远程地址成功时,Channel对应的Selector将会轮训到该事件,可以进一步处理

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();
}

我们进入到AbstractNioChannel#finishConnect方法中

        @Override
public final void finishConnect() {
// Note this method is invoked by the event loop only if the connection attempt was
// neither cancelled nor timed out.
//判断是否在EventLoop的线程中
assert eventLoop().inEventLoop(); try {
//获得Channel是否激活
boolean wasActive = isActive();
//执行完成连接
doFinishConnect();
//通知connectPromice连接完成
fulfillConnectPromise(connectPromise, wasActive);
} catch (Throwable t) {
//通知connectPromice连接异常
fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
} finally {
// 取消 connectTimeoutFuture 任务
// 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
connectPromise = null;
}
}

在调试的过程中我们可以知道isActive();返回false,说明连接还没完成,然后调用doFinishConnect();

    @Override
protected void doFinishConnect() throws Exception {
if (!javaChannel().finishConnect()) {
throw new Error();
}
}

在这里调用java NIO的方法进行连接,然后调用fulfillConnectPromise(ChannelPromise promise, boolean wasActive)

        private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
if (promise == null) {
// Closed via cancellation and the promise has been notified already.
return;
}
// 获得 Channel 是否激活
// Get the state as trySuccess() may trigger an ChannelFutureListener that will close the Channel.
// We still need to ensure we call fireChannelActive() in this case.
boolean active = isActive();
// 回调通知 promise 执行成功
// trySuccess() will return false if a user cancelled the connection attempt.
boolean promiseSet = promise.trySuccess();
// 若 Channel 是新激活的,触发通知 Channel 已激活的事件。
// Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
// because what happened is what happened.
if (!wasActive && active) {
pipeline().fireChannelActive();
} // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
if (!promiseSet) {
close(voidPromise());
}
}

在这里isActive();已返回true,说明已成功连接.

然后回调通知promise执行成功,如果你在connect(...)方法返回的ChannelFuture 的 ChannelFutureListener 的监听器,那么会执行里面的通知.

最后调用pipeline().fireChannelActive();触发Channel激活的事件.调用channelActive()doBeginRead()走server端一样的代码,设置的 readInterestOp = SelectionKey.OP_READ 添加为感兴趣的事件

走完doConnect(remoteAddress, localAddress)我们断点可知返回了false,所以是不会走fulfillConnectPromise(promise, wasActive);,而是执行else分支.

int connectTimeoutMillis = config().getConnectTimeoutMillis();这里可以知道,设置了一个30s的时间

    @Override
public int getConnectTimeoutMillis() {
return connectTimeoutMillis;
}
private volatile int connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT; private static final int DEFAULT_CONNECT_TIMEOUT = 30000;

调用 EventLoop#schedule(Runnable command, long delay, TimeUnit unit) 方法,发起定时任务connectTimeoutFuture,监听连接远程地址是否超时,并回调connectPromise超时异常

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);
}

调用ChannelPromise#addListener(ChannelFutureListener)方法,添加监听器,监听连接远程地址是否取消.若取消,则取消 connectTimeoutFuture 任务,并置空 connectPromise 。这样,客户端 Channel 可以发起下一次连接。

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());
}
}
});

至此为止,doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress)已经全部分析完毕.

Netty源码解析—客户端启动的更多相关文章

  1. Netty源码解析---服务端启动

    Netty源码解析---服务端启动 一个简单的服务端代码: public class SimpleServer { public static void main(String[] args) { N ...

  2. Netty 源码解析(八): 回到 Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第八篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  3. Netty 源码解析(五): Netty 的线程池分析

    今天是猿灯塔“365篇原创计划”第五篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty ...

  4. Netty 源码解析(二):Netty 的 Channel

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty源码解析(一):开始 当前:Netty 源码解析(二): Netty 的 Channel ...

  5. Netty 源码学习——客户端流程分析

    Netty 源码学习--客户端流程分析 友情提醒: 需要观看者具备一些 NIO 的知识,否则看起来有的地方可能会不明白. 使用版本依赖 <dependency> <groupId&g ...

  6. Netty 源码解析(九): connect 过程和 bind 过程分析

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第九篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  7. Netty 源码解析(七): NioEventLoop 工作流程

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第七篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  8. Netty 源码解析(四): Netty 的 ChannelPipeline

    今天是猿灯塔“365篇原创计划”第四篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty ...

  9. Netty 源码解析(三): Netty 的 Future 和 Promise

    今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...

随机推荐

  1. 如何在VMware12中安装centos6.7系统

    一.安装虚拟机,步骤如下: 1.安装好VMware12软件(略过),安装完后点击创建新的虚拟机 2.选择自定义类型安装 3.点击下一步 4.选择稍后安装操作系统,点击[下一步]. 5.客户机操作系统选 ...

  2. 设计模式-访问者模式(Visitor)

    访问者模式是行为模式的一种.访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的.比较稳定的对象结构,这些对象的类都拥有一个accept方法用来接受访问者的访问.访问者是一个接口,它拥有一个vi ...

  3. vue 跨域请求,后端cookie session取不到

    虽然后端设置了可以跨域请求,但是后台设置到cookie中的session取不到!这时候mac电脑自己设置nginx代理! mac电脑系统重装了,记录一下安装nginx的过程: 1.打开终端(cmd) ...

  4. 简述vue中父子组件是怎样相互传递值的(基础向)

    前言 首先,你需要知道vue中父组件和子组件分别指的是什么?   父组件:vue的根实例——用new Vue()构造函数创建的vue实例(实例会有一个挂载点,挂载点里的所有内容可理解为父组件的内容) ...

  5. Maven打包成Jar文件时依赖包的问题

    我们项目中使用到第三方的库文件,这些jar库文件并没有放到Maven中央库上,导致我们需要在项目中自己配置使用.我们的两三个开发人员对Java都是很熟,因此在使用中遇到了一些问题,表现在:在本地中引入 ...

  6. MySQL主从、主主、半同步节点架构的的原理及实验总结

    一.原理及概念: MySQL 主从复制概念 MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点.MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来 ...

  7. 把 python 程序打包成 egg 或者 whl 安装包

    原文出处:http://www.worldhello.net/2010/12/08/2178.html 本文略有改动 1.1 安装setuptools 首先要安装setuptools工具.Debian ...

  8. 【题解】P1892 [BOI2003]团伙-C++

    原题传送门 前置知识:并查集,不会的补了再来. 这道题只是在并查集的基础上多了一个操作而已. 这种操作,叫做反集(就先这么叫着) 题目里有一种关系是互为朋友,这很好理解,把互为朋友的两个点合并就可以了 ...

  9. 【题解】搬书-C++

    搬书 Description 陈老师桌上的书有三堆,每一堆都有厚厚的一叠,你想逗一下陈老师,于是你设计一个最累的方式给他,让他把书 拿下来给同学们.若告诉你这三堆分别有i,j,k本书,以及每堆从下到上 ...

  10. seleniumGrid分布式远程执行测试脚本

    执行UI自动化测试脚本时,不仅可以在本地执行,也可以远程指定某计算机执行测试脚本,seleniumGrid为我们提供了这类服务,但还需要自己搭建环境. 一.本地计算机需要准备java环境和seleni ...