Netty 网络编程实战指南
Netty 是 Java 高并发网络编程的事实标准——Dubbo、RocketMQ、Elasticsearch、Sentinel 底层全是它。也是 CSDN Java 板块的热门话题。本文从 IO 模型讲起,覆盖 NIO、Netty 核心组件、EventLoop、ChannelHandler、编解码、粘包拆包、实战一个 RPC 雏形、常见坑。
一、为什么要 Netty
1.1 Java IO 演进
BIO(阻塞 IO):一个线程一个连接 → 万级连接就爆炸 NIO(非阻塞):多路复用,一个线程管万级连接 Netty:把 NIO 封装得优雅好用(Dubbo/RocketMQ/ES 都在用)一句话:Netty 让 Java 开发者能写出高性能网络服务,而不用跟底层 NIO API 死磕。
1.2 Netty 优势
- 性能高:NIO + 零拷贝 + 池化缓冲区;
- 易用:Handler 链式处理,屏蔽了 NIO 复杂性;
- 可靠:心跳、重连、断线重传有现成组件;
- 生态:HTTP、WebSocket、RPC 协议都有实现。
二、NIO 基础(先懂原理)
2.1 三大组件
// Channel:连接(读写的通道)// Buffer:缓冲区(数据载体)// Selector:多路复用器(一个线程监听多个 Channel)一个 Selector 监听多个 Channel: Channel1 可读 → 处理 Channel2 可写 → 处理 Channel3 有连接 → 处理2.2 NIO vs BIO vs AIO
| 模型 | 阻塞 | 线程模型 | 适用 |
|---|---|---|---|
| BIO | 阻塞 | 一连接一线程 | 连接少 |
| NIO | 非阻塞 | 一线程多连接(Selector) | 高并发 |
| AIO | 异步 | 回调 | 真异步(Linux 支持一般) |
Netty 基于 NIO(多路复用)——就是操作系统篇讲过的 epoll。
三、Netty 核心组件
3.1 组件关系
Bootstrap(启动器) ├── EventLoopGroup(事件循环组,本质是线程池) │ └── EventLoop(一个线程 + 一个 Selector) ├── Channel(连接) ├── ChannelPipeline(Handler 链表) │ ├── 入站 Handler(解码/业务) │ └── 出站 Handler(编码) └── ChannelHandler(业务逻辑)关键规则:一个 Channel 的所有操作由同一个 EventLoop 串行执行——所以 Handler 里不用加锁(无并发问题)。
3.2 服务端最小实现
publicclassNettyServer{publicstaticvoidmain(String[]args)throwsException{// 1. 创建线程组:boss 管连接,worker 管读写EventLoopGroupboss=newNioEventLoopGroup(1);EventLoopGroupworker=newNioEventLoopGroup();try{// 2. 启动器ServerBootstrapbootstrap=newServerBootstrap();bootstrap.group(boss,worker).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer<SocketChannel>(){@OverrideprotectedvoidinitChannel(SocketChannelch){// 3. 添加 Handler(编解码 + 业务)ch.pipeline().addLast(newStringDecoder()).addLast(newStringEncoder()).addLast(newServerHandler());}});// 4. 绑定端口启动ChannelFuturefuture=bootstrap.bind(8080).sync();System.out.println("服务器启动: 8080");future.channel().closeFuture().sync();}finally{boss.shutdownGracefully();worker.shutdownGracefully();}}}// 业务 HandlerpublicclassServerHandlerextendsSimpleChannelInboundHandler<String>{@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,Stringmsg){System.out.println("收到: "+msg);// 回复ctx.writeAndFlush("服务端回复: "+msg);}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause){cause.printStackTrace();ctx.close();}}3.3 客户端
publicclassNettyClient{publicstaticvoidmain(String[]args)throwsException{EventLoopGroupgroup=newNioEventLoopGroup();try{Bootstrapbootstrap=newBootstrap();bootstrap.group(group).channel(NioSocketChannel.class).handler(newChannelInitializer<SocketChannel>(){@OverrideprotectedvoidinitChannel(SocketChannelch){ch.pipeline().addLast(newStringDecoder()).addLast(newStringEncoder()).addLast(newClientHandler());}});Channelchannel=bootstrap.connect("127.0.0.1",8080).sync().channel();channel.writeAndFlush("你好,Netty!");channel.closeFuture().sync();}finally{group.shutdownGracefully();}}}四、EventLoop 与线程模型(面试重点)
4.1 单线程事件循环
EventLoop = 一个线程 + 一个 Selector + 一个任务队列 处理流程: 1. Selector 监听就绪事件 2. 处理 IO 事件(读/写) 3. 执行队列中的普通任务 4. 循环4.2 为什么 Handler 不用加锁
一个 Channel 绑定一个固定 EventLoop——该 Channel 的所有操作(读、业务、写)都在同一线程串行执行。
这意味着:Handler 里直接改共享对象(只要只被一个 Channel 用)不需要 synchronized。
4.3 线程模型对比(面试常问)
| 模型 | 说明 |
|---|---|
| 单线程模型 | 一个 EventLoop 全干(简单,性能低) |
| 多线程模型 | boss 一个线程,worker 多线程(最常用) |
| 主从模型 | boss 多个线程(海量连接场景) |
默认配置:
// boss 线程数:1(够了,只管 accept)// worker 线程数:默认 2 * CPU 核数EventLoopGroupworker=newNioEventLoopGroup();五、粘包与拆包(必考)
5.1 问题
TCP 是字节流,没有消息边界:
发送方:ABC DEF 接收方可能收到:AB | CD | EF 或 ABCDEF(粘包/拆包)5.2 解决方案
| 方案 | 说明 | 适用 |
|---|---|---|
| 固定长度 | 每消息固定字节 | 简单但浪费 |
| 分隔符 | 按 \n 分割 | 文本协议 |
| 长度字段 | 头存长度 | 二进制协议(主流) |
| 自定义协议 | 魔数+版本+长度 | RPC/游戏 |
5.3 长度字段解码器(主流方案)
// 协议格式:4字节长度 + 内容ch.pipeline().addLast(newLengthFieldBasedFrameDecoder(1024,// 最大帧长度0,// 长度字段偏移4,// 长度字段长度0,// 长度调整4// 跳过的字节数(剥离长度头)));ch.pipeline().addLast(newStringDecoder());遇到"收到半截消息/多条消息黏一起"——第一反应就是加解码器。
六、编解码与自定义协议
6.1 序列化选择
// 常用:JSON(人可读)、Protobuf(性能)、JDK(不推荐)6.2 自定义 RPC 请求
publicclassRpcRequest{privateintrequestId;privateStringmethodName;privateObject[]args;// getter/setter}// 编码器publicclassRpcEncoderextendsMessageToByteEncoder<RpcRequest>{@Overrideprotectedvoidencode(ChannelHandlerContextctx,RpcRequestmsg,ByteBufout)throwsException{// 1. 写魔数(协议标识)out.writeInt(0x12345678);// 2. 写长度byte[]body=JSON.toJSONBytes(msg);out.writeInt(body.length);// 3. 写内容out.writeBytes(body);}}// 解码器(配合 LengthFieldBasedFrameDecoder 拆包)publicclassRpcDecoderextendsByteToMessageDecoder{@Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,List<Object>out)throwsException{// 读魔数校验intmagic=in.readInt();if(magic!=0x12345678){ctx.close();return;}intlen=in.readInt();byte[]body=newbyte[len];in.readBytes(body);out.add(JSON.parseObject(body,RpcRequest.class));}}零拷贝注意:协议里用ByteBuf,别在 Handler 里频繁toString()+getBytes()——性能杀手。
七、心跳与断线重连(生产必备)
7.1 服务端:IdleStateHandler
// 3 种空闲超时:读空闲/写空闲/读写空闲ch.pipeline().addLast(newIdleStateHandler(60,0,0,TimeUnit.SECONDS));// Handler 里处理publicclassHeartbeatHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoiduserEventTriggered(ChannelHandlerContextctx,Objectevt)throwsException{if(evtinstanceofIdleStateEvent){// 60 秒没读到数据 → 连接可能死了System.out.println("读空闲,关闭连接: "+ctx.channel().remoteAddress());ctx.close();}else{super.userEventTriggered(ctx,evt);}}}7.2 客户端:定时心跳
// 客户端启动后定时发心跳channel.eventLoop().scheduleAtFixedRate(()->{if(channel.isActive()){channel.writeAndFlush(newHeartbeatPacket());}},5,5,TimeUnit.SECONDS);// 断线重连// 在 channelInactive 里延时重连@OverridepublicvoidchannelInactive(ChannelHandlerContextctx)throwsException{System.out.println("连接断开,5 秒后重连");ctx.channel().eventLoop().schedule(()->connect(),5,TimeUnit.SECONDS);}八、实战:迷你 RPC 服务
// 服务端:注册服务 + 接收调用publicclassMiniRpcServer{privatefinalMap<String,Object>services=newHashMap<>();publicvoidregister(Stringname,Objectservice){services.put(name,service);}publicvoidstart(intport){EventLoopGroupboss=newNioEventLoopGroup(1);EventLoopGroupworker=newNioEventLoopGroup();try{newServerBootstrap().group(boss,worker).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer<SocketChannel>(){@OverrideprotectedvoidinitChannel(SocketChannelch){ch.pipeline().addLast(newLengthFieldBasedFrameDecoder(1024,4,4)).addLast(newRpcDecoder()).addLast(newRpcEncoder()).addLast(newRpcServerHandler(services));}}).bind(port).sync().channel().closeFuture().sync();}catch(Exceptione){e.printStackTrace();}finally{boss.shutdownGracefully();worker.shutdownGracefully();}}}// 业务处理:反射调用publicclassRpcServerHandlerextendsSimpleChannelInboundHandler<RpcRequest>{privatefinalMap<String,Object>services;publicRpcServerHandler(Map<String,Object>services){this.services=services;}@OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,RpcRequestreq)throwsException{Objectservice=services.get(req.getServiceName());Methodmethod=service.getClass().getMethod(req.getMethodName(),req.getParameterTypes());Objectresult=method.invoke(service,req.getArgs());ctx.writeAndFlush(newRpcResponse(req.getRequestId(),result));}}这就是 Dubbo 的迷你版原型:注册服务 + 协议编解码 + 反射调用。
九、常见坑速查
| 现象 | 原因 | 解决 |
|---|---|---|
| 消息半截/黏包 | 没做拆包 | LengthFieldBasedFrameDecoder |
| Handler 里卡顿 | 阻塞调用占住 EventLoop | 业务线程池异步处理 |
| 内存泄漏 | ByteBuf 没释放 | 用完 release / 用 SimpleChannelInboundHandler |
| 连接一直断 | 没心跳/没重连 | IdleStateHandler + 重连逻辑 |
| 线程死等 | future.sync() 卡住 | 检查事件循环死循环 |
| 客户端收不到 | 编码/解码不匹配 | 两端 pipeline 一致 |
| 高并发丢消息 | 业务处理慢 | 拆分 IO 线程和业务线程 |
本章小结
Netty 的核心心智:一个 Channel 一个 EventLoop 串行执行,Handler 链表处理消息。面试高频三件套:NIO 与多路复用原理、EventLoop 线程模型(为什么不用加锁)、粘包拆包解决方案。写代码的高频动作:加 LengthFieldBasedFrameDecoder、心跳 IdleStateHandler、业务逻辑丢线程池。
下一篇讲 Flutter——跨平台开发的性能王者。
(全文完)