本节摘要:ServerBootstrap 是车间的装配图。本节按时间顺序拆解从 group 到 bind 的每一步:参数落在哪个对象上、initChannel 何时被谁调用、bind 内部经历了哪些阶段,并给出客户端 Bootstrap 的对照与常见装配错误。
第一章抄过那串链式调用,现在逐行标注参数的去向:
ServerBootstrap b = new ServerBootstrap(); b.group(boss, workers) // ① 两组工人:接单组、加工组 .channel(NioServerSocketChannel.class) // ② 监听托盘的材质(反射实例化) .option(ChannelOption.SO_BACKLOG, 1024) // ③ 给监听托盘:三次握手完成队列上限 .option(ChannelOption.SO_REUSEADDR, true) // 重启时立即可绑同端口 .childOption(ChannelOption.TCP_NODELAY, true) // ④ 给每条新连接:禁用 Nagle 小包合并 .childOption(ChannelOption.SO_KEEPALIVE, true) // 连接级保活 .handler(new LoggingHandler(LogLevel.INFO)) // ⑤ 挂在监听托盘上:只看 accept 日志 .childHandler(new ChannelInitializer<SocketChannel>() { // ⑥ 每条新连接的工序表模板 @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new EchoHandler()); } });
最容易混的是三对概念:option 与 childOption(前者作用于监听通道,后者作用于每条 accept 出的新连接);handler 与 childHandler(同理,一个管前台、一个管车间);channel 与 childHandler 里得到的 SocketChannel(一个是店门,一个是客人)。面试与实战里翻车的多半是这三对。
b.bind(8080) 这一行内部发生的事比看上去多。按时间顺序:

这里有一个反直觉的要点值得放大:initChannel 是"连接到了才执行"的。也就是说第一章代码里那段 initChannel,服务启动时一次都不会跑,每个客户端连上来才各跑一次、给那条连接装一份专属工序表。这也解释了为什么 Handler 如果不加 @ChannelSharable 就不能被多个连接共享——每个连接都会经历一次装配。
ChannelFuture f = b.bind(8080).sync(); // 阻塞主线程直到绑定完成 f.channel().closeFuture().sync(); // 阻塞直到监听通道关闭(通常不会自己关) // 优雅停机:拒绝新任务、处理完存量、释放线程 boss.shutdownGracefully(); workers.shutdownGracefully();
sync() 与 await() 的区别值得记:两者都等结果,但 sync() 会把失败"重新抛出",await() 只等不抛,漏看异常的话问题会静默溜走。初学统一用 sync() 更安全。
关闭顺序:先 shutdownGracefully 释放线程组,再退出主线程;反过来可能出现线程未收尾进程先退的竞态。停机细节第八章还会展开。
客户端与服务端的装配图几乎同构,区别是"不用两组工人、没有 child 概念":
Bootstrap cb = new Bootstrap(); cb.group(new NioEventLoopGroup()) // 一组人手,既接拨号又管读写 .channel(NioSocketChannel.class) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) // 拨号超时 .option(ChannelOption.SO_KEEPALIVE, true) .handler(new ChannelInitializer<SocketChannel>() { // 客户端没有 childHandler @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new ClientHandler()); } }); ChannelFuture cf = cb.connect("127.0.0.1", 8080).sync(); cf.addListener(future -> { // 异步风格:不阻塞主线程 if (future.isSuccess()) System.out.println("connected"); });
⚠️ 常见坑三连:其一,
new ChannelInitializer里抛异常会被包装成ChannelPipelineException,日志层级很深,务必在 initChannel 里 try 住并打印;其二,忘了option(SO_BACKLOG)在突发连接风暴时握手队列溢出,客户端看到的是 connect 超时(第八章详析);其三,把连接级参数写进option而不是childOption,参数完全不生效且无报错。
💡 关键直觉:Bootstrap 系列的本质是"参数收集器 + 延迟装配器"。链式调用只是在填表格,真正的装配发生在 connect 或 bind 触发的事件线程上——理解这一点,所有"我明明配置了却不生效"的问题都有了解释框架。