netty咱们首先定个基调:Netty是java网络架构,根红苗正的java;现在技术成熟了,公司通信的场景不二人选,虽说现在有ai但是为什么用,怎么用更好还是咱们的基本功!
NIO与Netty
- NIO 就像给你一本《如何造火箭》的书;Netty 是直接送你一艘 SpaceX,附赠马斯克当司机
- javaNIO提供了非阻塞IO的底层能力(selector/channel/buffer这都是老朋友啦)但是api又臭又长,写起来挺麻烦:用螺丝刀组装战斗机的感脚
Netty = 异步事件驱动 + 高性能网络框架 + Java 程序员的 IO 救世主
⚡ 第二幕:Netty------"一场精心策划的 IO 政变"
我们可以把netty的出场想象成一场宫廷政变:几位朝廷老臣(Netty 核心开发者)早已看透:当今皇帝 BIO(Blocking IO) 暴虐无道------来一个使节(请求),就强征一名民夫(线程)全程伺候;万国来朝之日,便是国库破产、民怨沸腾之时!于是,他们秘密拥立 三皇子 Reactor ,誓要发动一场零流血、高效率、可持续 的政变:以事件为令,以异步为刃,建立新王朝!
- 传统 BIO(Blocking IO) :
→ 来一个包裹(请求),就雇一个快递员(线程)专门等它打包、发货。
→ 10000 个包裹?雇 10000 个快递员!
→ 结果:工资发破产,办公室挤成沙丁鱼罐头(线程上下文切换爆炸)。 - Netty 的 Reactor 模型 :
→ 只雇 2 个小队 :- Boss 小队(1人) :专职守宫门,只接见新使节(accept),绝不参与后续
- Worker 小队(CPU *2) :专职处理政务(read/write),绝不接触宫门 → 每个 Worker 是单线程 + 事件循环 ,像闪电侠在多个包裹间瞬移,绝不等待、绝不卡顿!
🏰 政变步骤(对应代码)
public class NettyServer {
public static void main(String[] args) {
#bossGroup只负责accept()新连接(像门卫)
EventLoopGroup boss = new NioEventLoopGroup(1);// 1个足以,专管"接头"
#Worker:负责所有 read/write(像特工小队)
EventLoopGroup worker = new NioEventLoopGroup();// 默认 CPU*2 个Worker,干脏活读写
try {
new ServerBootstrap()
.group(boss, worker) //指定死士队伍
.channel(NioServerSocketChannel.class)//使用 NIO 通道(武器类型)
.option(ChannelOption.SO_BACKLOG, 1024)//等待队列长度(政变预备队)配置 ServerSocketChannel(Boss 用)
.childOption(ChannelOption.TCP_NODELAY, true)//禁用Nagle,消息立刻发!配置SocketChannel(Worker用)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {//注册你的业务 Handler
ch.pipeline().addLast(new EchoHandler());
}
})
.bind(8080).sync(); // ⚡ 关键!启动政变!(业务逻辑)
} catch (Exception e) {
e.printStackTrace();
}
}
}
🔥 第一步:bind() → 点燃导火索!
重中之重的是那个最后的bind,如果没有bind那么一切都只是空想,不落地、空中楼阁,所以咱们现在看看具体是怎么个事,怎么就完美落地,政变成功了?
-
bind()返回一个ChannelFuture,内部调用``doBind(localAddress),然后我们来到了initAndRegister():创建并注册了channel,看到没 环环相扣!这么重要的事情必定是设计地很缜密// AbstractBootstrap.java
final ChannelFuture initAndRegister() {
// 1.建宫殿,否则无法见使节------没有外应
//创建 NioServerSocketChannel封装了 JDK 的 ServerSocketChannel
Channel channel = channelFactory.newChannel();
//派特工,使节天高皇帝远、无法策反
init(channel);// 2. 初始化(加 Pipeline)
// 通禁军咱们老巢在这,打起来 老巢要守住!
//3. 注册到 Boss EventLoop
ChannelFuture regFuture = config().group().register(channel);
return regFuture;
}
🏗️ 创建 NioServerSocketChannel ------ "打造影子宫殿"
上面channelFactory.newChannel()会调用:
// NioServerSocketChannel 构造函数
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER)); // 创建 JDK 的 ServerSocketChannel
}
private static ServerSocketChannel newSocket(SelectorProvider provider) {
return provider.openServerSocketChannel(); // 真正的底层 socket
}
这座宫殿有三大特征:
- 外表合法 :使用官方
ServerSocketChannel,骗过皇帝审查- 内藏机关:自带 Pipeline(未来安插特工的通道)
- 预留兵营 :
SO_BACKLOG=1024(政变预备队等待区)
💡 此时宫殿已成,但大门紧闭、无人知晓------还需两步激活!
📜 init(channel) ------ "安插征兵官(唯一特工)"
@Override
void init(Channel channel) {
// 把 childHandler(你的业务 Handler)存起来
channel.attr(CHILD_HANDLER).set(childHandler);
//给司令部 Pipeline 加一个"特工":"凡有新使节踏入此门立即带往Worker营地,为其洗脑(初始化业务Handler)
//有新连接进来就把它分配给Worker小队并初始化它的 Pipeline!
//Head → 征兵官(ServerBootstrapAcceptor) → Tail
channel.pipeline().addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
ch.pipeline().addLast(new ServerBootstrapAcceptor(
currentChildGroup, currentChildHandler, ...));
}
});
}
🪖 register() ------ "打通禁军(Selector)"
// MultithreadEventLoopGroup.register()
public ChannelFuture register(Channel channel) {
return next().register(channel); // next() = Boss EventLoop(单线程)
}
//SingleThreadEventLoop.register():
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
@Override
protected void doRegister() {
// ⚡ 核心!把 JDK 的 ServerSocketChannel 注册到 Selector 上
selectionKey = javaChannel().register(selector, 0, channel);
}
✅ 至此:
ServerSocketChannel已注册到 Boss 的 Selector 上- 监听的是 OP_ACCEPT 事件(但初始 interestOps=0,稍后才设)
老臣重金收买禁军统领(Selector),将其耳目对准影子宫殿。
但注意 :此时只是"备案",尚未下令监视(
interestOps=0)。为何不直接开启监听?因为宫殿尚未挂牌(未 bind),若提前监听,禁军会报"地址无效"!
💡 这一步是"静默渗透"------让系统认可宫殿的存在,为最后一步铺路。
🚩 :doBind() ------ "正式挂帅出征"
// AbstractChannel.doBind()
@Override
protected void doBind(SocketAddress localAddress) {
// 向皇帝申请:此地可接使节!
javaChannel().socket().bind(localAddress, config.getBacklog()); // JDK bind
pipeline.fireChannelActive(); // 触发 active 事件
}
//NioMessageUnsafe
@Override
protected void doBeginRead() {
// 密令禁军:从此刻起,紧盯此门!
selectionKey.interestOps(selectionKey.interestOps() | readInterestOp);
// readInterestOp = OP_ACCEPT(对 ServerSocketChannel)
}
💥 BOOM!政变机器全面激活!
皇帝批准(bind 成功)→ 影子宫殿合法化
禁军开始监视(OP_ACCEPT)→ 任何叩门声都将触发警报,✅ 至此,政变部署完成,只等第一个使节到来!
🤝 :接受连接 ------ "征兵官登场"
当 Boss 线程在 select() 中收到 OP_ACCEPT:
// NioMessageUnsafe.read()
for (;;) {
SocketChannel ch = serverSocketChannel.accept(); // 接受新连接
if (ch != null) {
readPending = false;
// ⚡ 关键!调用 Pipeline 的 fireChannelRead
pipeline.fireChannelRead(ch);
}
}
事件传递到 ServerBootstrapAcceptor:
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final SocketChannel child = (SocketChannel) msg;
// 1. 把新连接注册到 Worker EventLoop
childGroup.register(child).addListener(...);
// 2. 初始化它的 Pipeline(加入你的 EchoHandler!)
initChannel(child);
}
当使节叩门(客户端 connect):
- 禁军飞报 Boss:"有使节至!"(OP_ACCEPT 事件)
- Boss 开门迎接 (
accept()),拿到使节名帖(新 SocketChannel) - 立即触发 Pipeline → 征兵官现身!
- 征兵官低语 :"随我来",并将使节:
- 注册到 Worker 营地 (
workerGroup.register()) - 洗脑植入业务逻辑 (
initChannel()→ 加入EchoHandler)
- 注册到 Worker 营地 (
- Worker 闪电接手,开始处理政务(read/write)
💥 整个过程无锁、无等待、无上下文切换 ------
Boss 从未踏入 Worker 营地,Worker 也从未见过宫门 ,
全靠征兵官一人穿针引线!
🎯 全流程图
main()
↓
new ServerBootstrap()
↓
bind(8080)
├─ 创建 NioServerSocketChannel(司令部)
├─ 初始化 Pipeline(加入征兵官)
├─ 注册到 Boss EventLoop(Selector 监听 OP_ACCEPT)
└─ bind() 系统调用 + 开启监听
↓
客户端 connect
↓
Boss 线程 accept() → fireChannelRead(new SocketChannel)
↓
ServerBootstrapAcceptor:
├─ 注册到 Worker EventLoop
└─ 初始化子 Channel Pipeline(加入你的 Handler)
↓
Worker 线程处理 read/write
💡 记住 :
Boss 只管"招人"(accept),Worker 才是"干活的"(read/write) 至此就达到了旧IO皇帝无法达到的境地,历来被推倒的王朝都是可以推倒的;
💡再次强调: 为什么是"串行无锁"?
每个
Channel只属于一个 Worker 线程 ,所有操作都在该线程的 EventLoop 中执行------天然线程安全,无需加锁!
🏁 善后(优雅关闭)漏掉了
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
✅ 会等所有任务处理完再 shutdown,避免请求丢失
💡 关键设计:
- 串行无锁化:每个 Channel 只被一个 EventLoop 处理,避免锁竞争
- 减少上下文切换:线程数 ≈ CPU 核心数,效率拉满
好了,自己做主之后就开始吃香的喝辣的吧,大家都是"上we者"了,不要装 🐶
🧩 第三幕:Pipeline ------ "责任链的米其林厨房"
Netty 处理请求,就像一家米其林后厨:
- ChannelPipeline = 传送带
- ChannelHandler = 厨师(每个只干一件事)
- 请求数据 = 一份生牛排
流程:
生牛排 → [解冻师傅] → [腌制大师] → [煎烤大厨] → [摆盘艺术家] → 上菜!
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new HttpRequestDecoder()) // HTTP 解码器(解冻)
.addLast(new HttpObjectAggregator(65536)) // 合并分块(腌制)
.addLast(new HttpResponseEncoder()) // HTTP 编码器(煎烤)
.addLast(new MyBusinessHandler()); // 你的业务逻辑(摆盘)
}
}
✅ 优势:
- 关注点分离:解码、业务、编码各司其职
- 灵活组装 :想加"辣椒粉"(压缩)?插个
HttpContentCompressor就行! - 复用性强:这套流水线,TCP/HTTP/WebSocket 都能跑!
🧠 第四幕:异步与 Future ------ "先开单,后上菜"
-
netty的世界里你不需要等待,想写数据到网络:直接返回future(像一张"取餐号")
ChannelFuture future = ctx.writeAndFlush(response);
-
你可以说:"菜好了叫我!"(加监听器)
future.addListener(f -> {
if (f.isSuccess()) {
System.out.println("客户收到菜了!");
} else {
System.out.println("厨房着火了!" + f.cause());
}
});
💡 哲学 :
"别等我,你先忙,好了喊你" ------ 这就是异步非阻塞的精髓!
🧵 第五幕:内存管理 ------ "堆外内存的精算师"
Netty 为了快,直接绕过 JVM 堆内存 ,用 Direct Buffer(堆外内存):
- 传统方式:数据 → JVM 堆 → 复制到内核 → 网卡(2次拷贝)
- Netty 方式:数据 → 堆外内存 → 内核 → 网卡(1次拷贝)
但堆外内存创建/销毁很贵?Netty 早想到了!
🏦 内存池(PooledByteBufAllocator)
- 向 OS 一次性申请大块内存(Chunk = 16MB)
- 切成小块(Page = 8KB)再细分(Subpage)
- 线程用完不释放,放回"本地缓存",下次直接复用!
✅ 效果 :
零拷贝 + 内存池 = 高频网络传输不 GC、不卡顿!
🛡️ 第六幕:健壮性 ------ "自带防弹衣的战士"
Netty 不只是快,还稳如老狗:
| 问题 | Netty 的解决方案 |
|---|---|
| TCP 粘包/拆包 | LengthFieldBasedFrameDecoder 自动切包 |
| 心跳检测 | IdleStateHandler + 自定义心跳包 |
| Epoll 空轮询 Bug | 检测到空转超阈值,重建 Selector |
| 内存泄漏 | ReferenceCountUtil.release() + ResourceLeakDetector |
🧩TCP 粘包/拆包:自定义协议编解码实战 ------ "造一套黑话系统"
💡 粘包问题:
客户连续发 "Hello" "World",TCP 可能合成 "HelloWorld" 或拆成 "Hel" "loWorld"
而我们即深谋远虑、顾全大局又锱铢必较、看重小节的Netty,他手下的兵Decoder 会按长度字段自动重组,业务层永远拿到完整消息!
在现实中TCP 是字节流 ,没有消息边界,放到汪洋大海中,谁知道
你发 "login:alice" + "msg:hello",对方可能收到:
"login:alicemsg:hello"(粘包)"log"+"in:alice..."(拆包)
能做到如此,是因为手握两大法宝:定义协议 + 编解码器
📜 协议设计
我们设计一个长度 + 内容的私有协议:
4字节长度\]\[JSON内容\]0000001A{"cmd":"login","user":"alice"} ###### 🔧 步骤 1:写 Decoder(拆包 + 解析) //ByteToMessageDecoder 自动处理粘包/拆包 public class MyMessageDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List