Netty4官方指南翻译

Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。

netty架构图
Netty是一个NIO客户端服务器框架,可以快速方便地开发协议服务器和客户端等网络应用程序。它大大简化和流线式网络编程,如TCP和UDP套接字服务器。

“快速和简单”并不意味着最终的应用程序会受到可维护性或性能问题的影响。Netty是根据从许多协议(如FTP、SMTP、HTTP和各种基于二进制和文本的遗留协议)的实现中获得的经验精心设计的。因此,Netty成功地找到了一种方法,在不妥协的情况下实现了易于开发、性能、稳定性和灵活性。

特性

设计

  • 用于各种传输类型的统一API -阻塞和非阻塞套接字
  • 基于灵活和可扩展的事件模型,允许明确的关注点分离
  • 高度可定制的线程模型-单线程,一个或多个线程池,如SEDA
  • 真正的无连接数据报套接字支持(从3.1开始)

简单易用

  • 文档完备的Javadoc、用户指南和示例
  • 没有额外的依赖,JDK 5 (Netty 3.x)或6 (Netty 4.x)就足够了
    一些组件,如HTTP/2,可能有更多的要求。更多信息请参阅需求页面

性能

  • 高吞吐,低延迟
  • 减少资源消耗
  • 最小化不必要的内存拷贝[零拷贝]

安全性

完整的SSL/TLS和StartTLS支持

社区

  • 尽早发布,经常发布
  • 作者自2003年以来一直在编写类似的框架,他仍然认为您的反馈非常宝贵!

前言

问题

现在我们使用通用应用程序或库来相互通信。例如,我们经常使用HTTP客户端库从web服务器检索信息,并通过web服务调用远程过程调用。然而,通用协议或其实现有时不能很好地扩展。这就像我们不使用通用的HTTP服务器来交换巨大的文件、电子邮件消息和接近实时的消息,如金融信息和多人游戏数据。需要的是一个高度优化的协议实现,专门用于特殊目的。例如,您可能希望实现一个针对基于ajax的聊天应用程序、媒体流或大文件传输进行优化的HTTP服务器。您甚至可以设计和实现一个完全适合您需要的全新协议。另一个不可避免的情况是,您必须处理遗留的专有协议,以确保与旧系统的互操作性。在这种情况下,重要的是在不牺牲现有应用程序的稳定性和性能的情况下,我们能多快地实现该协议。

解决方案

Netty项目致力于提供一个异步事件驱动的网络应用程序框架和工具,用于快速开发可维护的、高性能的和高可伸缩性的协议服务器和客户端。

换句话说,Netty是一种NIO客户端服务器框架,能够快速、轻松地开发协议服务器和客户端等网络应用程序。它极大地简化了网络编程,如TCP和UDP套接字服务器开发。

“快速和简单”并不意味着最终的应用程序将受到可维护性或性能问题的影响。Netty是根据从许多协议(如FTP、SMTP、HTTP和各种基于二进制和文本的遗留协议)的实现中学到的经验精心设计的。因此,Netty成功地找到了一种方法,在不妥协的情况下实现了易于开发、性能、稳定性和灵活性。

有些用户可能已经发现其他网络应用程序框架也声称具有同样的优势,您可能想问,是什么让Netty与它们如此不同。答案是它所建立的哲学。Netty从一开始就为您提供了最舒适的API和实现体验。这不是什么有形的东西,但当你阅读这篇指南并和使用netty时,你会意识到这一哲学会让你的生活更容易。

快速开始

本章通过简单的例子介绍了Netty的核心结构,让您快速入门。在本章的最后,您将能够立即在Netty上编写客户机和服务器。

如果您喜欢自顶向下的学习方法,您可能想从第2章,架构概述开始,然后回到这里。

开始之前

运行本章示例的最低要求只有两个;最新版本的Netty和JDK 1.6或以上。最新版本的Netty可以在项目下载页面找到。如需下载正确版本的JDK,请浏览您的JDK供应商网站。

当你阅读的时候,你可能会对这一章介绍的课程有更多的问题。如果您想了解更多有关API的信息,请参考API参考。为了方便您,本文档中的所有类名都链接到在线API引用。此外,如果有任何不正确的信息、语法错误或拼写错误,以及您有任何帮助改进文档的好主意,请不要犹豫联系Netty项目社区,并让我们知道。

写一个Discard Server

世界上最简单的协议不是“Hello, world !”但“Discard ”。这是一个丢弃任何接收到的数据而没有任何响应的协议。
要实现DISCARD协议,惟一需要做的事情就是忽略所有接收到的数据。让我们直接从处理程序实现开始,它处理Netty生成的I/O事件。
package io.netty.example.discard;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**

  • Handles a server-side channel.
    /
    public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
    // Discard the received data silently.
    ((ByteBuf) msg).release(); // (3)
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
    // Close the connection when an exception is raised.
    cause.printStackTrace();
    ctx.close();
    }
    }
    1、DiscardServerHandler 集成ChannelInboundHandlerAdapter,它是ChannelInboundHandler的一个实现。ChannelInboundHandler提供了各种可以重写的事件处理程序方法。现在,仅仅扩展ChannelInboundHandlerAdapter就足够了,而不是自己实现处理程序接口。
    2、我们在这里重写channelRead()事件处理程序方法。每当从客户端接收到新数据时,都会使用接收到的消息调用此方法。在本例中,接收到的消息类型为ByteBuf
    3、要实现DISCARD协议,处理程序必须忽略接收到的消息。ByteBuf是一个引用计数的对象,必须通过release()方法显式释放它。请记住,释放传递给处理程序的引用计数对象是处理程序的责任。通常,channelRead()处理程序方法的实现如下:
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
    // Do something with msg
    } finally {
    ReferenceCountUtil.release(msg);
    }
    }
    4、当Netty由于I/O错误引发异常或处理程序实现由于处理事件时抛出异常时,使用Throwable调用exceptionCaught()事件处理程序方法。在大多数情况下,应该记录捕获的异常,并在这里关闭其关联的通道,尽管此方法的实现可能会根据您想要处理异常情况的方式而有所不同。例如,您可能希望在关闭连接之前发送一个带有错误代码的响应消息。
    到目前为止还好。我们已经实现了DISCARD服务器的前半部分。现在剩下的工作是编写main()方法,该方法使用DiscardServerHandler启动服务器。
    package io.netty.example.discard;
    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;
    /
    *
  • Discards any incoming data.
    */
    public class DiscardServer {
    private int port;
    public DiscardServer(int port) {
    this.port = port;
    }
    public void run() throws Exception {
    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 ChannelInitializer() { // (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 Exception {
    int port = 8080;
    if (args.length > 0) {
    port = Integer.parseInt(args[0]);
    }
    new DiscardServer(port).run();
    }
    }
    1、NioEventLoopGroup是一个处理I/O操作的多线程事件循环。Netty为不同类型的传输提供了各种EventLoopGroup实现。在本例中,我们实现了一个服务器端应用程序,因此将使用两个NioEventLoopGroup。第一个,通常被称为“老板”,接受一个传入的连接。第二个通常称为“worker”,在老板接受连接并向worker注册接受的连接后,处理已接受连接的流量。使用了多少线程以及如何将它们映射到创建的 Channels,这取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
    2、ServerBootstrap是一个建立服务器的助手类。您可以直接使用Channel设置服务器。然而,请注意,这是一个乏味的过程,在大多数情况下您不需要这样做。
    3、这里,我们指定使用NioServerSocketChannel类,该类用于实例化一个新的Channel以接受传入的连接。
    4、这里指定的处理程序将始终由新接受的Channel进行计算。ChannelInitializer是一个特殊的处理程序,用于帮助用户配置新Channel。您很可能希望通过添加一些处理程序(如DiscardServerHandler)来配置新Channel的ChannelPipeline,以实现您的网络应用程序。随着应用程序变得复杂,您很可能会向管道中添加更多的处理程序,并最终将这个匿名类提取到顶级类中。
    5、您还可以设置特定于Channel实现的参数。我们正在编写一个TCP/IP服务器,因此允许设置套接字选项,如tcpNoDelay和keepAlive。请参阅ChannelOption的apidocs和特定的ChannelConfig实现,以获得有关受支持的ChannelOptions的概述。
    6、你注意到option()和childOption()了吗?option()用于接受传入连接的NioServerSocketChannel。childOption()用于父ServerChannel接受的通道,在本例中是NioSocketChannel
    7、我们现在可以出发了。剩下的就是绑定到端口并启动服务器。这里,我们绑定到机器中所有网卡(网络接口卡)的8080端口。现在可以调用bind()方法,次数不限(使用不同的绑定地址)。
    恭喜你!您刚刚在Netty上完成了第一个服务器。

查看收到的数据

现在我们已经编写了第一个服务器,我们需要测试它是否真的能工作。测试它的最简单方法是使用telnet命令。例如,您可以在命令行中输入telnet localhost 8080并输入一些内容。

但是,我们可以说服务器工作正常吗?我们不能真正知道,因为它是一个丢弃服务器。你不会得到任何回应。为了证明它确实有效,让我们修改服务器以打印它所接收到的内容。

我们已经知道,只要接收到数据,就会调用channelRead()方法。让我们将一些代码放入到DiscardServerHandler的channelRead()方法中:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
ReferenceCountUtil.release(msg); // (2)
}
}
1、这个低效的循环实际上可以简化为:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
2、或者,您可以在这里使用in.release()。
如果再次运行telnet命令,您将看到服务器打印它所接收到的内容。
discard服务器的完整源代码位于发行版的io.net .example.discard包中。

写个应答服务器

到目前为止,我们一直在消费数据而没有任何响应。然而,服务器通常是响应请求的。让我们学习如何通过实现ECHO协议向客户端编写响应消息,在ECHO协议中,任何接收到的数据都将被发送回。
与我们在前几节中实现的discard服务器的唯一区别是,它将接收到的数据发送回,而不是将接收到的数据打印到控制台。因此,修改channelRead()方法就足够了:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
1、ChannelHandlerContext对象提供各种操作,使您能够触发各种I/O事件和操作。在这里,我们调用write(Object)来逐字写入接收到的消息。请注意,我们没有像在DISCARD示例中那样释放接收到的消息。这是因为Netty在将它写到网络时为您发布了它。
2、ctx.write(Object)不会将消息写入到网络。它在内部进行缓冲,然后由ctx.flush()将其刷新到网络。或者,为了简洁起见,您可以调用ctx.writeAndFlush(msg)。
如果再次运行telnet命令,您将看到服务器返回您发送给它的任何内容。
echo服务器的完整源代码位于发行版的io.net .example.echo包中。

写个时间服务器

本节介绍的协议为TIME协议。与前面的示例不同的是,它发送一个包含32位整数的消息,而不接收任何请求,并在消息发送后关闭连接。在本例中,您将学习如何构造和发送消息,以及在完成时关闭连接。
因为我们将忽略任何接收到的数据,而是在连接建立后立即发送消息,所以这次不能使用channelRead()方法。相反,我们应该重写channelActive()方法。具体实施如下:
package io.netty.example.time;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) { // (1)
final ByteBuf time = ctx.alloc().buffer(4); // (2)
time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
final ChannelFuture f = ctx.writeAndFlush(time); // (3)
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
}); // (4)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1、如前所述,当连接建立并准备生成流量时,将调用channelActive()方法。让我们在这个方法中写一个表示当前时间的32位整数。
2、要发送一个新消息,我们需要分配一个新的缓冲区来包含该消息。我们要写一个32位整数,因此我们需要一个容量至少为4字节的ByteBuf。通过ChannelHandlerContext.alloc()获取当前的ByteBufAllocator并分配一个新的缓冲区。
3、像往常一样,我们编写构造的消息。
但是等等,flip在哪里?我们以前不是在NIO中发送消息之前调用java.nio.ByteBuffer.flip()吗?ByteBuf没有这样的方法,因为它有两个指针;一个用于读操作,另一个用于写操作。当向ByteBuf写入内容时,写入器索引增加,而读取器索引没有改变。阅读器索引和写入器索引分别表示消息开始和结束的位置。
相比之下,如果不调用flip方法,NIO缓冲区并没有提供一种清晰的方法来确定消息内容的开始和结束位置。当您忘记flip(翻转)缓冲区时,您将遇到麻烦,因为没有任何数据或不正确的数据将被发送。这样的错误不会在Netty中发生,因为不同的操作类型有不同的指针。你会发现,当你习惯它时,它会让你的生活更容易——一种没有失控的生活!
另一点需要注意的是ChannelHandlerContext.write()(和writeAndFlush())方法返回一个ChannelFuture。ChannelFuture表示尚未发生的I/O操作。这意味着,任何请求的操作可能还没有执行,因为在Netty中所有操作都是异步的。例如,以下代码可能会在消息发送之前关闭连接:
Channel ch = …;
ch.writeAndFlush(message);
ch.close();
因此,您需要在ChannelFuture完成之后调用close()方法,该方法由write()方法返回,当写操作完成时,它会通知它的侦听器。请注意,close()也可能不会立即关闭连接,它会返回一个ChannelFuture。
4、当写请求完成时,我们如何得到通知?这就像向返回的ChannelFuture添加一个ChannelFutureListener一样简单。在这里,我们创建了一个新的匿名ChannelFutureListener,它在操作完成时关闭Channel。
或者,您可以使用预定义的侦听器简化代码:
f.addListener(ChannelFutureListener.CLOSE);

要测试我们的时间服务器是否按照预期工作,可以使用UNIX rdate命令:
$ rdate -o -p
其中是main()方法中指定的端口号,通常是localhost。

写个时间客户端

与DISCARD和ECHO服务器不同,我们需要TIME协议的客户端,因为人类无法将32位二进制数据转换为日历上的日期。在本节中,我们将讨论如何确保服务器正确工作,并学习如何使用Netty编写客户机。
在Netty中,服务器和客户机之间最大也是唯一的区别是使用了不同的BootstrapChannel实现。请看下面的代码:
package io.netty.example.time;
public class TimeClient {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap(); // (1)
b.group(workerGroup); // (2)
b.channel(NioSocketChannel.class); // (3)
b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
b.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(host, port).sync(); // (5)
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
1、Bootstrap与ServerBootstrap类似,不同之处在于它用于非服务器通道,如客户端或无连接通道。
2、如果只指定一个EventLoopGroup,它将同时用作老板组和工作者组。但是,boss worker并不用于客户端。
3、NioSocketChannel用于创建客户端通道,而不是NioServerSocketChannel。
4、注意,我们这里不像在ServerBootstrap中那样使用childOption(),因为客户端SocketChannel没有父类。
5、我们应该调用connect()方法而不是bind()方法。
如您所见,它与服务器端代码并没有真正的区别。那么ChannelHandler的实现呢?它应该从服务器接收一个32位整数,将其转换为人类可读的格式,打印转换后的时间,并关闭连接:
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg; // (1)
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1、在TCP/IP中,Netty将从对等体发送的数据读入字节缓存器(bytebuf)
它看起来非常简单,与服务器端示例没有任何不同。然而,这个处理程序有时会拒绝引发IndexOutOfBoundsException。我们将在下一节讨论为什么会发生这种情况。

处理一个基于流的传输

Socket Buffer的一个小警告

在基于流的传输(如TCP/IP)中,接收到的数据存储在套接字接收缓冲区中。不幸的是,基于流的传输的缓冲区不是包队列而是字节队列。这意味着,即使您将两个消息作为两个独立的包发送,操作系统也不会将它们视为两个消息,而只是一串字节。因此,不能保证您所读的内容就是远程对等程序所写的内容。例如,让我们假设一个操作系统的TCP/IP栈已经收到三个数据包:
1、发送时收到了三个数据包
由于基于流的协议的这种一般属性,在你的应用程序中,很有可能以以下片段形式读取它们:
在这里插入图片描述
2、三个包被拆分并合并为四个缓冲区
因此,接收部分,无论它是服务器端还是客户端,都应该将接收到的数据整理成一个或多个有意义的、应用程序逻辑容易理解的帧。在上面的例子中,接收到的数据应该如下所示:
在这里插入图片描述
3、四个缓冲区整理成三个
在这里插入图片描述

第一个解决方案

现在让我们回到TIME客户机示例。我们这里也有同样的问题。一个32位整数是一个非常小的数据量,它不太可能经常被分片。但问题是,它可能会被碎片化,而且随着流量的增加,碎片化的可能性也会增加。

最简单的解决方案是创建一个内部累积缓冲区并等待,直到所有4个字节都被接收到内部缓冲区中。以下是修改后的TimeClientHandler实现,它修复了这个问题:
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
buf = ctx.alloc().buffer(4); // (1)
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
buf.release(); // (1)
buf = null;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg;
buf.writeBytes(m); // (2)
m.release();
if (buf.readableBytes() >= 4) { // (3)
long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1、ChannelHandler有两个生命周期侦听器方法:handlerAdded()和handlerRemoved()。只要不长时间阻塞,就可以执行任意(反)初始化任务。
2、首先,所有接收到的数据都应该被累积到buf中。
3、然后,处理程序必须检查buf是否有足够的数据(本例中为4字节),然后继续执行实际的业务逻辑。否则,当更多数据到达时,Netty将再次调用channelRead()方法,最终将累积所有4个字节。

第二种解法

尽管第一个解决方案已经解决了TIME客户机的问题,但是修改后的处理程序看起来并没有那么干净。想象一个更复杂的协议,它由多个字段组成,比如可变长度字段。你的ChannelInboundHandler实现将很快变得不可维护。
正如您可能已经注意到的,您可以向ChannelPipeline添加多个ChannelHandler,因此,您可以将一个单独的ChannelHandler分割为多个模块化的ChannelHandler,以降低应用程序的复杂性。例如,你可以将TimeClientHandler分成两个处理程序:

  • TimeDecoder 解决了碎片化问题
  • 最初简单的TimeClientHandler版本。
    幸运的是,Netty提供了一个可扩展的类,可以帮助您编写第一个开箱即用的类:
    package io.netty.example.time;
    public class TimeDecoder extends ByteToMessageDecoder { // (1)
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { // (2)
    if (in.readableBytes() < 4) {
    return; // (3)
    }
    out.add(in.readBytes(4)); // (4)
    }
    }
    1、ByteToMessageDecoder是ChannelInboundHandler的一个实现,它可以很容易地处理碎片问题。
    2、每当接收到新数据时,ByteToMessageDecoder使用内部维护的累积缓冲区调用decode()方法。
    3、当累积缓冲区中没有足够的数据时,Decode()可以决定不向外添加任何内容。当接收到更多数据时,ByteToMessageDecoder将再次调用decode()。
    4、如果decode()向out添加对象,则表示解码器成功解码了消息。ByteToMessageDecoder将丢弃累积缓冲区的读部分。请记住,您不需要解码多个信息。ByteToMessageDecoder将继续调用decode()方法,直到它不向外添加任何内容。
    现在我们有了另一个要插入到ChannelPipeline中的处理程序,我们应该在TimeClient中修改ChannelInitializer实现:
    b.handler(new ChannelInitializer() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
    ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
    });
    如果你是一个喜欢冒险的人,你可能会想要尝试ReplayingDecoder,它可以使解码器更加简化。不过,您需要查阅API参考资料以获得更多信息。
    public class TimeDecoder extends ReplayingDecoder {
    @Override
    protected void decode(
    ChannelHandlerContext ctx, ByteBuf in, List out) {
    out.add(in.readBytes(4));
    }
    }
    此外,Netty提供了开箱即用的解码器,使您能够非常容易地实现大多数协议,并帮助您避免最终使用不可维护的单片处理程序实现。更详细的例子,请参考以下包:
  • 对于二进制协议 io.netty.example.factorial
  • 用于基于文本行的协议 io.netty.example.telnet

用POJO而不是ByteBuf

到目前为止,我们回顾的所有示例都使用了ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进TIME协议客户机和服务器示例,使用POJO代替ByteBuf。
在ChannelHandlers中使用POJO的好处是显而易见的;通过将从ByteBuf提取信息的代码从处理程序中分离出来,处理程序变得更易于维护和可重用。在TIME客户端和服务器示例中,我们只读取一个32位整数,直接使用ByteBuf不是主要问题。然而,您会发现,在实现真实的协议时,有必要进行分离。
首先,让我们定义一个名为UnixTime的新类型。
package io.netty.example.time;
import java.util.Date;
public class UnixTime {
private final long value;
public UnixTime() {
this(System.currentTimeMillis() / 1000L + 2208988800L);
}
public UnixTime(long value) {
this.value = value;
}
public long value() {
return value;
}
@Override
public String toString() {
return new Date((value() - 2208988800L) * 1000L).toString();
}
}
我们现在可以修改TimeDecoder来生成一个UnixTime而不是一个ByteBuf。
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) {
if (in.readableBytes() < 4) {
return;
}
out.add(new UnixTime(in.readUnsignedInt()));
}
有了更新的解码器,TimeClientHandler不再使用ByteBuf了:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
UnixTime m = (UnixTime) msg;
System.out.println(m);
ctx.close();
}
更简单,更优雅,对吧?同样的技术也可以应用于服务器端。这次让我们先更新TimeServerHandler:
@Override
public void channelActive(ChannelHandlerContext ctx) {
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
f.addListener(ChannelFutureListener.CLOSE);
}
现在,唯一缺少的部分是编码器,它是ChannelOutboundHandler的实现,可以将UnixTime转换回ByteBuf。它比编写解码器简单得多,因为在编码消息时不需要处理包碎片和组装。
package io.netty.example.time;
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
UnixTime m = (UnixTime) msg;
ByteBuf encoded = ctx.alloc().buffer(4);
encoded.writeInt((int)m.value());
ctx.write(encoded, promise); // (1)
}
}
1、在这一行中有很多重要的东西
首先,我们按原样传递原始的ChannelPromise,以便当编码数据实际写入到网络时,Netty将其标记为成功或失败。
其次,我们没有调用ctx.flush()。有一个单独的处理程序方法void flush(ChannelHandlerContext ctx),它用于覆盖flush()操作。
为了进一步简化,你可以使用MessageToByteEncoder:
public class TimeEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
out.writeInt((int)msg.value());
}
}
剩下的最后一个任务是在TimeServerHandler之前将一个TimeEncoder插入到服务器端的ChannelPipeline中,这是一个简单的练习。

关闭你的应用

关闭一个Netty应用程序通常和关闭所有你通过shutdowngraceful()创建的EventLoopGroups一样简单。当EventLoopGroup被完全终止,并且属于该组的所有channel已经被关闭时,它会返回一个Future通知你。

总结

在本章中,我们快速浏览了Netty,并演示了如何在Netty上编写一个完全工作的网络应用程序。
在接下来的章节中有更多关于妮蒂的详细信息。我们还鼓励您查看io.net .example包中的Netty示例。
请注意,社区一直在等待您的问题和想法来帮助您,并根据您的反馈不断改进Netty及其文档。

##附录

Discard协议

该RFC为ARPA Internet社区指定了一个标准。在ARPA Internet上选择实现Discard 协议的主机将采用并实现该标准。
一个有用的调试和测量工具是Discard服务。discard服务只是丢弃它接收到的任何数据。

基于TCP的Discard 服务
一个Discard 服务被定义为基于应用程序的连接TCP。服务器侦听TCP端口9上的TCP连接。一次
建立连接后,将丢弃接收到的任何数据。没有响应被发送。这将一直持续到调用用户终止连接。

基于UDP的Discard 服务
另一个Discard 服务定义为基于应用程序的数据报UDP。服务器在UDP端口9上侦听UDP数据报。当一个
数据报被接收,它被丢弃。没有发送响应。

Echo协议

该RFC为ARPA Internet社区指定了一个标准。在ARPA Internet上选择实现Echo协议的主机将采用并实现该标准。
一个非常有用的调试和测量工具是echo服务。echo服务只是简单地将它接收到的任何数据发送回原始源。

基于TCP的Echo服务
一个echo服务被定义为基于TCP的连接应用程序。服务器侦听TCP端口7上的TCP连接。一次
连接建立,接收到的任何数据都被发送回。这继续,直到调用用户终止连接。

基于UDP的Echo服务
另一个echo服务定义为基于应用程序的数据报UDP。服务器侦听UDP端口7上的UDP数据报。当一个
数据报被接收,从它的数据被发送回应答数据报。

TIME协议

该RFC为ARPA Internet社区指定了一个标准。在ARPA Internet上选择实现时间协议的主机预计将采用并实现该标准。

这个协议提供了一个站点独立的、机器可读的日期和时间。时间服务将1900年1月1日午夜以来的时间以秒为单位发回源。

原因之一是,并非所有系统都有日期/时间时钟,所有系统都偶尔会发生人为或机器错误。使用时间服务器可以通过对网络上的几个独立站点进行简短的投票,快速确认或纠正系统的时间观念。

此协议可以在传输控制协议之上使用(TCP)或以上的用户数据报协议(UDP)。

当通过TCP使用时,时间服务的工作方式如下:
S:监听端口37(45八进制)
U:连接37号端口。
S:以32位二进制数发送时间。
U:收到时间。
U:关闭连接。
S:关闭连接。
服务器侦听端口37上的连接。当连接时,服务器返回一个32位的时间值,并关闭连接。 如果服务器无法确定它的时间网站,它应该拒绝连接或关闭它且不发送任何东西。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆天至尊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值