客户端向服务端建立连接
这篇主要看 TCP 连接在 Netty 中的两条链路:
- 客户端主动发起连接:
Channel.connect()是出站事件,最终调用底层SocketChannel.connect()。 - 服务端接收连接:服务端
ServerChannel收到 accept 事件后创建子SocketChannel,再把子 Channel 注册到 workerEventLoop。
注意区分两个概念:
- 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;
}
这里要理解两点:
findContextOutbound()不是查找任意 Handler,而是查找能处理当前出站事件的 Handler。connect最终一定会走到HeadContext,因为真正访问底层网络的逻辑在 Head 节点里。
入站和出站事件
Netty 的 ChannelPipeline 是一个双向链表,链表中每个节点都是一个 ChannelHandlerContext。两端有两个特殊节点:
HeadContext:靠近底层 I/O,负责把事件接入或写出到底层。TailContext:入站事件的终点,出站事件的起点。
事件传播方向取决于事件类型:
| 类型 | 例子 | 传播方向 | 说明 |
|---|---|---|---|
| 入站事件 | channelRead、channelActive、exceptionCaught |
Head -> Tail | 底层 I/O 触发,向业务 Handler 传播 |
| 出站事件 | connect、bind、write、flush、close |
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。
真正连接成功有两种路径:
doConnect()返回true:连接立即完成,直接调用fulfillConnectPromise()。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)底层对应 JDKSocketChannel.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 对应一个长期运行的事件循环线程。