文章目录- 2. Netty大动脉ChannelPipeline
- 2.1. ChannelPipeline
- 2.1.1 Channel 与ChannelPipeline
- 2.1. 3 ChannelInitializer的添加
- 2.1.4自定义ChannelHandler 的添加过程
- 2.1.5 ChannelHandler的名字
- 2.1.6自动生成handler的名字
- 2.1.7关于Pipeline的事件传输机制
- 2.1.8 Outbound的操作(outbound operations of a channel)
- 2.1. ChannelPipeline
- 2.1.1 Channel 与ChannelPipeline
- 2.1. 3 ChannelInitializer的添加
- 2.1.4自定义ChannelHandler 的添加过程
- 2.1.5 ChannelHandler的名字
- 2.1.6自动生成handler的名字
- 2.1.7关于Pipeline的事件传输机制
- 2.1.8 Outbound的操作(outbound operations of a channel)
2. Netty大动脉ChannelPipeline
2.1. ChannelPipeline
2.1.1 Channel 与ChannelPipeline
相信大家都知道了,在Netty中每个Channel 都有且仅有一个ChannelPipeline 与之对应,它们的组成关系如下:
通过上图我们可以看到,一个Channel 包含了一个ChannelPipeline, 而ChannelPipeline中又维护了一个由ChannelHandlerContext 组成的双向链表.这个链表的头是HeadContext,链表的尾是TailContext, 并且每个ChannelHandl erContext 中又关联着一个ChannelHandler. 上面的图示给了我们一个对ChannelPipeline 的直观认识,但是实际上Netty 实现的Channel是否真的是这样的呢?我们继续用源码说话. . 在前我们已经知道了一个Channel 的初始化的基本过程,下面我们再回顾一下. 下面的代码是AbstractChannel 构造器:
代码语言:javascript复制protected AbstractChannel(Channel parent, ChannelId id) {
this.parent = parent;
this.id = id;
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
AbstractChannel有一个pipeline 字段,在构造器中会初始化它为Defaul tChannelPipeline的实例.这里的代码就印证了一点:每个Channel都有一个ChannelPipeline.接着我们跟踪一下DefaultChannelPipeline的初始化过程。
首先进入到Defaul tChannelPipeline 构造器中:
代码语言:javascript复制protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
可以看到,在DefaultChannelPipeline的构造方法中,将传入的channel 赋值给字段this. channel,接着又实例化了两个特殊的字段: tail与head. 这两个字段是一个双向链表的头和尾.其实在Defaul tChannelPipeline中,维 护了一个以AbstractChanne lHandlexContext为节点的双向链表,这个链表是Netty 实现Pipeline 机制的关键. head实现了Channel InboundHandler,而tail 实现了Channe l0utboundHandler接口,并且它们都实现了ChannelHandlerContext 接口,因此可以说head和tail即是一个ChannelHandler, 又是一个ChannelHandlerContext. 接着看一下HeadContext的构造器:
代码语言:javascript复制HeadContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, HEAD_NAME, false, true);
unsafe = pipeline.channel().unsafe();
setAddComplete();
}
它调用了父类AbstractChanne lHandlerContext 的构造器,并传入参数inbound = false, outbound = true.
TailContext的构造器与HeadContext的相反它调用了父类AbstractChanne lHandlerContext的构造器,并传入参数inbound = true, outbound = false. 即header 是一个outboundHandler, 而tail 是一个inboundHandler,关于这一点,大家要特别注意,因为在后面的分析中,我们会反复用到inbound 和outbound 这两个属性.
2.1. 3 ChannelInitializer的添加
前面我们已经分析了Channel 的组成,其中我们了解到,最开始的时候ChannelPipeline 中含有两个Channe lHandl erContext(同时也是ChannelHandler), 但是这个Pipeline 并不能实现什么特殊的功能,因为我们还没有给它添加自定义的ChannelHandler. 通常来说,我们在初始化Bootstrap, 会添加我们自定义的ChannelHandler, 就以我们熟悉的 ChatClient来举例吧:
代码语言:javascript复制Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new IMDecoder());
socketChannel.pipeline().addLast(new IMEncoder());
socketChannel.pipeline().addLast(chatClientHandler);
}
});
ChannelFuture channelFuture = bootstrap.connect(this.host, this.port).sync();
channelFuture.channel().closeFuture().sync();
上面代码的初始化过程,相信大家都不陌生.在调用handler时,传入了ChannelInitializer对象,它提供了一个initChannel 方法供我们初始化ChannelHandler. 那么这个初始化过程是怎样的呢?下面我们就来揭开它的神秘面纱. ChannelInitializer实现了ChannelHandler, 那么它是在什么时候添加到ChannelPipeline中的呢?进行了一番搜索后,我们发现它是在Bootstrap.init 方法中添加到ChannelPipeline中的. 其代码如下:
代码语言:javascript复制void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline();
p.addLast(config.handler());
final Map<ChannelOption<?>, Object> options = options0();
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 = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
上面的代码将handler() 返回的ChannelHandler 添加到Pipeline 中,而handler() 返回的是handler其实就是我们在初始化Bootstrap 调用handler 设置的ChannelInitializer实例,因此这里就是将ChannelInitializer 插入到了Pipeline 的末端. 此时Pipeline 的结构如下图所示:
我们刚才提到,在Bootstrap. init中会调用p. addLast()方法,将ChannelInitializer 插入到链表末端:
代码语言:javascript复制 @Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);
addLast0(newCtx);
// If the registered is false it means that the channel was not registered on an eventloop yet.
// In this case we add the context to the pipeline and add a task that will call
// ChannelHandler.handlerAdded(...) once the channel is registered.
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
@Override
public void run() {
callHandlerAdded0(newCtx);
}
});
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
代码语言:javascript复制private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}
addLast有很多重载的方法,我们关注这个比较重要的方法就可以了。 上面的addLast 方法中,首先检查这个ChannelHandler 的名字是否是重复的,如果不重复的话,则调用newContext方法为这个Handler 创建一个对应的Defaul tChanne lHandl erContext实例,并与之关联起来(Context中有一个handler 属性保存着对应的Handler 实例). 为了添加一个handler 到pipeline 中,必须把此handler 包装成Channe lHandl erContext. 因此在,上面的代码中我们可以看到新实例化了一个newCtx 对象,并将handler作为参数传递到构造方法中.那么我们来看一下实例化的DefaultChanne lHandlerContext到底有什么玄机吧. 首先看它的构造器:
代码语言:javascript复制DefaultChannelHandlerContext(
DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
if (handler == null) {
throw new NullPointerException("handler");
}
this.handler = handler;
}
Defaul tChannelHandlerContext的构造器中,调用了两个很有意思的方法: isInbound 与isOutbound,这两个方法是做什么的呢?
代码语言:javascript复制private static boolean isInbound(ChannelHandler handler) {
return handler instanceof ChannelInboundHandler;
}
private static boolean isOutbound(ChannelHandler handler) {
return handler instanceof ChannelOutboundHandler;
}
从源码中可以看到,当一个handler 实现了Channel InboundHandler接口,则isInbound 返回真;相似地,当一个handler 实现了Channe l0utboundHandler 接口,则isOutbound 就返回真. 而这两个boolean 变量会传递到父类AbstractChanne lHandlerContext中,并初始化父类的两个字段: inbound 与outbound. 那么这里的ChannelInitializer 所对应的Defaul tChanne lHandlerContext的inbound 与inbound字段分别是什么呢?那就看一下ChannelInitializer到底实现了哪个接口不就行了? 如下是ChannelInitializer的类层次结构图:
可以清楚地看到,ChannelInitializer 仅仅实现了Channel InboundHandler接口,因此这里实例化的Defaul tChanne lHandlerContext的inbound = true, outbound = false.不就是inbound 和outbound 两个字段嘛,为什么需要这么大费周章地分析一番?其实这两个字段关系到pipeline的事件的流向与分类求因此是十分关键的,不过我在这里先卖个关子,后面我们再来详细分析这两个字段所起的作用.在这里,我暂且只需要记住,ChannelInitializer所对应的Defaul tChanne lHandlerContext的inbound = true, outbound= false 即可. 当创建好Context后,就将这个Context插入到Pipeline 的双向链表中:
代码语言:javascript复制private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
2.1.4自定义ChannelHandler 的添加过程
前面我们已经分析了一个ChannelInitializer 如何插入到Pipeline 中的,接下来就来探讨一下ChannelInitializer 在哪里被调用,ChannelInitializer 的作用,以及我们自定义的ChannelHandler是如何插入到Pipeline 中的. 现在我们再简单地复习一下Channel的注册过程: 1、首先在AbstractBootstrap. ini tAndRegister中,通过group(). regi ster (channel),调用Multi threadEventLoopGroup. register方法 2、在MultithreadEventLoopGroup. register 中,通过next()获取一个可用的SingleThreadEventLoop,然后调用它的register
3、在SingleThreadEventLoop. register 中,通过channel. unsafe (). register(this,promise)来获取channel 的unsafe() 底层操作对象,然后调用它的register. 4、在AbstractUnsafe. register方法中,调用register0 方法注册Channel 5、在. AbstractUnsafe. register0中,调用AbstractNi oChannel#doRegister方法 6、AbstractNi oChannel. doRegister方法通过javaChannel (). register (eventLoop (). selector, 0,this) 将Channel 对应的Java NIO SockerChannel注册到一个eventLoop 的Selector 中,并且将当前Channel作为attachment. 而我们自定义ChannelHandler 的添加过程,发生在AbstractUnsafe. register0中,在这个方法中调用了pipeline.fireChannelRegistered()方法,其实现如下:
代码语言:javascript复制@Override
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(head);
return this;
}
再看AbstractChannelHandl erContext. invokeChannelRegistered方法:
代码语言:javascript复制static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRegistered();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRegistered();
}
});
}
}
很显然,这个代码会从head开始遍历Pipelhe 的双向链表,然后找到第一个属性inbound为true的 ChannelHandlerContext实例.想起来了没?我们在前面分析ChannelInitializer时,花了大量的笔墨来分析了inbound和outbound属性,你看现在这里就用上了.回想一下,ChannelInitializer实现了ChannelInboudllandler,因此它所对应的 ChannelllandlerContext的inbound属性就是true,因此这里返回就是 ChannelInitializer实例所对应的ChannelHandlerContext.即:
当获取到inbound的 Context后,就调用它的invokeChannelRegistered方法:
代码语言:javascript复制private void invokeChannelRegistered() {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRegistered(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRegistered();
}
}
我们已经强调过了,每个 ChannelHandler都与一个ChannelHandlerContext 关联,我们可以通过ChannelHandlerContext获取到对应的ChannelHandler.因此很显然了,这里handler()返回的,其实就是一开始我们实例化的 ChannelInitializer对象,并接着调用了ChannelInitializer.channelRegistered方法.看到这里,是否会觉得有点眼熟呢?ChannelInitializer.channelRegistered这个方法我们在一开始的时候已经大量地接触了,但是我们并没有深入地分析这个方法的调用过程,那么在这里读者朋友应该对它的调用有了更加深入的了解了吧
那么这个方法中又有什么玄机呢?继续看代码:
代码语言:javascript复制@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
// Normally this method will never be called as handlerAdded(...) should call initChannel(...) and remove
// the handler.
if (initChannel(ctx)) {
// we called initChannel(...) so we need to call now pipeline.fireChannelRegistered() to ensure we not
// miss an event.
ctx.pipeline().fireChannelRegistered();
} else {
// Called initChannel(...) before which is the expected behavior, so just forward the event.
ctx.fireChannelRegistered();
}
}
代码语言:javascript复制@SuppressWarnings("unchecked")
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
try {
initChannel((C) ctx.channel());
} catch (Throwable cause) {
// Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
// We do so to prevent multiple calls to initChannel(...).
exceptionCaught(ctx, cause);
} finally {
remove(ctx);
}
return true;
}
return false;
}
initchannel(©ctx.channel());这个方法我们很熟悉了吧,它就是我们在初始化Bootstrap时,调用handler方法传入的匿名内部类所实现的方法:
代码语言:javascript复制bootstrap.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new IMDecoder());
socketChannel.pipeline().addLast(new IMEncoder());
socketChannel.pipeline().addLast(chatClientHandler);
}
});
因此当调用了这个方法后,我们自定义的 Channelllandler就插入到Pipeline 了,此时的Pipeline如下图所示:
当添加了自定义的Channellandler后,会删除 ChannelInitializer这个ChannelHandler,即"ctx.pipeline().remove(this)",因此最后的 Pipeline如下:
好了,到了这里,我们的自定义 ChannelHandler的添加过程也分析的查不多了
2.1.5 ChannelHandler的名字
我们注意到,pipeline.addXXX都有一个重载的方法,例如addLast,它有一个重载的版本是:
ChannelPipeline addLast(String name,ChannelHandler handler);
第一个参数指定了所添加的handler的名字(更准确地说是 ChannelllandlerContext的名字,不过我们通常是以 handler作为叙述的对象,因此说成handler的名字便于理解).那么handler的名字有什么用呢?如果我们不设置name,那么handler会有怎样的名字?
为了解答这些疑惑,老规矩,依然是从源码中找到答案.
我们还是以addLast 方法为例:
代码语言:javascript复制@Override
public final ChannelPipeline addLast(ChannelHandler... handlers) {
return addLast(null, handlers);
}
这个方法会调用重载的addLast 方法:
代码语言:javascript复制@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
if (handlers == null) {
throw new NullPointerException("handlers");
}
for (ChannelHandler h: handlers) {
if (h == null) {
break;
}
addLast(executor, null, h);
}
return this;
}
第一个参数被设置为null,我们不关心它.第二参数就是这个handler的名字.看代码可知,在添加一个handler之前,需要调用checkMultiplicity方法来确定此 handler和已添加的handler的名字重复.
2.1.6自动生成handler的名字
如果我们调用的是如下的 addLast方法 ChannelPipeline addLast(ChannelHandler… handlers); 那么Netty会调用generateName为我们的handler自动生成一个名字:
代码语言:javascript复制private String filterName(String name, ChannelHandler handler) {
if (name == null) {
return generateName(handler);
}
checkDuplicateName(name);
return name;
}
代码语言:javascript复制private String generateName(ChannelHandler handler) {
Map<Class<?>, String> cache = nameCaches.get();
Class<?> handlerType = handler.getClass();
String name = cache.get(handlerType);
if (name == null) {
name = generateName0(handlerType);
cache.put(handlerType, name);
}
// It's not very likely for a user to put more than one handler of the same type, but make sure to avoid
// any name conflicts. Note that we don't cache the names generated here.
if (context0(name) != null) {
String baseName = name.substring(0, name.length() - 1); // Strip the trailing '0'.
for (int i = 1;; i ) {
String newName = baseName i;
if (context0(newName) == null) {
name = newName;
break;
}
}
}
return name;
}
而generateName会接着调用generateNameO来实际产生一个 handler的名字:
代码语言:javascript复制 private static String generateName0(Class<?> handlerType) {
return StringUtil.simpleClassName(handlerType) "#0";
}
自动生成的名字的规则很简单,就是handler的简单类名加上“#O",因此我们的ChatClientHandler的名字就是"ChatClientHandler#O"
2.1.7关于Pipeline的事件传输机制
前面章节中,我们知道AbstractChannelHandlerContext中有inbound和outbound两个
boolean变量,分别用于标识Context所对应的 handler的类型,即:
1、inbound为真时,表示对应的Channelllandler实现了ChannelInboundllandler方法. 2、outbound为真时,表示对应的ChannelHlandler实现了ChannelOutboundHlandler方法.
这里大家肯定很疑惑了吧:那究竟这两个字段有什么作用呢?其实这还要从ChannelPipeline的传输的事件类型说起. Netty的事件可以分为Inbound和Outbound事件. inbound 事件和outbound事件的流向是不一样的,inbound 事件的流行是从下至上,而outbound刚好相反,是从上到下.并且inbound 的传递方式是通过调用相应的ChannelHandlerContext.fireIN_EVT()方法,而outbound方法的的传递方式是通过调用ChannelHandlerContext.OUT_EVT() 例如ChannellandlerContext.fireChannelRegistered()调用会发送一个 ChannelRegistered 的inbound给下一个ChannellandlerContext,而 ChannelHlandlerContext.bind调用会发送一个bind的outbound事件给下一个ChannelHandlerContext.
注意,如果我们捕获了一个事件,并且想让这个事件继续传递下去,那么需要调用Context相应的传播方法.
例如:
代码语言:javascript复制public void channelActive(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
IMMessage message = new IMMessage(IMP.LOGIN.getName(),System.currentTimeMillis(),this.nickName);
sendMsg(message);
logger.info("成功连接服务器,已执行登录动作");
session();
}
上面的例子中,MyInboundHandler收到了一个 channelActive事件,它在处理后,如果希望将事件继续传播下去,那么需要接着调用ctx.fireChannelActive().
2.1.8 Outbound的操作(outbound operations of a channel)
Outbound事件都是请求事件(request event),即请求某件事情的发生,然后通过Outbound事件进行通知. Outbound事件的传播方向是tail -> customContext ->head. 我们接下来以 connect事件为例,分析一下 Outbound事件的传播机制.
首先,当用户调用了Bootstrap.connect方法时,就会触发一个Connect请求事件,此调用会触发如下调用链: Bootstrap.connect->Bootstrap. doResolveAndConnect->Bootstrap.doResolveAndConnectO->Bootstrap.doConnect->AbstractChannel.connect 继续跟踪的话,我们就发现,AbstractChannel.connect其实由调用了 DefaultChannelPipeline.connect方法:
代码语言:javascript复制@Override
public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
return tail.connect(remoteAddress, localAddress);
}
可以看到,当outbound事件(这里是connect事件)传递到Pipeline后,它其实是以tail为起点开始传播的.
而tail.connect其实调用的是 AbstractChannelHandlerContext.connect方法:
代码语言:javascript复制@Override
public ChannelFuture connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
if (!validatePromise(promise, false)) {
// cancelled
return promise;
}
final AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeConnect(remoteAddress, localAddress, promise);
} else {
safeExecute(executor, new Runnable() {
@Override
public void run() {
next.invokeConnect(remoteAddress, localAddress, promise);
}
}, promise, null);
}
return promise;
}
findContextOutbound()顾名思义,它的作用是以当前Context为起点,向Pipeline中的Context双向链表的前端寻找第一个outbound属性为真的Context(即关联着ChannelOutboundHandler的 Context),然后返回.
它的实现如下:
代码语言:javascript复制private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
当我们找到了一个 outbound 的Context后,就调用它的invokeConnect方法,这个方法中会调用 Context所关联着的 ChannelHandler的connect方法:
代码语言:javascript复制private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
if (invokeHandler()) {
try {
((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
} else {
connect(remoteAddress, localAddress, promise);
}
}
如果用户没有重写ChannelHandler的connect方法,那么会调用ChannelOutboundHandlerAdapter所实现的方法:
代码语言:javascript复制@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
我们看到,ChannelOutboundllandlerAdapter.connect仅仅调用了ctx.connect,而这个调用又回到了:
Context.connect ->Connect.findContextOutbound ->next.invokeConnect ->handler.connect ->Context.connect
这样的循环中,直到connect事件传递到DefaultChannelPipeline的双向链表的头节点,即head中.为什么会传递到head中呢?回想一下,head 实现了ChannelOutboundHlandler,因此它的 outbound属性是true.
因此当connect消息传递到head后,会将消息转递到对应的 ChannelHandler中处理,而恰因此当connect消息传递到head后,会将消息转递到对应的 ChannelHandler中处理,而恰好,head的handler()返回的就是head本身:
代码语言:javascript复制@Override
public ChannelHandler handler() {
return this;
}
因此最终connect事件是在 head中处理的. head的connect事件处理方法如下:
代码语言:javascript复制@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
到这里,整个Connect请求事件就结束了. 下面以一幅图来描述一个整个Connect请求事件的处理过程:
我们仅仅以Connect请求事件为例,分析了Outbound 事件的传播过程,但是其实所有的outbound 的事件传播都遵循着一样的传播规律,同学们可以试着分析一下其他的outbound 事件,体会一下它们的传播过程.