Netty 第五篇:bind(8080) 内部到底发生了什么

 

📅 2026-05-22    ✍️ 原创    🏷️ Java NIO Netty 源码分析 bind

上一篇我们讲清楚了 parentGroup 和 childGroup 的分工,以及为什么 parentGroup 通常只需要 1 个线程。 到目前为止,我们的服务端代码已经配置了 EventLoopGroup、指定了 Channel 类型、设置了 childHandler, 一切看起来都就绪了。

但真正让服务端"活起来"的那行代码,其实是最后这个:

.bind(8080)
.sync();

就是这短短两行,完成了端口绑定、Channel 初始化、Channel 注册到 EventLoop 等一系列操作。 这一篇,我们就来把这个"黑盒子"打开,看看 bind(8080) 内部到底做了什么。

不过先说好:这一篇我们只讲骨架,不挖细节。具体来说,我们会说到 initAndRegister()doBind0() 两大阶段,以及 initAndRegister() 中包含的 Channel 初始化和注册两个步骤。至于每个步骤内部 具体是怎么实现的,我们留到后面的章节逐一展开。


一、从 bind() 方法切入

先回顾一下完整的服务端代码:

package demo.nio;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyServer {
    public static void main(String[] args) throws Exception {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap
                .group(new NioEventLoopGroup(1), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                ByteBuf byteBuf = (ByteBuf) msg;
                                byte[] bytes = new byte[5];
                                byteBuf.readBytes(bytes);
                                System.out.println(new String(bytes));
                                super.channelRead(ctx, msg);
                            }
                        });
                    }
                })
                .bind(8080)
                .sync();
    }
}

我们顺着调用链往下追。


二、bind() 的核心骨架

bind(8080) 最终会调用到 AbstractBootstrap.doBind() 方法。 这个方法的逻辑可以高度简化为以下伪代码:

// AbstractBootstrap.doBind() 的骨架
private ChannelFuture doBind(final SocketAddress localAddress) {
    // 第一阶段:初始化并注册 Channel
    final ChannelFuture regFuture = initAndRegister();

    // 第二阶段:真正执行端口绑定
    doBind0(regFuture, channel, localAddress, promise);

    return promise;
}

整个 bind() 的过程,本质上就是两个阶段:

  1. initAndRegister() —— 创建 Channel、初始化 Channel、把 Channel 注册到 EventLoop 上
  2. doBind0() —— 在 EventLoop 线程中执行真正的端口绑定
一句话概括:bind() = 初始化注册 + 端口绑定。

三、initAndRegister:一切的开始

initAndRegister()bind() 中第一个核心方法。 它的名字已经把要做的事情说得很清楚了:init(初始化)+ register(注册)

这个方法内部主要做了三件事:

3.1 Channel 的创建

还记得我们在 ServerBootstrap 中配置的 .channel(NioServerSocketChannel.class) 吗? 这里就是通过反射,根据这个 Class 对象创建出一个 NioServerSocketChannel 实例。

创建 Channel 的同时,还会一并创建它内部的 ChannelPipelineChannelConfig。 也就是说,Channel 一出生,就自带了一条"生产线"(Pipeline)和一份"配置表"(Config)。

3.2 Channel 的初始化(init)

Channel 创建好了,接下来就是初始化。这一步的核心逻辑在 ServerBootstrap.init() 方法中,主要包括:

  • 把我们在 ServerBootstrap 上配置的 Option(如 SO_BACKLOG)设置到 Channel 上
  • 把我们在 ServerBootstrap 上配置的 Attribute 设置到 Channel 上
  • 向 Channel 的 Pipeline 中添加 ChannelHandler(包括我们配置的 childHandler

简单说,init 阶段就是把之前通过 Builder 模式攒下的所有配置,真正应用到 Channel 上

这一步具体做了什么、Pipeline 中到底添加了哪些 Handler、childHandlerServerBootstrapAcceptor 是什么关系——这些我们会在后面的章节中详细展开。 这一篇我们只知道"有这么一步"就够了。

3.3 Channel 的注册(register)

Channel 初始化完成后,接下来要做的就是把它注册到 EventLoop 上。 具体来说,就是调用 eventLoop().register(channel), 把 Channel 注册到 parentGroup 中某个 NioEventLoop 的 Selector 上。

注册的时候,还会在 Pipeline 中触发一系列事件(如 channelRegistered), 让所有注册的 Handler 都有机会感知到"这个 Channel 已经加入组织了"。

同样,注册的具体细节——比如注册到哪个 NioEventLoop、注册时的 interestOps 是什么、 Pipeline 事件是如何传播的——这些也留到后面再讲。


四、doBind0:真正的端口绑定

initAndRegister() 完成后,Channel 已经被创建出来,也已经被注册到了 parentGroup 的某个 NioEventLoop 上。接下来就是 doBind0() 出场了。

doBind0() 做的事情可以用一句话概括:

在 NioEventLoop 的事件循环线程中,调用 JDK 的 ServerSocketChannel.bind() 完成端口绑定。

这里有几点值得注意:

  1. 端口绑定是在 EventLoop 线程中执行的,不是在调用 bind() 的那个线程中。 这就是为什么 bind() 是非阻塞的——它把真正的工作提交到了 EventLoop 的任务队列中。
  2. 绑定完成后,Selector 开始监听该 Channel 上的 Accept 事件。 从此,这个 Channel 就正式"上岗"了,可以接收客户端连接。
  3. sync() 的作用是等待绑定完成。 如果你不加 sync()bind() 会立刻返回,此时端口可能还没绑好。 加 sync() 就是让当前线程阻塞,直到绑定成功(或失败)。
bind() 方法本身是非阻塞的,它只是把任务提交到了 EventLoop 中。 sync() 才是那个让你"等到绑定完成"的阻塞调用。 如果你的程序不需要阻塞等待,完全可以去掉 sync()

五、完整流程总结

我们把整个 bind(8080) 的过程串起来,用一张表来总结:

阶段方法做的事情执行线程
① Channel 创建 initAndRegister() 通过反射创建 NioServerSocketChannel 调用 bind() 的线程
② Channel 初始化 ServerBootstrap.init() 设置 Options / Attributes / Handler 调用 bind() 的线程
③ Channel 注册 eventLoop().register() 注册到 parentGroup 的 Selector 上 调用 bind() 的线程
④ 端口绑定 doBind0() 在 EventLoop 中执行 JDK bind() parentGroup 的 NioEventLoop 线程

用更通俗的话说:

  1. 先把 Channel 造出来(创建)
  2. 再把配置灌进去(初始化)
  3. 然后把它挂到 EventLoop 的 Selector 上(注册)
  4. 最后让 EventLoop 去绑端口(doBind0)

六、总结

这一篇我们拆解了 bind(8080) 的内部骨架,核心要点如下:

  1. bind() 内部的核心逻辑分为两大阶段:initAndRegisterdoBind0
  2. initAndRegister() 包含三个步骤:Channel 创建 → Channel 初始化 → Channel 注册
  3. doBind0() 在 EventLoop 线程中执行真正的端口绑定,完成后 Selector 开始监听 Accept 事件。
  4. bind() 本身是非阻塞的,sync() 用于阻塞等待绑定完成。
理解了 bind() 的骨架,你就知道了 Netty 服务端启动的"大蓝图"。 接下来我们要做的,就是走进这张蓝图的每一个房间,看看里面到底藏着什么。

— 原创文章,转载请注明出处 —

posted @ 2026-05-29 14:58  小金虫  阅读(12)  评论(0)    收藏  举报