
自学八股对Netty的初步应用和理解Netty这套东西我前前后后啃了三轮才敢说自己摸到了门槛。第一轮看尚硅谷的教程弹幕里全是“双击666”我跟到一半就迷路了——不是老师讲得不好是我连BIO和NIO的底层逻辑都没吃透直接冲进Netty的世界不被绕晕才怪。第二轮开始学乖了先把网络编程的基础框架搭起来再去碰Netty这次总算能跑通第一个Demo。第三轮才真正敢说自己理解了EventLoop、ChannelHandler这些核心概念是怎么配合工作的。这篇笔记就是我把三趟学习路程浓缩下来的“八股文”。说实话面试也好工作也罢Netty的懂和不懂聊两句就能分辨出来。很多人能背出“Netty是基于NIO的异步事件驱动网络框架”但问到“一个请求到底经过了哪些组件、每条线程在干嘛、数据是怎么从一个ByteBuf变成你的业务对象的”就卡壳了。这篇文章就是想把这些卡壳的地方一一捋顺给想入门Netty、或者像我一样自学学得磕磕绊绊的人提供一个能顺着走下来的路径。1. 自学Netty之前先把这些背景补齐1.1 Netty到底解决什么问题想理解Netty先得明白没有它的时候Java写网络编程有多痛苦。Java原生的网络编程接口分三代最早的BIOBlocking IO每个连接来了之后都要开一个线程去处理线程阻塞在read操作上一旦连接多了线程数暴涨系统直接被拖垮后来JDK 1.4引入了NIONon-blocking IO用Selector来管理多路复用一个线程可以管理成千上万个连接但API设计得极其反人类Buffer、Channel、Selector三个概念绕来绕去字节读写还要手动处理半包粘包写起来又长又容易出错再后来的AIO虽然在模型上更先进但实际用下来Linux上的性能表现并不理想很多框架到最终也没有基于它去构建。Netty就是在这个背景下出现的。它把NIO那套复杂的东西封装成好用的API同时做了很多性能上的极致优化比如零拷贝、内存池、无锁串行化设计。更重要的是Netty把“网络层”和“业务层”很好地解耦了——你在Pipeline里加一个个Handler数据流经这些Handler时会被逐步处理业务代码和网络细节分开维护性好了不止一个量级。所以说Netty解决的本质问题不是“能不能通信”——Java原生也能通信——而是“如何在高并发、高吞吐、低延迟的要求下还让代码写得优雅、稳定、可维护”。像Dubbo、RocketMQ、Elasticsearch、Sentinel这些知名开源项目底层通信全都有Netty的影子。你要是搞Java服务端尤其是中间件、高并发网关这类方向绕不开它。1.2 NIO和BIO差别在哪里为什么必须理解面试的时候经常有人问“NIO和BIO的区别”但我发现很多人的回答就停在表面BIO是阻塞的NIO是非阻塞的。这当然没错但理解太浅了后面根本解释不了Netty的线程模型。我自己的理解方式是打个比方BIO就像饭店里一个服务员一对一盯着一桌客人客人不点菜read不到数据服务员就闲着站在旁边等其他人没人管想服务更多客人就得招更多服务员开线程NIO则是有一个叫Selector的大堂经理这个经理负责盯着所有桌子的状态哪桌客人招手了有数据可读他就安排一个服务员去处理其他时候服务员可以去忙别的事。关键点在于NIO中真正阻塞的地方是Selector的select()调用但它阻塞的不是某个连接而是在等“任何连接”的事件到来。这就实现了用一个线程监控海量连接的效果。这个模型叫IO多路复用Netty的线程模型就是在它基础上构建出来的。你不需要把Selector的每个方法都背下来但一定要理解“一个线程怎么管理多个连接”这个核心思路以及为什么有了这个能力就能支撑高并发。这个底层认知建立了看到Netty的EventLoopGroup就不会觉得突兀因为它的本质就是“把NIO的Selector进一步封装成了更好用的事件循环”。1.3 一定要搞懂的Reactor线程模型Netty的线程模型是面试必问也是理解Netty自认为掌握的“分水岭”。它用的是Reactor模型这个模式简单说就是一个线程负责接收连接Acceptor然后把连接注册到另外一组线程上由这组线程负责处理该连接后续的所有IO事件和业务逻辑。Netty里面对应实现是EventLoopGroup bossGroup和EventLoopGroup workerGroup。bossGroup对应Reactor模型里的Main Reactor负责accept新的连接然后把连接注册到workerGroupworkerGroup对应Sub Reactor负责在连接上处理read、write等IO事件并触发对应的ChannelHandler。我第一次看这个的时候有个疑问bossGroup只做accept会不会很闲实际上是这样的连接建立只是瞬间的事情而连接建立之后的数据收发才是长期高频的工作所以bossGroup的线程数可以很少通常1个就够workerGroup的线程数默认是CPU核心数×2这个数量也可以根据实际场景调整。更深入一层Netty保证了一个连接的所有操作都在同一个EventLoop线程上执行这就是串行无锁设计的基础。简单说就是连接A的读、写、业务处理不会同时被两个线程执行天然避免了并发竞争不需要加锁。这是一个很聪明的设计理解了这个你才真正配得上说“我了解Netty的线程模型”。2. 从零搭一个Netty服务端理解核心组件2.1 项目依赖和最小启动代码理解了背景之后就是动手了。我先从服务端写起这个流程跑通了很多疑惑会自己解开。用Maven建项目引入依赖dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.100.Final/version /dependency然后写一个最简单的服务端代码其实就这么几行public class NettyServer { public static void main(String[] args) throws InterruptedException { EventLoopGroup bossGroup new NioEventLoopGroup(1); EventLoopGroup workerGroup new NioEventLoopGroup(); try { ServerBootstrap bootstrap new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new SimpleChannelInboundHandlerString() { Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(收到客户端消息 msg); ctx.writeAndFlush(服务端已收到 msg); } }); } }); ChannelFuture future bootstrap.bind(8080).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }这段代码虽然短但背后每一个组件都值得一提。NioEventLoopGroup是把NIO的Selector封装成了EventLoop的集合每个EventLoop绑定了自己的SelectorNioServerSocketChannel是Netty对ServerSocketChannel的封装ServerBootstrap是服务端的启动引导类它做的事情可以理解成“组装并启动整个Netty服务”。我第一次跑通这段代码的时候除了“能跑”之外其实没太多感觉但后来回头看才意识到这里面的每个类都有它存在的必然逻辑没有一个是多余的。2.2 EventLoopGroup、Channel、Pipeline各司其职在学习Netty的过程中最容易让人迷糊的就是这些核心组件之间的关系。Channel是网络连接的抽象一个Channel对应一个连接。它负责底层IO操作比如connect、bind、read、write。你在channelRead0里拿到的ChannelHandlerContext就能拿到绑定的Channel。EventLoop是事件循环可以理解成“绑定在某个线程上的任务循环”。同一个EventLoop中的任务都是串行执行的。在Netty中一个EventLoop可以服务多个Channel连接但一个Channel只能被一个EventLoop同时管理。这就是前面提到的串行无锁设计的基础。ChannelPipeline是责任链模式的实现。每个Channel都有一条自己的Pipeline里面按顺序排列着一堆ChannelHandler。数据从网络进来后从Pipeline头部流入经过一个个Handler的处理数据要从业务层发出去则从尾部反向流经。我一度把Pipeline理解成“拦截器列表”后来发现还是“流水线”这个比喻更准确——数据就是流水线上的工件每个工位Handler只负责一道工序解码、业务处理、编码工序之间是串行执行的。EventLoopGroup则是EventLoop的池子你把它理解成一个线程池就行只是它里面的线程各有各的Selector。顺带一提ServerBootstrap还有两个方法特别容易忽略一个option()是针对服务端Channel的配置另一个childOption()是针对客户端连接Channel的配置。比如设置TCP的backlog、keepalive等都是通过这两个方法做的。初学者经常搞混我就踩过这个坑。2.3 客户端的写法以及连接建立过程服务端写完接着写客户端。客户端写起来和服务端对照着来理解会更深刻public class NettyClient { public static void main(String[] args) throws InterruptedException { EventLoopGroup group new NioEventLoopGroup(); try { Bootstrap bootstrap new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new SimpleChannelInboundHandlerString() { Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System.out.println(收到服务端消息 msg); } }); } }); Channel channel bootstrap.connect(127.0.0.1, 8080).sync().channel(); channel.writeAndFlush(你好Netty); channel.closeFuture().sync(); } finally { group.shutdownGracefully(); } } }要注意客户端用的引导类是Bootstrap不是ServerBootstrap而且只需要一个EventLoopGroup。原因也简单客户端不需要accept新的连接只需要发起连接并处理这条连接上的IO事件一个EventLoopGroup就够了。这个区别对应到代码上就是一行行的不同但这背后是两者职责不同。连接建立的底层过程大致是客户端发起connect - 服务端bossGroup从Selector上感知到accept事件 - 服务端创建NioSocketChannel并把它注册到workerGroup - workerGroup对这条新连接的事件开始监听和处理。这个过程如果你用Wireshark抓包看TCP三次握手再对照着Netty日志里的[id: 0x...] REGISTERED、[id: 0x...] ACTIVE这些信息会非常有画面感。3. 八股重灾区ChannelHandler和Pipeline的工作过程3.1 数据是怎么在Pipeline中流转的我对Netty感到真正“通了”的时刻是我弄懂了数据在Pipeline里的流转顺序。这个知识点也是面试官最爱追问的地方。服务端收到一条消息时数据从底层Socket读上来Netty会把它封装成一个ByteBuf然后从Pipeline的头部开始往后传递。所以你的第一个Handler一般应该是解码器——把ByteBuf转成字符串或者Java对象。解码完成之后数据继续往后传经过业务Handler处理。处理完要返回给客户端的时候从当前Handler开始往后找能处理出站数据的Handler编码器通常放在Pipeline尾部。这里有个特别容易踩的坑如果Handler是继承ChannelInboundHandlerAdapter处理完数据后如果没有显示调用ctx.fireChannelRead(msg)数据流就断在这里后面的Handler不会再收到这条消息而SimpleChannelInboundHandler处理完消息之后会自动释放内存不需要手动管也可以控制autoRelease参数。这个细节看起来小但直接影响你后面处理消息的方式而且跟内存泄漏问题直接相关后面我会专门讲。顺带说一个Netty极好的设计——ByteBuf。Java原生的ByteBuffer只有一个指针读写切换要调用flip()方法非常容易漏调Netty的ByteBuf用readerIndex和writerIndex两个指针分别管理读写位置读写不用切换还支持引用计数和内存池。我第一次用ByteBuf的时候最大的感受是“终于不用再为了flip掉头发了”。3.2 拆包粘包问题为什么面试必问拆包粘包是网络编程的基本问题Netty面试题里几乎必出现。它的根源是TCP是流式协议没有消息边界。你发两次写操作的数据底层可能合并到一次读取中粘包也可能一次写操作的数据被拆成多次读取拆包。举个例子就清楚了客户端连续发送“你好”和“世界”两条消息服务端在一次read里可能同时读到“你好世界”也可能先读到“你”再读到“好世界”。没有统一的消息边界业务层没法处理。解决方案通常有四类固定长度每个消息都是定长的不够就补齐但业务上很少能用。分隔符每条消息后面加特殊字符比如\n使用LineBasedFrameDecoder或者DelimiterBasedFrameDecoder来解码。简单但消息内容里不能出现分隔符否则就出错。长度字段消息头用固定几个字节表示消息体的长度比如用LengthFieldBasedFrameDecoder。这是最通用、最推荐的做法很多私有协议都是这么设计的。使用时要算清楚参数maxFrameLength、lengthFieldOffset、lengthFieldLength这几个值一搞错就解析不对这个我后面专门讲述。自定义协议比如HTTP、WebSocket这样的协议Netty自带对应的编解码器本质上也是长度字段或类似机制的延伸。我刚开始做的时候觉得粘包拆包问题没什么大不了的直到自己写了个简单的客户端连续发消息测试服务端收到的消息乱成一团才明白这个问题的实际威力。这也是为什么建议各位学Netty时一定要动手写测试自己复现一次拆包粘包比看十篇博客都管用。3.3 编解码器的设计思路编解码器是Netty的“翻译官”。网络传输只能传字节而你的业务需要的是对象所以必须有编解码器把两边连接起来。Netty内置了很多开箱即用的编解码器StringDecoder/StringEncoder处理字符串ObjectDecoder/ObjectEncoder处理Java对象但用Java原生序列化性能差且不安全不推荐生产使用ProtobufEncoder/ProtobufDecoder处理Protobuf还有JSON相关的编解码器。但实际项目里我们通常会自定义协议并自己写编解码器。我建议你可以拿一个比较常见的场景练手消息格式为“2字节的消息长度 2字节的消息类型 业务数据JSON”自己实现一个编解码器加上LengthFieldBasedFrameDecoder做拆包处理。这个练习能同时让你理解三个知识点一是半包粘包的处理二是自定义协议的设计三是ByteBuf的读写操作。练完之后你对Netty的感觉绝对会不一样。设计编解码器时有个重要的注意点解码器要放在入站处理的头部编码器要放在出站处理的尾部。顺序搞反了数据就乱了。另外要注意编码器是“出站”处理器所以如果你用ctx.writeAndFlush发送对象这个对象会从当前Handler位置开始沿着出站方向寻找编码器处理也就是说编码器和你的业务Handler在Pipeline中的相对位置非常重要。4. 实战用Netty集成WebSocket4.1 为什么需要WebSocketWebSocket想必大家都听说过它让服务端能主动给客户端推送消息解决了HTTP请求-响应模式没法实时推送的问题在实时聊天、游戏、行情推送、协作编辑等场景里非常常用。Netty对WebSocket的支持非常完善这也是为什么很多人用Netty做WebSocket服务。它的核心工作就是从HTTP协议升级到WebSocket协议之后就保持一条长连接双方都能主动发消息。你用Netty实现WebSocket时有个好处底层的编解码、握手这些Netty都已经实现好了。你只需要配置好Pipeline然后关注业务消息的处理。4.2 服务端集成WebSocket的步骤看一下Pipeline的配置这是WebSocket集成里最重要的部分ch.pipeline().addLast(new HttpServerCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new WebSocketServerProtocolHandler(/ws)); ch.pipeline().addLast(new MyWebSocketHandler());每个Handler的职责很清晰HttpServerCodec用来处理HTTP请求的编解码HttpObjectAggregator把HTTP请求的多个部分请求行、请求头、请求体合并成一个完整的FullHttpRequest因为WebSocket握手是基于HTTP Upgrade的所以必须有这个聚合器才能完整解析握手请求WebSocketServerProtocolHandler负责处理WebSocket握手、协议升级、以及帧的发送接收MyWebSocketHandler就是你的业务处理器了。写业务Handler时注意WebSocket消息的完整封装是WebSocketFrame常用子类包括TextWebSocketFrame文本消息、BinaryWebSocketFrame二进制消息、PingWebSocketFrame/PongWebSocketFrame心跳、CloseWebSocketFrame关闭连接。处理消息时通常判断类型再决定具体业务逻辑。WebSocketServerProtocolHandler(/ws)这个路径参数表示WebSocket连接的端点是/ws。客户端连接时用的URL就是类似ws://127.0.0.1:8080/ws这样的形式。如果你的服务端有别的HTTP服务可以设置不同的路径做区分。4.3 心跳、断线重连这些细节用WebSocket做长连接应用你会发现一个很烦人的问题连接挂在那边没消息你不知道它是不是还活着。客户端可能突然断网或者程序崩溃服务端在TCP层面可能很长时间感知不到TCP的KeepAlive默认要72分钟才探测一次。这时候就需要心跳机制。Netty里有个现成的组件IdleStateHandler。它可以在管道里触发IdleStateEvent事件分别监控读空闲、写空闲、读写全部空闲。服务端一般这样用如果超过一定时间没收到客户端的任何数据就认为这个连接可能已经死了主动关闭它释放资源。客户端则用一个定时任务定期发送Ping帧或者心跳消息保持连接活跃同时能及时发现网络异常并进行重连。我见过很多做WebSocket长连接的新手一上来就废掉心跳结果线上全是半死不活的僵尸连接服务端的连接数不断上涨最后把系统拖垮。这个问题的教训就是长连接应用心跳不是可选项而是必须项。5. 常见问题与排查实录5.1 内存泄漏问题Netty使用的时候比较容易遇到的严重问题就是内存泄漏。Netty为了性能使用了堆外内存说白了就是不受JVM堆管理的内存需要手动释放。如果你忘记释放DirectBuffer并且一直处理高并发请求很快就会把堆外内存耗尽然后报出OutOfDirectMemoryError。不过现在的Netty版本已经做了很多帮助用户规避设计。比如你使用SimpleChannelInboundHandlerchannelRead0处理完之后Netty会自动帮你释放消息的引用而如果你用ChannelInboundHandlerAdapter就必须手动调用ReferenceCountUtil.release(msg)或ctx.fireChannelRead(msg)把消息继续传递下去让后面负责释放的Handler去处理。这里有个需要主意的坑什么情况下会自动释放、什么情况下不会。其实准确说是SimpleChannelInboundHandler在channelRead方法里会在调完channelRead0之后自动释放消息ChannelInboundHandlerAdapter不会帮你释放任何东西。所以如果你用Adapter且不继续传递消息引用计数就泄漏了。Netty自己有一个检测内存泄漏的开关你可以在启动参数里加上-Dio.netty.leakDetection.levelparanoid这个开关能帮你在内存泄漏发生的第一时间打印日志定位具体是哪条Handler链上泄漏了。生产环境建议用advanced级别paranoid开销太大建议测试环境用。5.2 业务线程阻塞导致的事件循环卡死这个问题我自己踩过的坑非常深刻也属于最常见的“隐性Bug”。Netty的EventLoop线程是处理IO事件的响应速度非常重要。但很多初学者会直接在ChannelHandler的channelRead0方法里写阻塞操作最常见的就是查数据库、调远程HTTP接口等耗时操作。举个例子你在线程池里等一个第三方接口返回假设平均耗时200ms而你的EventLoop只有4个线程那这4个线程可能都在等待第三方接口返回新来的连接或者请求全部排队。系统的QPS直接掉到个位数而你还找不到原因。正确的做法是把耗时操作丢到专门的业务线程池里去执行。DefaultEventExecutorGroup可以专门给Pipeline配置一组业务线程或者你用Spring的Async、自己维护的线程池都行。关键在于EventLoop线程只负责IO处理不要在它上面做任何可能会阻塞的操作。顺带提一嘴这里也是很多面试官喜欢深入问的地方比如“如果业务处理很耗时你会怎么处理”。如果你能主动说出把业务处理丢到独立线程池、同时注意不要在Handler里阻塞、而且要考虑异步回调中ChannelHandlerContext的生命周期安全面试官就会认为你是真的写过Netty项目的不是纯背八股。5.3 连接管理、异常处理以及资源释放Netty的异常处理有一个容易忽略的地方如果你的Handler链中某个Handler抛出了异常异常的传播方向是沿着Pipeline从当前Handler向尾部传播的。如果不处理异常Netty最终会打印日志并关闭连接。如果你希望某个异常在特定位置终止并做自定义处理就要在Handler的exceptionCaught方法里捕获并且不要继续向后传播。连接资源释放也要注意每次channelInactive事件触发时要检查你的业务资源是否清理干净比如移除缓存的Channel引用、关闭跟这个连接相关的业务会话。如果你用ConcurrentHashMap维护了当前在线连接表channelInactive里忘记移除就会导致连接表越来越大内存泄漏就是这么产生的。还有一个常见问题用ctx.writeAndFlush()还是ctx.channel().writeAndFlush()两者有区别。前者从当前Handler位置开始沿着出站方向寻找下一个出站Handler处理不会经过当前Handler之前的出站Handler后者从Pipeline尾部开始反方向经过当前Handler之前的所有出站Handler。这个区别在写WebSocket的时候尤其重要。如果你想给客户端发消息而且希望走完整个出站流程用channel().writeAndFlush()如果你只是在这个Handler的处理流程中回一个包用ctx.writeAndFlush()就够了。6. 站在面试角度回看学习笔记6.1 高频面试题背后的原理学完Netty再去准备面试很多问题就有了答案背后的逻辑而不是死记硬背。比如“Netty为什么性能高”你可以从这样几个分点展开NIO多路复用让少量线程管理海量连接、无锁串行设计避免了并发竞争、零拷贝减少了数据复制次数、内存池减少了GC压力、以及FastThreadLocal等微优化。再比如“Netty的EventLoopGroup数量怎么设置”现实中并不是“越大越好”。bossGroup线程数通常设为1就够了因为它的工作只是处理accept事件workerGroup默认是CPU核心数×2但这不一定最适合你的业务场景。如果业务Handler里有大量CPU计算可以适当减少Worker线程数如果IO密集型且CPU核数不多可以适当增加。这个配置没有标准答案要根据压测数据去调。还有“Netty和Tomcat有什么区别”。这个问题挺有迷惑性。Tomcat本质上是一个实现了Servlet规范的Web容器主要处理HTTP请求Netty是一个通用的网络通信框架它能承载的服务类型要广泛得多HTTP只是它能支持的应用层协议之一。Netty更底层、更灵活但也意味着你需要自己实现更多的东西。两者不是替代关系HTTP请求经过Nginx到Spring Boot内嵌Tomcat也可以经过Netty网关做转发它们是能在同一个系统中共存的。6.2 学习路径建议如果你也想自学Netty我的建议是遵循“基础 - Demo - 实践 - 复盘”的节奏走不要一上来就想把Netty的所有特性学完。第一步是打基础理解NIO、BIO、Reactor线程模型这些底层概念。第二步是跑通最简单的服务端和客户端通信Demo用Netty把一条消息发出去、收回来。第三步是尝试在实际项目里用Netty解决一个具体问题比如做一个WebSocket消息推送服务或者改造一个文件传输功能。第四步是复盘哪些地方卡住了、为什么卡住了、怎么解决的。我自己的学习过程中最有收获的部分不是看视频和文档而是半夜调试拆包粘包问题、分析内存泄漏日志的时候。关于资料的选择我用的是尚硅谷的Netty教程入门然后在Netty官方的User Guide和JavaDoc之间来回对照。注意一个坑Netty更新速度很快网上很多博客的代码还是老版本的API已经变了编译都过不去。建议以官方文档为准博客用来理解思路不要直接复制代码。还有一个非常有用的技巧是加日志。把Netty的日志级别调成DEBUG尤其是io.netty.handler.logging.LoggingHandler这个组件加在Pipeline里可以看到每个Handler里数据的进进出出ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));配合日志观察数据流转比看任何源码都直观。结尾最后再分享一个小技巧学习Netty的时候不要把眼光只放在“怎么用”上要花点时间读一下源码。重点看这几个类NioEventLoop、ChannelPipeline、DefaultChannelHandlerContext、ByteBufAllocator。我第一次读NioEventLoop的源码时有种“原来如此”的感觉——之前很多看不懂的设计细节都串起来了。源码其实并不难难的是静下心来一行一行读进去。这也是我从自学Netty中收获最大的一点能让你对并发、网络、IO这些底层机制理解得更深一层。希望这篇笔记能让你在自学Netty的路上少走几步弯路。