Netty 第五篇:bind(8080) 内部到底发生了什么
上一篇我们讲清楚了 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() 的过程,本质上就是两个阶段:
- initAndRegister() —— 创建 Channel、初始化 Channel、把 Channel 注册到 EventLoop 上
- doBind0() —— 在 EventLoop 线程中执行真正的端口绑定
一句话概括:bind() = 初始化注册 + 端口绑定。
三、initAndRegister:一切的开始
initAndRegister() 是 bind() 中第一个核心方法。 它的名字已经把要做的事情说得很清楚了:init(初始化)+ register(注册)。
这个方法内部主要做了三件事:
3.1 Channel 的创建
还记得我们在 ServerBootstrap 中配置的 .channel(NioServerSocketChannel.class) 吗? 这里就是通过反射,根据这个 Class 对象创建出一个 NioServerSocketChannel 实例。
创建 Channel 的同时,还会一并创建它内部的 ChannelPipeline 和 ChannelConfig。 也就是说,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、childHandler 和 ServerBootstrapAcceptor 是什么关系——这些我们会在后面的章节中详细展开。 这一篇我们只知道"有这么一步"就够了。
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() 完成端口绑定。
这里有几点值得注意:
- 端口绑定是在 EventLoop 线程中执行的,不是在调用
bind()的那个线程中。 这就是为什么bind()是非阻塞的——它把真正的工作提交到了 EventLoop 的任务队列中。 - 绑定完成后,Selector 开始监听该 Channel 上的 Accept 事件。 从此,这个 Channel 就正式"上岗"了,可以接收客户端连接。
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 线程 |
用更通俗的话说:
- 先把 Channel 造出来(创建)
- 再把配置灌进去(初始化)
- 然后把它挂到 EventLoop 的 Selector 上(注册)
- 最后让 EventLoop 去绑端口(doBind0)
六、总结
这一篇我们拆解了 bind(8080) 的内部骨架,核心要点如下:
bind()内部的核心逻辑分为两大阶段:initAndRegister 和 doBind0。initAndRegister()包含三个步骤:Channel 创建 → Channel 初始化 → Channel 注册。doBind0()在 EventLoop 线程中执行真正的端口绑定,完成后 Selector 开始监听 Accept 事件。bind()本身是非阻塞的,sync()用于阻塞等待绑定完成。
理解了 bind() 的骨架,你就知道了 Netty 服务端启动的"大蓝图"。 接下来我们要做的,就是走进这张蓝图的每一个房间,看看里面到底藏着什么。
— 原创文章,转载请注明出处 —

浙公网安备 33010602011771号