Netty 入门实战:从零搭建 Discard、Echo 和 Time 服务器

发布时间:2026/8/13 1:19:45
Netty 入门实战:从零搭建 Discard、Echo 和 Time 服务器 一、引言为什么需要 Netty现在我们使用通用应用程序或包进行通信。例如我们经常使用 HTTP 客户端库从 Web 服务器检索信息并通过 Web 服务调用远程过程调用。然而通用协议或其实现有时不能很好地扩展。这就像我们不使用通用 HTTP 服务器来交换大量文件电子邮件和近实时消息如财务信息和多人游戏数据。需要的是高度优化的协议实现专门用于特殊目的。例如您可能希望实现针对基于 AJAX 的聊天应用程序媒体流或大型文件传输进行了优化的 HTTP 服务器。你甚至可以设计和实施一个全新的协议这个协议是根据你的需要而定制的。另一个不可避免的情况是当您必须处理旧版专有协议以确保与旧系统的互操作性。在这种情况下重要的是我们能够快速实现该协议而不会牺牲最终应用程序的稳定性和性能。二、Netty 方案概述Netty 项目是为了快速开发可维护的高性能高可扩展性协议服务器和客户端而努力提供异步事件驱动的网络应用程序框架和工具。换句话说Netty 是一个 NIO 客户端服务器框架可以快速轻松地开发诸如协议服务器和客户端之类的网络应用程序。它大大简化了网络编程流程如 TCP 和 UDP 套接字服务器开发。“快速和容易”并不意味着由此产生的应用程序将遭受可维护性或性能问题的困扰。Netty 经过精心设计实现了许多协议如 FTPSMTPHTTP 以及各种基于二进制和基于文本的传统协议。因此Netty 成功地找到了一种方法来实现轻松的开发性能稳定性和灵活性而无需妥协。有些用户可能已经找到了声称具有相同优势的其他网络应用程序框架您可能想问问 Netty 与他们的区别。答案是它建立的哲学。Netty 旨在为您提供 API 和执行方面最舒适的体验从第一天开始。这不是有形的东西但你会意识到这个哲学将使你的生活更容易当你阅读本指南和玩 Netty 的时候。好了以上就是关于 Netty 的一个官网的初步介绍。下面进入搭建最简单的服务器的环节我这里会按照官网的思路走不过不会完全一点不差。好了我们开始三、环境准备与项目建立3.1 建立项目首先我们需要建立项目如下图所示项目名称是 NettyDemo官网建议使用 JDK 1.6 以上我这里使用的 JDK 1.8然后加入使用 Maven 导入 Netty 依赖dependencies !-- https://mvnrepository.com/artifact/io.netty/netty-all -- dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.6.Final/version /dependency /dependencies那么现在我们可以正式开始我们的项目编写了。四、实战入门三个 Netty 服务器示例4.1 编写一个 Discard 服务器世界上最简单的协议不是“hello world”而是……什么也不做的协议 Discard丢弃的意思服务端丢弃那就是啥也不做的协议呗尝试把协议理解为用户自定义功能。想要实现一个 Discard 协议那么我们唯一需要做的就是忽略所有接收到的数据。让我们从处理器实现开始它处理由 Netty 生成的 I/O 事件。首先我们创建一个 Java 包netty_beginner然后在里面创建一个类 DiscardServerHandler。类的内容如下package netty_beginner; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** Created by moon on 2017/4/5. */ public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1) Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // (2) // super.channelRead(ctx, msg); ((ByteBuf) msg).release(); // (3) // ByteBuf in (ByteBuf) msg; // try { // while (in.isReadable()) { // System.out.print((char) in.readByte()); // System.out.flush(); // } // } finally { // ReferenceCountUtil.release(msg); // } } Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // (5) // super.exceptionCaught(ctx, cause); cause.printStackTrace(); ctx.close(); } }DiscardServerHandler继承自ChannelInboundHandlerAdapter它是ChannelInboundHandler的实现。提供可以覆盖的各种事件处理程序方法。现在只需要扩展 ChannelInboundHandlerAdapter 即可而不是自己实现处理程序接口。在这里我们重写通道读取 channelRead事件处理方法。每当从客户端收到新数据时都会使用接收到的消息调用此方法。在这个例子中接收到的消息的类型是 ByteBuf。为了实现 DISCARD 协议处理程序必须忽略收到的消息。ByteBuf 是一个引用计数对象必须通过 release方法显式释放。请记住处理程序有责任释放传递给处理程序的引用计数对象。通常channelRead处理方法的实现方式如下Override public void channelRead(ChannelHandlerContext ctx, Object msg) { try { // Do something with msg } finally { ReferenceCountUtil.release(msg); } }当由于 I/O 错误或由于在处理事件时抛出异常而使得 Netty 抛出异常时exceptionCaught()事件将会被 Throwable 抛出。在大多数情况下应该记录捕获到的异常并在此关闭其关联的通道虽然这种方法的实现可以根据你想要处理的异常情况而有所不同。例如您可能希望在关闭连接之前发送带有错误代码的响应消息。到目前位置一切顺利。我们已经实现了 DISCARD 服务器的前半部分。现在剩下的是写入使用 DiscardServerHandler 启动服务器的 main方法。我们创建另外一个类DiscardServer 如下package netty_beginner;import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;/**Created by moon on 2017/4/5. */ public class DiscardServer { private int port;public DiscardServer(int port) { this.port port; }public void run() throws InterruptedException { EventLoopGroup bossGroup new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup new NioEventLoopGroup(); try { ServerBootstrap b new ServerBootstrap(); // (2) b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // (3) .childHandler(new ChannelInitializerSocketChannel() { // (4) Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new DiscardServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) // (5) .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)// Bind and start to accept incoming connections. ChannelFuture f b.bind(port).sync(); // (7) // Wait until the server socket is closed. // In this example, this does not happen, but you can do that to gracefully // shut down your server. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); }}public static void main(String[] args) throws InterruptedException { int port; if (args.length 0) { port Integer.parseInt(args[0]); } else { port 8080; } new DiscardServer(port).run(); } }NioEventLoopGroup 是一个处理 I/O 操作的多线程事件循环。Netty 为不同类型的传输提供了各种 EventLoopGroup 实现。在这个例子中我们正在实现一个服务器端应用程序因此将使用两个 NioEventLoopGroup。第一个通常称为“老板”接受传入的连接。第二个通常称为“工人”一旦老板接受连接并将接受的连接注册给工作人员就处理接受的连接的流量。使用多少线程以及它们如何映射到创建的通道取决于 EventLoopGroup 实现甚至可以通过构造函数进行配置。ServerBootstrap 是一个帮助类用于设置服务器。您可以直接使用 Channel 设置服务器。但是请注意这是一个繁琐的过程在大多数情况下您不需要这样做。在这里我们指定使用 NioServerSocketChannel 类来实例化一个新的 Channel 来接受传入的连接。可以这么理解每个客户端连接我们服务端我们都会为他们创建一个 channel那么这个 channel 对于面向对象的我们来说就是一个类我们同意对于我们接受到的连接都初始化为NioServerSocketChannel这里指定的处理程序将始终由新接受的 Channel 进行评估。ChannelInitializer 是一个特殊的处理程序旨在帮助用户配置新的 Channel。很可能您想通过添加一些处理程序如 DiscardServerHandler来配置新 Channel 的 ChannelPipeline 来实现您的网络应用程序。随着应用程序的复杂化您可能会在管道中添加更多的处理程序并将这个匿名类最终提取到顶级类中。个人感觉说白了就是想自己实现包含自己处理逻辑的 Channel但是又需要包含一些通用的原有功能咋办继承呗这就是为什么上面的 DiscardServerHandler 继承 Netty 的类您还可以设置特定于 Channel 实现的参数。我们正在编写一个 TCP / IP 服务器因此我们可以设置套接字选项如 tcpNoDelay 和 keepAlive。请参阅 ChannelOption 的 apidocs 和特定的 ChannelConfig 实现以获得有关支持的 ChannelOptions 的概述。你有没有注意到 option和 childOptionoption用于接受传入连接的 NioServerSocketChannel。childOption用于在这种情况下由父级 ServerChannel 接受的通道即 NioServerSocketChannel。我的理解就是前者用于配置我们父级 Channel后者用于配置我们自定义的子级 Channel。我们现在准备好了。剩下的是绑定到端口并启动服务器。这里我们绑定机器中所有 NIC网络接口卡的端口到 8080。您现在可以根据需要调用 bind方法多次具有不同的绑定地址。4.1.1 测试与验证恭喜到了现在这个阶段我们已经完成了。下面可以进行尝试那么在尝试之前我要说一句这个例子非常好就是一点比较费解即使我开始测试往本机 8080 端口发送内容我们根本看不出来是否成功因为我们把内容忽略了 - -。所以改一下我们的 DiscardServerHandler 改成如下打印收到的字符package netty_beginner; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.util.ReferenceCountUtil; /** Created by moon on 2017/4/5. */ public class DiscardServerHandler extends ChannelInboundHandlerAdapter { Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // super.channelRead(ctx, msg); // ((ByteBuf) msg).release(); ByteBuf in (ByteBuf) msg; try { while (in.isReadable()) { System.out.print((char) in.readByte()); System.out.flush(); } } finally { ReferenceCountUtil.release(msg); } } Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // super.exceptionCaught(ctx, cause); cause.printStackTrace(); ctx.close(); } }然后我们启动运行我们的 main 方法由于我用的 win不是 linux所以下面的操作可能有人不好使因为有的 win 默认没有启动 telnet这个自己网上百度很容易。我们打开 cmd输入 telnet进入一个新的窗口然后我们可以查看帮助输入 ?/help查看 win 下的使用方式注意 o也就是 open 是我们所需的我们使用命令open localhost 8080 如下图这说明已经连上了别跟我似的一直以为在连接 ing。。。。。那么我们现在就可以联系了由于我们的逻辑是一个字符一个字符输出所以我们输入 hello在 idea 控制台会挨个字符输出那么到这里说明我们的服务端小 demo 成功。4.2 写一个 Echo 服务器到目前为止我们一直都在假设服务端是没有响应的。然而服务器通常应该响应请求。让我们学习如何通过实现 ECHO 协议向客户端写入响应消息其中任何接收到的数据都将被发回。与前面部分实现的 Discard 服务器的唯一区别在于它将接收到的数据发回而不是将接收的数据输出到控制台。因此再次修改 channelRead方法就行了public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.write(msg); // (1) ctx.flush(); // (2) }ChannelHandlerContext 对象提供了各种可以触发各种 I/O 事件和操作的操作。在这里我们调用 writeObject来逐字写入接收到的消息。请注意我们没有像 DISCARD 示例中那样发布接收的消息。这是因为当 Netty 发布给电子邮件时Netty 会为您报告。如果再次运行 telnet 命令您将看到服务器发送回发送给它的任何内容。自行查看4.3 编写一个时间服务器本节中实现的协议是 TIME 协议。它与前面的示例不同之处在于它发送一个包含 32 位整数的消息而不接收任何请求并在发送消息后关闭连接。在此示例中您将学习如何构建和发送消息并在完成时关闭连接。因为我们不是将忽略任何接收到的数据而是在建立连接后立即发送消息这次我们不能使用 channelRead方法。相反我们应该覆盖 channelActive方法。所以我们创建一个新的类 TimeServerHandler以下是实现package netty_beginner;import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter;/**Created by moon on 2017/4/5. */ public class TimeServerHandler extends ChannelInboundHandlerAdapter { Override public void channelActive(final ChannelHandlerContext ctx) throws Exception { final ByteBuf time ctx.alloc().buffer(4); time.writeInt((int) (System.currentTimeMillis() / 1000L 2208988800L));final ChannelFuture f ctx.writeAndFlush(time); f.addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture future) throws Exception { assert f future; ctx.close(); } });}Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); } }如上所述当建立连接并准备好发送时将调用 channelActive方法。我们来写一个 32 位整数表示这个方法当前的时间。要发送一条新消息我们需要分配一个包含消息的新缓冲区。我们要写一个32位整数因此我们需要一个容量至少为4个字节的ByteBuf。通过ChannelHandlerContext.alloc获取当前的ByteBufAllocator并分配一个新的缓冲区。像往常一样我们编写构造的消息。但等等翻转的地方在哪里在NIO发送消息之前我们是否曾经调用过java.nio.ByteBuffer.flipByteBuf没有这样的方法因为它有两个指针;一个用于读操作另一个用于写操作。当你写入东西到ByteBuf中写索引增加而读索引并没有改变。读者索引和作者索引分别表示消息的开始和结束位置。相比之下如果不调用flip方法NIO缓冲区不能提供干净的方式来确定消息内容的起始和结束位置。当您忘记翻转缓冲区时您将会遇到麻烦因为你会发送不正确甚至是空的内容。在Netty中不会发生这样的错误因为我们针对不同的操作类型有不同的指针。你会发现它使你的生活更容易当你习惯了 - 一个没有翻转的生活另外要注意的是ChannelHandlerContext.write和writeAndFlush方法返回一个ChannelFuture。ChannelFuture表示尚未发生的I / O操作。这意味着任何请求的操作可能尚未执行因为所有操作在Netty中都是异步的。例如即使在发送消息之前以下代码也可能会关闭连接Channel ch ...; ch.writeAndFlush(message); ch.close();因此您需要在ChannelFuture完成之后调用close方法该方法由write方法返回并且在写入操作完成后通知其监听器。请注意close也可能不会立即关闭连接并且它返回ChannelFuture。当写请求完成时我们如何得到通知这就像将ChannelFutureListener添加到返回的ChannelFuture一样简单。在这里我们创建了一个新的匿名ChannelFutureListener当操作完成时关闭通道。或者您可以使用预定义的监听器简化代码f.addListener(ChannelFutureListener.CLOSE);要测试我们的时间服务器是否按预期工作可以使用UNIX rdate命令$ rdate -o port -p host由于我的是win我就不测试了。五、总结与后续5.1 三个示例的核心要点与适用场景本文通过三个循序渐进的 Netty 服务器示例展示了 Netty 的基本使用方式和核心概念Discard 服务器最简单的协议实现接收数据后直接丢弃。核心要点是理解 Netty 的 ChannelHandler 生命周期、ByteBuf 的引用计数管理以及如何释放资源。适用于学习 Netty 基础架构和事件处理流程。Echo 服务器在 Discard 基础上增加了响应功能将接收到的数据原样返回。核心要点是理解 Netty 的 write() 和 flush() 操作以及如何向客户端发送响应。适用于需要双向通信的简单协议实现。Time 服务器主动发送数据并在完成后关闭连接。核心要点是理解异步操作ChannelFuture、监听器ChannelFutureListener以及如何构建和发送自定义消息。适用于需要主动推送数据或一次性响应的场景。5.2 继续深入学习的方向掌握了这三个基础示例后你可以从以下几个方向继续深入 Netty 的学习编解码器Codec学习 Netty 提供的各种编解码器如 StringEncoder/StringDecoder、LengthFieldBasedFrameDecoder 等了解如何自定义编解码器来处理复杂的协议格式。心跳检测与空闲检测通过 IdleStateHandler 实现连接保活和超时检测这对于长连接应用至关重要。性能调优学习如何配置 EventLoopGroup 线程数、调整 ByteBuf 分配策略、优化 ChannelOption 参数等以提升服务器性能。SSL/TLS 支持了解如何在 Netty 中启用安全传输保护通信数据。HTTP/WebSocket 服务器基于 Netty 构建完整的 HTTP 服务器或 WebSocket 服务器这是实际项目中最常见的应用场景。源码阅读深入阅读 Netty 核心组件的源码理解其设计哲学和实现原理这对于解决复杂问题和性能优化非常有帮助。Netty 的学习曲线虽然有一定坡度但一旦掌握了其核心概念和设计模式你将能够轻松构建高性能、高可靠性的网络应用。建议从官方文档和示例代码开始逐步深入到实际项目实践中。

相关新闻