Netty学习路线图 - 第四阶段:Netty基础应用
📚 Netty学习系列之四
本文是Netty学习路线的第四篇,我们将用大白话讲解Netty的基础应用,带你从理论走向实践。
写在前面
大家好!在前面三篇文章中,我们学习了Java基础、NIO编程和Netty的核心概念。但是光有理论可不行,这次我们就动手实践,看看Netty到底能干些啥!
本文我们会通过几个实用的例子,一步步带你掌握Netty的基础应用,包括:
- 搭建简单的Netty服务器和客户端
- 实现一个迷你HTTP服务器
- 开发一个WebSocket聊天室
- 设计自定义协议
- 玩转编解码器
好了,话不多说,我们直接开始动手吧!
一、搭建简单Netty服务器与客户端
1. 先来个Echo服务器
Echo服务器是最简单的网络应用之一 - 它就像个"复读机",客户端发啥它就回啥。我们就从这个入手。
首先,我们需要创建一个Maven项目,并添加Netty依赖:
<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.86.Final</version>
</dependency>
接下来,我们创建一个处理器(Handler),它负责处理客户端消息:
public class EchoServerHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {// 接收到消息后直接发回去ctx.write(msg);}@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) {// 刷新队列中的数据ctx.flush();}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {// 异常处理cause.printStackTrace();ctx.close();}
}
然后是服务器主类:
public class EchoServer {public static void main(String[] args) throws Exception {// 创建两个线程池EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 接收连接EventLoopGroup workerGroup = new NioEventLoopGroup(); // 处理数据try {// 创建服务器启动器ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) // 使用NIO.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ch.pipeline().addLast(new EchoServerHandler());}});// 绑定端口并启动ChannelFuture f = b.bind(8888).sync();System.out.println("Echo服务器已启动,端口:8888");// 等待服务器关闭f.channel().closeFuture().sync();} finally {// 优雅地关闭线程池bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
2. 再来个Echo客户端
服务器有了,我们还需要一个客户端来测试:
public class EchoClientHandler extends ChannelInboundHandlerAdapter {private final ByteBuf message;public EchoClientHandler() {// 创建一条测试消息message = Unpooled.buffer();message.writeBytes("你好,Netty!".getBytes());}@Overridepublic void channelActive(ChannelHandlerContext ctx) {// 连接建立后发送消息ctx.writeAndFlush(message.copy());}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {// 收到服务器回复ByteBuf in = (ByteBuf) msg;System.out.println("收到服务器回复: " + in.toString(CharsetUtil.UTF_8));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}
}
客户端主类:
public class EchoClient {public static void main(String[] args) throws Exception {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap b = new Bootstrap();b.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ch.pipeline().addLast(new EchoClientHandler());}});// 连接服务器ChannelFuture f = b.connect("localhost", 8888).sync();System.out.println("已连接到服务器");// 等待连接关闭f.channel().closeFuture().sync();} finally {group.shutdownGracefully();}}
}
运行这两个程序,你就能看到客户端发送的消息被服务器原样返回了!这就是一个最基础的Netty应用。
3. 关于这个例子的几点解释
可能有人会问,这个例子看起来代码挺多的,比Socket编程复杂啊?别急,我来解释一下Netty的优势:
- 异步非阻塞:虽然代码看着多,但Netty是完全异步的,可以处理成千上万的连接
- 线程模型清晰:BossGroup负责接收连接,WorkerGroup负责处理数据
- Pipeline机制:可以轻松添加多个处理器,形成处理链
- 扩展性强:这个例子很简单,但架构适用于任何复杂度的应用
二、实现HTTP服务器
接下来,我们来实现一个简单的HTTP服务器。这比Echo服务器稍微复杂一点,但Netty已经为我们提供了HTTP编解码器,省了不少事。
1. HTTP服务器处理器
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {if (msg instanceof HttpRequest) {HttpRequest request = (HttpRequest) msg;// 获取请求URIString uri = request.uri();System.out.println("收到请求: " + uri);// 构建响应内容StringBuilder content = new StringBuilder();content.append("<!DOCTYPE html>\r\n");content.append("<html>\r\n");content.append("<head><title>Netty HTTP 服务器</title></head>\r\n");content.append("<body>\r\n");content.append("<h1>你好,这是一个Netty HTTP服务器</h1>\r\n");content.append("<p>请求路径: ").append(uri).append("</p>\r\n");content.append("</body>\r\n");content.append("</html>\r\n");// 创建响应FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,Unpooled.copiedBuffer(content.toString(), CharsetUtil.UTF_8));// 设置响应头response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());// 发送响应ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}
}
2. HTTP服务器主类
public class HttpServer {public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline pipeline = ch.pipeline();// 添加HTTP编解码器pipeline.addLast(new HttpServerCodec());// 添加HTTP对象聚合器pipeline.addLast(new HttpObjectAggregator(65536));// 添加我们的处理器pipeline.addLast(new HttpServerHandler());}});ChannelFuture f = b.bind(8080).sync();System.out.println("HTTP服务器已启动,访问地址: http://localhost:8080");f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
启动这个服务器后,打开浏览器访问 http://localhost:8080 ,你就能看到一个网页了!是不是很神奇?
3. 关键点解析
看起来我们只写了几十行代码,就实现了一个HTTP服务器,这是怎么做到的?关键在于Netty的几个特殊处理器:
- HttpServerCodec:HTTP编解码器,负责将字节流转换为HTTP请求/响应
- HttpObjectAggregator:HTTP消息聚合器,将HTTP消息的多个部分合并成一个完整的HTTP请求或响应
- SimpleChannelInboundHandler:简化的入站处理器,帮我们自动释放资源
通过这些处理器的组合,我们可以轻松处理HTTP请求,而不用关心底层的编解码细节。
三、WebSocket应用开发
接下来,我们实现一个简单的WebSocket聊天室。WebSocket是HTML5的一个新特性,允许浏览器和服务器建立持久连接,非常适合聊天应用。
1. WebSocket处理器
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {// 用于保存所有WebSocket连接private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);@Overrideprotected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {if (msg instanceof HttpRequest) {// 处理HTTP请求,WebSocket握手HttpRequest request = (HttpRequest) msg;// 如果是WebSocket请求,进行升级if (isWebSocketRequest(request)) {// 将HTTP升级为WebSocketctx.pipeline().replace(this, "websocketHandler", new WebSocketFrameHandler());// 执行握手handleHandshake(ctx, request);} else {// 如果不是WebSocket请求,返回HTML页面sendHttpResponse(ctx, request, getWebSocketHtml());}} else {// 如果不是HTTP请求,传递给下一个处理器ctx.fireChannelRead(msg);}}private boolean isWebSocketRequest(HttpRequest req) {return req.headers().contains(HttpHeaderNames.UPGRADE, "websocket", true);}private void handleHandshake(ChannelHandlerContext ctx, HttpRequest req) {WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://" + req.headers().get(HttpHeaderNames.HOST) + "/websocket", null, false);WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);if (handshaker == null) {WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());} else {handshaker.handshake(ctx.channel(), req);}}private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, String html) {FullHttpResponse res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));res.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE);}private String getWebSocketHtml() {return "<!DOCTYPE html>\r\n" +"<html>\r\n" +"<head>\r\n" +" <title>WebSocket聊天室</title>\r\n" +" <script type=\"text/javascript\">\r\n" +" var socket;\r\n" +" if (window.WebSocket) {\r\n" +" socket = new WebSocket(\"ws://\" + window.location.host + \"/websocket\");\r\n" +" socket.onmessage = function(event) {\r\n" +" var chat = document.getElementById('chat');\r\n" +" chat.innerHTML += event.data + '<br>';\r\n" +" };\r\n" +" socket.onopen = function(event) {\r\n" +" console.log(\"WebSocket已连接\");\r\n" +" };\r\n" +" socket.onclose = function(event) {\r\n" +" console.log(\"WebSocket已关闭\");\r\n" +" };\r\n" +" } else {\r\n" +" alert(\"浏览器不支持WebSocket!\");\r\n" +" }\r\n" +" \r\n" +" function send(message) {\r\n" +" if (!socket) return;\r\n" +" if (socket.readyState == WebSocket.OPEN) {\r\n" +" socket.send(message);\r\n" +" }\r\n" +" }\r\n" +" </script>\r\n" +"</head>\r\n" +"<body>\r\n" +" <h1>Netty WebSocket聊天室</h1>\r\n" +" <div id=\"chat\" style=\"height:300px;overflow:auto;border:1px solid #ccc;padding:10px;\"></div>\r\n" +" <input type=\"text\" id=\"message\" style=\"width:300px\">\r\n" +" <button onclick=\"send(document.getElementById('message').value)\">发送</button>\r\n" +"</body>\r\n" +"</html>";}
}
2. WebSocket帧处理器
public class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);@Overridepublic void channelActive(ChannelHandlerContext ctx) {// 有新连接加入channels.add(ctx.channel());System.out.println("客户端加入: " + ctx.channel().remoteAddress());}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {if (frame instanceof TextWebSocketFrame) {// 处理文本帧String message = ((TextWebSocketFrame) frame).text();System.out.println("收到消息: " + message);// 广播消息给所有连接String response = "用户" + ctx.channel().remoteAddress() + " 说: " + message;channels.writeAndFlush(new TextWebSocketFrame(response));} else {// 不支持的帧类型System.out.println("不支持的帧类型: " + frame.getClass().getName());}}@Overridepublic void channelInactive(ChannelHandlerContext ctx) {// 连接断开channels.remove(ctx.channel());System.out.println("客户端断开: " + ctx.channel().remoteAddress());}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}
}
3. WebSocket服务器主类
public class WebSocketServer {public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline p = ch.pipeline();p.addLast(new HttpServerCodec());p.addLast(new HttpObjectAggregator(65536));p.addLast(new WebSocketServerHandler());}});ChannelFuture f = b.bind(8080).sync();System.out.println("WebSocket服务器已启动,访问地址: http://localhost:8080");f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
启动服务器后,打开浏览器访问 http://localhost:8080 ,你就能看到一个简单的聊天室页面。打开多个浏览器窗口,就可以实现多人聊天了!
4. WebSocket关键点解析
WebSocket应用比HTTP服务器稍微复杂一些,关键点在于:
- HTTP升级为WebSocket:客户端首先发起HTTP请求,然后协议升级为WebSocket
- 握手过程:服务器需要返回特定的HTTP响应完成握手
- 不同类型的帧:WebSocket有多种帧类型,我们这里只处理了文本帧
- 广播消息:使用ChannelGroup可以轻松地将消息广播给所有连接的客户端
四、自定义协议开发
在实际应用中,我们经常需要设计自己的通信协议。下面我们来实现一个简单的自定义协议。
1. 协议设计
我们设计一个简单的消息协议,格式如下:
+--------+------+--------+----------------+
| 魔数 | 版本 | 消息长度 | 消息内容 |
| 4字节 | 1字节 | 4字节 | 变长数据 |
+--------+------+--------+----------------+
- 魔数:固定值0xCAFEBABE,用于快速识别协议
- 版本:协议版本号,便于后续扩展
- 消息长度:消息内容的长度,单位为字节
- 消息内容:实际传输的数据,格式为JSON字符串
2. 消息对象定义
public class MyMessage {private String content;private int type;private long timestamp;// getter、setter和构造方法省略@Overridepublic String toString() {return "MyMessage{" +"content='" + content + '\'' +", type=" + type +", timestamp=" + timestamp +'}';}
}
3. 编码器实现
public class MyMessageEncoder extends MessageToByteEncoder<MyMessage> {@Overrideprotected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception {// 1. 写入魔数out.writeInt(0xCAFEBABE);// 2. 写入版本号out.writeByte(1);// 3. 将消息对象转为JSONString json = new ObjectMapper().writeValueAsString(msg);byte[] content = json.getBytes(CharsetUtil.UTF_8);// 4. 写入消息长度out.writeInt(content.length);// 5. 写入消息内容out.writeBytes(content);}
}
4. 解码器实现
public class MyMessageDecoder extends ByteToMessageDecoder {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {// 1. 检查是否有足够的字节if (in.readableBytes() < 9) { // 魔数(4) + 版本(1) + 长度(4) = 9字节return;}// 2. 标记当前读取位置in.markReaderIndex();// 3. 检查魔数int magic = in.readInt();if (magic != 0xCAFEBABE) {in.resetReaderIndex();throw new CorruptedFrameException("Invalid magic number: " + magic);}// 4. 读取版本号byte version = in.readByte();if (version != 1) {in.resetReaderIndex();throw new CorruptedFrameException("Invalid version: " + version);}// 5. 读取消息长度int length = in.readInt();if (length < 0 || length > 65535) {in.resetReaderIndex();throw new CorruptedFrameException("Invalid length: " + length);}// 6. 检查是否有足够的字节if (in.readableBytes() < length) {in.resetReaderIndex();return;}// 7. 读取消息内容byte[] content = new byte[length];in.readBytes(content);// 8. 将JSON转换为对象MyMessage message = new ObjectMapper().readValue(content, MyMessage.class);// 9. 将对象添加到输出列表out.add(message);}
}
5. 使用自定义协议的服务器
public class CustomProtocolServer {public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline p = ch.pipeline();// 添加编解码器p.addLast(new MyMessageDecoder());p.addLast(new MyMessageEncoder());// 添加业务处理器p.addLast(new SimpleChannelInboundHandler<MyMessage>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, MyMessage msg) {System.out.println("收到消息: " + msg);// 回复一条消息MyMessage response = new MyMessage();response.setContent("已收到你的消息: " + msg.getContent());response.setType(200);response.setTimestamp(System.currentTimeMillis());ctx.writeAndFlush(response);}});}});ChannelFuture f = b.bind(8888).sync();System.out.println("自定义协议服务器已启动,端口: 8888");f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
6. 自定义协议的关键点
设计自定义协议时,需要注意以下几点:
- 协议格式明确:需要明确定义消息的格式,包括头部、长度、内容等
- 魔数校验:使用魔数可以快速识别协议,过滤掉非法请求
- 版本控制:协议需要有版本号,便于后续升级
- 长度字段:必须有长度字段,便于拆包
- 编解码器分离:将编码和解码逻辑分开,便于维护
五、编解码器实践
最后,我们来看几种常见的编解码器实现。
1. 基于长度的拆包器
TCP是流式协议,数据可能会被分成多个包传输,也可能多个消息会合并成一个包。为了正确地拆分消息,我们需要使用拆包器。
public class LengthBasedServer {public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline p = ch.pipeline();// 添加长度字段解码器,格式为: 长度(4字节) + 内容p.addLast(new LengthFieldBasedFrameDecoder(65535, 0, 4, 0, 4));// 添加长度字段编码器p.addLast(new LengthFieldPrepender(4));// 添加字符串编解码器p.addLast(new StringDecoder(CharsetUtil.UTF_8));p.addLast(new StringEncoder(CharsetUtil.UTF_8));// 业务处理器p.addLast(new SimpleChannelInboundHandler<String>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println("收到消息: " + msg);ctx.writeAndFlush("回复: " + msg);}});}});ChannelFuture f = b.bind(8888).sync();System.out.println("拆包示例服务器已启动,端口: 8888");f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
2. 基于分隔符的拆包器
有时候我们使用特定的字符作为消息分隔符,比如换行符。
public class DelimiterBasedServer {public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline p = ch.pipeline();// 添加行分隔符解码器p.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));// 添加字符串编解码器p.addLast(new StringDecoder(CharsetUtil.UTF_8));p.addLast(new StringEncoder(CharsetUtil.UTF_8));// 业务处理器p.addLast(new SimpleChannelInboundHandler<String>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) {System.out.println("收到消息: " + msg);ctx.writeAndFlush("回复: " + msg + "\r\n");}});}});ChannelFuture f = b.bind(8888).sync();System.out.println("分隔符示例服务器已启动,端口: 8888");f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
3. 对象序列化编解码器
如果我们想直接传输Java对象,可以使用对象序列化编解码器。
// 可序列化的消息对象
public class User implements Serializable {private static final long serialVersionUID = 1L;private String name;private int age;// getter、setter和构造方法省略@Overridepublic String toString() {return "User{name='" + name + "', age=" + age + "}";}
}
public class SerializationServer {public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ChannelPipeline p = ch.pipeline();// 添加对象编解码器p.addLast(new ObjectEncoder());p.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));// 业务处理器p.addLast(new SimpleChannelInboundHandler<User>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, User user) {System.out.println("收到用户: " + user);// 回复一个用户对象User response = new User();response.setName("服务器用户");response.setAge(20);ctx.writeAndFlush(response);}});}});ChannelFuture f = b.bind(8888).sync();System.out.println("序列化示例服务器已启动,端口: 8888");f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
4. 编解码器的选择
- LengthFieldBasedFrameDecoder:适合自定义协议,灵活性强
- DelimiterBasedFrameDecoder:适合基于文本的协议,如HTTP、SMTP等
- ObjectEncoder/ObjectDecoder:适合Java对象传输,但不适合跨语言通信
- JsonEncoder/JsonDecoder:适合跨语言通信
- ProtobufEncoder/ProtobufDecoder:高性能、跨语言通信的首选
总结与实践建议
通过本文,我们学习了Netty的四种基础应用:
- 简单的Echo服务器与客户端
- HTTP服务器
- WebSocket聊天室
- 自定义协议
在实际开发中,记住以下几点:
- 选择合适的编解码器:根据需求选择合适的编解码器,避免重复造轮子
- 正确处理粘包/拆包:TCP是流式的,必须正确处理消息边界
- 异常处理要完善:网络编程中异常情况很多,一定要做好异常处理
- 资源要及时释放:关闭连接、释放ByteBuf等资源
- 避免阻塞EventLoop:耗时操作放到单独的线程池中执行
Netty的基础应用非常广泛,掌握了这些基础应用,你就能开发出各种高性能的网络应用了。
在下一篇文章中,我们将探讨Netty的高级特性,包括心跳检测、空闲连接处理、内存管理等内容,敬请期待!
作者:by.G
如需转载,请注明出处