3.客户端向服务端建立连接

客户端向服务端建立连接

这篇主要看 TCP 连接在 Netty 中的两条链路:

  1. 客户端主动发起连接:Channel.connect() 是出站事件,最终调用底层 SocketChannel.connect()
  2. 服务端接收连接:服务端 ServerChannel 收到 accept 事件后创建子 SocketChannel,再把子 Channel 注册到 worker EventLoop

注意区分两个概念:

  • TCP 建连:操作系统协议栈完成三次握手。
  • Netty 注册 :把 Channel 绑定到某个 EventLoop,并注册到底层 I/O 多路复用器,例如 Selector。

客户端连接成功不等于"服务端业务 Handler 已经执行完"。它表示客户端本地 SocketChannel 已经连接成功,服务端接收连接、初始化 child Channel、注册到 worker EventLoop 是服务端内部流程。

总体流程

text 复制代码
客户端:
Channel.connect()
    -> DefaultChannelPipeline.connect()
    -> TailContext.connect()
    -> AbstractChannelHandlerContext.connect()
    -> HeadContext.connect()
    -> AbstractNioUnsafe.connect()
    -> NioSocketChannel.doConnect()
        -> SocketChannel.connect()
        -> 立即成功,或注册 OP_CONNECT 等待连接完成

服务端:
Selector 发现 ServerChannel 可 accept
    -> NioMessageUnsafe.read()
    -> NioServerSocketChannel.doReadMessages()
    -> pipeline.fireChannelRead(child)
    -> ServerBootstrapAcceptor.channelRead()
    -> child.pipeline().addLast(childHandler)
    -> childGroup.register(child)
    -> child channelRegistered / channelActive

客户端发起连接

客户端调用的入口是 Channel.connect()

java 复制代码
default ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    return pipeline().connect(remoteAddress, promise);
}

connect 是一个出站操作,所以会进入 ChannelPipeline 的出站传播链。

java 复制代码
public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    return tail.connect(remoteAddress, promise);
}

tail 开始是因为出站事件的传播方向是 Tail -> Head

Pipeline 出站传播

TailContext.connect() 最终会进入 AbstractChannelHandlerContext.connect()

java 复制代码
public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    return connect(remoteAddress, null, promise);
}

核心逻辑如下:

java 复制代码
public ChannelFuture connect(final SocketAddress remoteAddress,
                             final SocketAddress localAddress,
                             ChannelPromise promise) {
    // 参数、Promise 状态检查
    ...

    // 从当前节点向前查找下一个能处理 connect 的出站 Handler
    final AbstractChannelHandlerContext next = findContextOutbound(MASK_CONNECT);
    EventExecutor executor = next.executor();

    if (executor.inEventLoop()) {
        if (next.invokeHandler()) {
            promise = ensurePromiseUseCorrectExecutor(promise);
            try {
                final ChannelHandler handler = next.handler();
                final DefaultChannelPipeline.HeadContext headContext = pipeline.head;

                if (handler == headContext) {
                    headContext.connect(next, remoteAddress, localAddress, promise);
                } else if (handler instanceof ChannelDuplexHandler) {
                    ((ChannelDuplexHandler) handler).connect(next, remoteAddress, localAddress, promise);
                } else if (handler instanceof ChannelOutboundHandlerAdapter) {
                    ((ChannelOutboundHandlerAdapter) handler).connect(next, remoteAddress, localAddress, promise);
                } else {
                    ((ChannelOutboundHandler) handler).connect(next, remoteAddress, localAddress, promise);
                }
            } catch (Throwable t) {
                notifyOutboundHandlerException(t, promise);
            }
        } else {
            next.connect(remoteAddress, localAddress, promise);
        }
    } else {
        // 如果当前线程不是对应 executor 的线程,就提交任务过去执行
        final ChannelPromise p = promise;
        safeExecute(executor, () -> connect(remoteAddress, localAddress, p), promise, null, false);
    }
    return promise;
}

查找出站 Handler 的方法是 findContextOutbound()

java 复制代码
private AbstractChannelHandlerContext findContextOutbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        // 出站事件向前传播,也就是沿 prev 指针查找
        ctx = ctx.prev;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
    return ctx;
}

这里要理解两点:

  1. findContextOutbound() 不是查找任意 Handler,而是查找能处理当前出站事件的 Handler。
  2. connect 最终一定会走到 HeadContext,因为真正访问底层网络的逻辑在 Head 节点里。

入站和出站事件

Netty 的 ChannelPipeline 是一个双向链表,链表中每个节点都是一个 ChannelHandlerContext。两端有两个特殊节点:

  • HeadContext:靠近底层 I/O,负责把事件接入或写出到底层。
  • TailContext:入站事件的终点,出站事件的起点。

事件传播方向取决于事件类型:

类型 例子 传播方向 说明
入站事件 channelReadchannelActiveexceptionCaught Head -> Tail 底层 I/O 触发,向业务 Handler 传播
出站事件 connectbindwriteflushclose Tail -> Head 应用主动发起,最终由 Head 写到底层

所以客户端主动连接服务端时,connect 是出站事件;服务端接收到新连接后触发的 channelRead(child) 是入站事件。

客户端底层连接

NioSocketChannel 为例,Pipeline 传播到 HeadContext 后,会调用 unsafe.connect(),实际实现位于 AbstractNioChannel.AbstractNioUnsafe.connect()

java 复制代码
public final void connect(final SocketAddress remoteAddress,
                          final SocketAddress localAddress,
                          final ChannelPromise promise) {
    // Promise 已完成或 Channel 已关闭时直接返回
    if (promise.isDone() || !ensureOpen(promise)) {
        return;
    }

    try {
        if (connectPromise != null) {
            // 已经有连接正在进行,不能重复 connect
            throw new ConnectionPendingException();
        }

        boolean wasActive = isActive();

        if (doConnect(remoteAddress, localAddress)) {
            // 底层连接立即成功
            fulfillConnectPromise(promise, wasActive);
        } else {
            // 非阻塞连接尚未完成,保存 Promise,等待 OP_CONNECT
            connectPromise = promise;
            requestedRemoteAddress = remoteAddress;

            final int connectTimeoutMillis = config().getConnectTimeoutMillis();
            if (connectTimeoutMillis > 0) {
                connectTimeoutFuture = eventLoop().schedule(() -> {
                    ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                    if (connectPromise != null && !connectPromise.isDone()
                            && connectPromise.tryFailure(new ConnectTimeoutException(...))) {
                        close(voidPromise());
                    }
                }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }

            promise.addListener(future -> {
                if (future.isCancelled()) {
                    if (connectTimeoutFuture != null) {
                        connectTimeoutFuture.cancel(false);
                    }
                    connectPromise = null;
                    close(voidPromise());
                }
            });
        }
    } catch (Throwable t) {
        promise.tryFailure(annotateConnectException(t, remoteAddress));
        closeIfClosed();
    }
}

这里 connectTimeoutFuture 负责超时失败处理:如果超时时连接还没完成,就把 connectPromise 置为失败并关闭 Channel。

真正连接成功有两种路径:

  1. doConnect() 返回 true:连接立即完成,直接调用 fulfillConnectPromise()
  2. doConnect() 返回 false:连接还在进行,注册 OP_CONNECT,等 Selector 后续通知连接完成。

第二种路径比较容易误解:它不是服务端主动回调客户端,而是客户端本机的 Selector 监听到了底层 Socket 的连接完成事件。

NioSocketChannel.doConnect()

NioSocketChannel.doConnect() 负责调用 JDK NIO 的底层连接方法。

java 复制代码
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        doBind0(localAddress);
    }

    boolean success = false;
    try {
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
        // 非阻塞连接没有立即完成时,关注 CONNECT 事件
        if (!connected) {
            addAndSubmit(NioIoOps.CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}

这里的关键点:

  • 如果配置了本地地址,会先执行本地绑定。
  • SocketUtils.connect(javaChannel(), remoteAddress) 底层对应 JDK SocketChannel.connect()
  • 非阻塞模式下,connect() 可能不能立即完成,此时返回 false
  • 返回 false 时,Netty 会注册 CONNECT 事件,等待客户端本机 Selector 后续收到连接完成通知。

doConnect 返回 false 后做了什么

doConnect() 返回 false 后,AbstractNioUnsafe.connect() 会先保存本次连接的上下文:

java 复制代码
connectPromise = promise;
requestedRemoteAddress = remoteAddress;

这两个字段后面会在 finishConnect() 中使用:

  • connectPromise:用来通知 Bootstrap.connect() 返回的 ChannelFuture 成功或失败。
  • requestedRemoteAddress:连接失败时用于包装异常信息。

同时,NioSocketChannel.doConnect() 里已经调用了:

java 复制代码
addAndSubmit(NioIoOps.CONNECT);

这一步的作用是把当前 Channel 对应的 SelectionKey 改成关注连接完成事件。调用链大致是:

java 复制代码
AbstractNioChannel.addAndSubmit(NioIoOps.CONNECT)
    -> SingleThreadIoEventLoop.IoRegistrationWrapper.submit(...)
    -> NioIoHandler.DefaultNioRegistration.submit(...)
    -> SelectionKey.interestOps(...)

核心代码可以理解为:

java 复制代码
public long submit(IoOps ops) {
    if (!isValid()) {
        return -1;
    }

    int v = cast(ops).value;
    key.interestOps(v);
    return v;
}

key.interestOps(v) 就是告诉 Selector:这个 Channel 现在关心哪些 I/O 事件。这里的 NioIoOps.CONNECT 可以理解成 JDK NIO 里的 SelectionKey.OP_CONNECT

完整过程是:

text 复制代码
SocketChannel.connect(remoteAddress) 返回 false
    -> TCP 三次握手还在进行
    -> Netty 保存 connectPromise
    -> SelectionKey 关注 CONNECT
    -> EventLoop 继续执行 select()
    -> 操作系统发现连接完成
    -> Selector 返回 CONNECT 就绪事件

异步连接完成

当底层连接完成后,Selector 会返回 connect-ready 事件。Netty 4.2 中对应处理在 AbstractNioChannel.AbstractNioUnsafe.handle()

java 复制代码
public void handle(IoRegistration registration, IoEvent event) {
    try {
        NioIoEvent nioEvent = (NioIoEvent) event;
        NioIoOps nioReadyOps = nioEvent.ops();

        if (nioReadyOps.contains(NioIoOps.CONNECT)) {
            // 连接完成后移除 CONNECT 监听,避免 select 不断返回
            removeAndSubmit(NioIoOps.CONNECT);
            // 调用 SocketChannel.finishConnect() 完成连接收尾
            unsafe().finishConnect();
        }

        if (nioReadyOps.contains(NioIoOps.WRITE)) {
            forceFlush();
        }

        if (nioReadyOps.contains(NioIoOps.READ_AND_ACCEPT) || nioReadyOps.equals(NioIoOps.NONE)) {
            read();
        }
    } catch (CancelledKeyException ignored) {
        close(voidPromise());
    }
}

这里先调用 removeAndSubmit(NioIoOps.CONNECT),是因为连接完成事件只需要处理一次。如果不移除,Selector 可能会持续报告 connect-ready。

finishConnect() 会真正完成连接收尾。

java 复制代码
public final void finishConnect() {
    assert eventLoop().inEventLoop();

    try {
        boolean wasActive = isActive();
        doFinishConnect();
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
    } finally {
        if (connectTimeoutFuture != null) {
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}

NioSocketChannel.doFinishConnect() 最终调用 JDK 的 SocketChannel.finishConnect()。这一步不是重新发起连接,而是确认之前那次非阻塞连接是否已经真正完成。

java 复制代码
protected void doFinishConnect() throws Exception {
    if (!javaChannel().finishConnect()) {
        throw new UnsupportedOperationException("finishConnect is not supported for " + getClass().getName());
    }
}

如果 finishConnect() 成功,fulfillConnectPromise() 会设置连接 Promise 成功。如果 Channel 状态从 inactive 变成 active,还会触发 pipeline.fireChannelActive(),这时客户端 Handler 才能感知连接可用。

所以 Bootstrap.connect(host, port).sync() 等待的就是这个 Promise 的完成:

text 复制代码
finishConnect()
    -> doFinishConnect()
    -> fulfillConnectPromise(connectPromise, wasActive)
    -> promise.trySuccess()
    -> pipeline.fireChannelActive()

如果 finishConnect() 失败,则会把异常设置到 connectPromise 上,connect().sync() 会感知到失败。

服务端接收连接

服务端启动后,NioServerSocketChannel 已经注册到 boss/parent EventLoop,并监听 accept 事件。当客户端连接到来时,服务端 EventLoop 的 Selector 会返回可读/可接收事件。

大致链路是:

text 复制代码
SingleThreadIoEventLoop.run()
    -> runIo()
    -> NioIoHandler.run()
    -> processSelectedKeys()
    -> processSelectedKey()
    -> AbstractNioUnsafe.handle()
    -> read()

NioIoHandler.run() 会处理本轮 Selector 选中的 key。

java 复制代码
public int run(IoHandlerContext context) {
    int handled = 0;
    try {
        cancelledKeys = 0;
        needsToSelectAgain = false;

        if (context.shouldReportActiveIoTime()) {
            long activeIoStartTimeNanos = System.nanoTime();
            handled = processSelectedKeys();
            long activeIoEndTimeNanos = System.nanoTime();
            context.reportActiveIoTime(activeIoEndTimeNanos - activeIoStartTimeNanos);
        } else {
            handled = processSelectedKeys();
        }
    } catch (Error e) {
        throw e;
    } catch (Throwable t) {
        handleLoopException(t);
    }
    return handled;
}

对于服务端 ServerChannel 来说,read() 不是读取普通字节,而是接收新连接,生成子 SocketChannel

java 复制代码
public void read() {
    ...
    try {
        do {
            int localRead = doReadMessages(readBuf);
            if (localRead == 0) {
                break;
            }
            if (localRead < 0) {
                closed = true;
                break;
            }

            allocHandle.incMessagesRead(localRead);
        } while (continueReading(allocHandle));
    } catch (Throwable t) {
        exception = t;
    }

    int size = readBuf.size();
    for (int i = 0; i < size; i++) {
        readPending = false;
        // 对 ServerChannel 来说,这里的 msg 是 accepted child Channel
        pipeline.fireChannelRead(readBuf.get(i));
    }
    readBuf.clear();
    allocHandle.readComplete();
    pipeline.fireChannelReadComplete();
    ...
}

也就是说,服务端 accept 到的新连接会作为一个 child Channel 通过 pipeline.fireChannelRead(child) 继续向后传播。

ServerBootstrapAcceptor

服务端启动时,ServerBootstrap.init() 会给 ServerChannel 的 Pipeline 添加 ServerBootstrapAcceptor。这个 Handler 专门处理 accept 出来的子 Channel。

ServerBootstrapAcceptor.channelRead() 的核心逻辑:

java 复制代码
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;

    // 给子 Channel 添加用户配置的 childHandler
    child.pipeline().addLast(childHandler);

    // 设置 childOptions
    setChannelOptions(child, childOptions, logger);

    // 设置 childAttrs
    setAttributes(child, childAttrs);

    // 执行扩展点
    ...

    // 将子 Channel 注册到 childGroup,也就是 worker EventLoopGroup
    childGroup.register(child).addListener(future -> {
        if (!future.isSuccess()) {
            forceClose(child, future.cause());
        }
    });
}

这个步骤的意义是:ServerChannel 只负责监听和 accept,新建立的客户端连接由子 SocketChannel 表示,并交给 worker EventLoop 负责后续读写。

子 Channel 注册成功

子 Channel 的注册逻辑和客户端 Channel 注册逻辑一样,都会进入 AbstractChannel.AbstractUnsafe.register()

java 复制代码
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    ObjectUtil.checkNotNull(eventLoop, "eventLoop");

    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    if (!isCompatible(eventLoop)) {
        promise.setFailure(new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }

    // 绑定当前 Channel 到指定 EventLoop
    AbstractChannel.this.eventLoop = eventLoop;

    // 清理 Pipeline 中缓存的 executor,避免继续引用旧 EventLoop
    AbstractChannelHandlerContext context = pipeline.tail;
    do {
        context.contextExecutor = null;
        context = context.prev;
    } while (context != null);

    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        eventLoop.execute(() -> register0(promise));
    }
}

真正注册成功后的处理在 register0() 中。

java 复制代码
private void register0(ChannelPromise promise) {
    ...
    registerPromise.addListener(future -> {
        if (future.isSuccess()) {
            neverRegistered = false;
            registered = true;

            // 先触发 handlerAdded,保证用户 Handler 已经就位
            pipeline.invokeHandlerAddedIfNeeded();

            safeSetSuccess(promise);

            // 触发 channelRegistered
            pipeline.fireChannelRegistered();

            // accepted SocketChannel 通常已经是 active,所以首次注册后会触发 channelActive
            if (isActive()) {
                if (firstRegistration) {
                    pipeline.fireChannelActive();
                } else if (config().isAutoRead()) {
                    beginRead();
                }
            }
        } else {
            close(newPromise());
            closeFuture.setClosed();
            safeSetFailure(promise, future.cause());
        }
    });
    ...
}

这里要注意事件顺序:

text 复制代码
childGroup.register(child)
    -> doRegister()
    -> pipeline.invokeHandlerAddedIfNeeded()
    -> pipeline.fireChannelRegistered()
    -> pipeline.fireChannelActive()

channelActive() 触发后,这个服务端 child Channel 才算进入可读写状态。

最容易混淆的点

1. 客户端 connect 和服务端 accept 不是同一条 Pipeline

客户端 connect 发生在客户端 Channel 的 Pipeline 中,是出站事件。

服务端 accept 发生在服务端 ServerChannel 的 Pipeline 中,是入站事件。accept 之后产生的 child Channel 又会有自己的 Pipeline。

2. TCP 建连不是"客户端到服务端单向注册"

客户端调用 connect() 发起 TCP 连接;服务端通过 accept() 接收连接。两边都会各自创建或激活自己的 Channel。

Netty 层面的 register() 只是把本端 Channel 绑定到本端 EventLoop,不会跨进程把客户端 Channel "注册到服务端"。服务端注册的是自己 accept 出来的 child Channel。

3. 服务端不会因为 child 注册成功自动回调客户端业务代码

服务端 child Channel 注册成功后,会触发服务端自己的 channelRegistered()channelActive()。客户端只会在自己的连接完成后触发客户端侧的 channelActive()

如果服务端想通知客户端业务状态,需要主动 writeAndFlush() 发送数据;这属于后续的数据读写流程,不属于建连注册流程。

4. EventLoop 线程启动和注册任务有关

register() 如果不在目标 EventLoop 线程内执行,会通过 eventLoop.execute() 提交任务。SingleThreadEventExecutor.execute() 会确保 EventLoop 线程启动,随后在线程主循环中持续处理任务队列和 I/O 事件。

所以可以理解为:注册任务会把 Channel 的实际注册逻辑切到 EventLoop 线程中执行,但不要把它理解成每次注册都会新建一个线程;一个 SingleThreadEventExecutor 对应一个长期运行的事件循环线程。

相关推荐
沙湖遇雨1 小时前
1.netty源码阅读-管理端Server启动
后端
玖石书1 小时前
ASP.NET Core迁移Spring系列:运行时与 GC 篇
后端·spring·asp.net
谁来拯救笨小孩1 小时前
Cursor 流式协议:为何不是 REST,HTTP/2 与双向流怎么选
后端
明月光8181 小时前
在数据库里做权限控制?重新理解 PostgreSQL 的 RLS(行级安全)
后端
用户298698530141 小时前
PDF 转图片?三种方案,覆盖全平台与自动化场景
人工智能·后端
沙湖遇雨1 小时前
2.启动客户端client
后端
程序员只因1 小时前
SpringBoot实战项目目录结构+高频注解详解(黑马点评适配版)
后端
Conan在掘金1 小时前
鸿蒙 7.0 小艺全面进化:超 200 项感知+全天候引擎+超强记忆——AI 私人助理根因
后端
老孙讲技术1 小时前
值班室画面慢半拍?5 分钟拿齐乐橙设备 HLS / FLV / 低延迟三条可播路径
后端·物联网