Netty学习一:官网文档翻译(4.x版本)

官网地址:https://netty.io/

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引用。另外,请不要犹豫与Netty project社区联系,如果有任何错误信息、语法错误和打印错误,以及您是否有改进文档的好主意,请告诉我们。

编写一个可退出的服务器

世界上最简单的协议不是“你好,世界!”但退出。它是一个丢弃任何接收到的数据而没有任何响应的协议。

要实现丢弃协议,惟一需要做的就是忽略所有接收到的数据。让我们直接从处理程序实现开始,它处理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. 要实现丢弃协议,处理程序必须忽略接收到的消息。ByteBuf是一个引用计数的对象,必须通过release()方法显式地释放它。请记住,释放传递给处理程序的引用计数对象是处理程序的责任。通常,channelRead()处理程序方法是这样实现的:
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // Do something with msg
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
  4. exceptionCaught()事件处理程序方法在Netty由于I/O错误引发异常或处理程序实现在处理事件时由于异常引发异常时调用。在大多数情况下,应该记录捕获的异常,并在这里关闭它的关联通道,尽管该方法的实现可能不同,这取决于您希望如何处理异常情况。例如,您可能希望在关闭连接之前发送带有错误代码的响应消息。

到目前为止一切顺利。我们已经实现了丢弃服务器的前一半。现在剩下的就是编写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<SocketChannel>() { // (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。第一个通常被称为“boss”,接受传入的连接。第二个通常称为“worker”,在boss接受连接并将接受的连接注册到worker之后,处理接受连接的流量。使用多少线程以及如何将它们映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
  2. ServerBootstrap是一个设置服务器的助手类。您可以直接使用通道设置服务器。但是,请注意这是一个冗长的过程,在大多数情况下您不需要这样做。
  3. 在这里,我们指定使用NioServerSocketChannel类,该类用于实例化一个新通道以接受传入连接。
  4. 这里指定的处理程序总是由新接受的通道计算。ChannelInitializer是用于帮助用户配置新通道的特殊处理程序。您很可能希望通过添加一些处理程序(如DiscardServerHandler)来实现您的网络应用程序,来配置新通道的ChannelPipeline。随着应用程序变得复杂,您可能会向管道中添加更多的处理程序,并最终将这个匿名类提取到顶级类中。
  5. 您还可以设置特定于通道实现的参数。我们正在编写TCP/IP服务器,因此我们可以设置套接字选项,如tcpNoDelay和keepAlive。请参阅ChannelOption的apidocs和特定的ChannelConfig实现,以获得受支持的ChannelOptions的概述。
  6. 你注意到option()和childOption()了吗?option()用于接收传入连接的NioServerSocketChannel。childOption()用于父服务器通道接受的通道,在本例中是NioServerSocketChannel。
  7. 我们现在准备好出发了。剩下的就是绑定到端口并启动服务器。在这里,我们绑定到机器中所有NICs(网络接口卡)的端口8080。现在,您可以任意次数地调用bind()方法(使用不同的绑定地址)。

恭喜你!您刚刚在Netty上完成了第一个服务器。

查看接收到的数据

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

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

我们已经知道,只要接收到数据,就会调用channelRead()方法。让我们将一些代码放入DiscardServerHandler的channelRead()方法中:

@Overridechar
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命令,您将看到服务器打印接收到的内容。

丢弃服务器的完整源代码位于发行版的io.net .example. throw包中。

编写一个Echo服务器

到目前为止,我们一直在使用数据而没有任何响应。然而,服务器通常应该响应请求。让我们了解如何通过实现ECHO协议向客户机写入响应消息,在ECHO协议中,接收到的任何数据都将被发回。

与我们在前几节中实现的丢弃服务器的唯一区别是,它将接收到的数据发送回,而不是将接收到的数据打印到控制台。因此,再次修改channelRead()方法就足够了:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); // (1)
        ctx.flush(); // (2)
    }
  1. ChannelHandlerContext对象提供各种操作,使您能够触发各种I/O事件和操作。在这里,我们调用write(Object)以逐字记录接收到的消息。请注意,与丢弃示例不同,我们没有释放接收到的消息。这是因为Netty在它被写入到wire时为您释放它。
  2. ctx.write(Object)不会将消息写出来。它在内部进行缓冲,然后通过ctx.flush()将其刷新到连接。或者,您也可以调用ctx.writeAndFlush(msg)来获得简洁性。

如果您再次运行telnet命令,您将看到服务器将您发送给它的任何内容发回。

echo服务器的完整源代码位于发行版的io.net .example.echo包中。

编写时间服务器

本节要实现的协议是时间协议。它与前面的示例不同,它发送一条消息,其中包含一个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位整数,因此我们需要一个Bytebuf,它的容量至少是4字节。通过ChannelHandlerContext.alloc()获取当前ByteBufAllocator,并分配一个新的缓冲区。
  3. 像往常一样,我们编写构造的消息。等等,翻转在哪里?在NIO中发送消息之前,我们不是经常调用java.nio.ByteBuffer.flip()吗?ByteBuf没有这样的方法,因为它有两个指针;一个用于读操作,另一个用于写操作。当您向ByteBuf写入内容而阅读器索引没有更改时,写入器索引会增加。reader索引和writer索引分别表示消息开始和结束的位置。相反,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,它在操作完成时关闭通道。或者,您可以使用预定义的侦听器简化代码:
    f.addListener(ChannelFutureListener.CLOSE);
    要测试time服务器是否正常工作,可以使用UNIX rdate命令,其中<port>是您在main()方法中指定的端口号,而<host>通常是localhost。
    
    $ rdate -o <port> -p <host>

编写一个时间客户端

与丢弃和回送服务器不同,我们需要一个用于时间协议的客户机,因为人类无法将32位二进制数据转换为日历上的日期。在本节中,我们将讨论如何确保服务器正确工作,以及如何使用Netty编写客户机。

在Netty中,服务器和客户机之间最大也是唯一的区别是使用了不同的引导和通道实现。请看以下代码:

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<SocketChannel>() {
                @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组使用。但是boss worker并不用于客户端。
  3. NioSocketChannel用于创建客户端通道,而不是NioServerSocketChannel。
  4. 注意,这里我们不像在ServerBootstrap中那样使用childOption(),因为客户端SocketChannel没有父节点。
  5. 我们应该调用connect()方法,而不是bind()方法。

正如您所看到的,它与服务器端代码并没有什么不同。那么ChannelHandler实现呢?它应该从服务器接收一个32位整数,将其转换为人类可读的格式,打印转换后的时间,并关闭连接:

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。我们将在下一节讨论为什么会发生这种情况。

处理基于流的传输

套接字缓冲区的一个小警告

在基于流的传输(如TCP/IP)中,接收到的数据存储在套接字接收缓冲区中。不幸的是,基于流的传输的缓冲区不是包队列,而是字节队列。这意味着,即使您将两条消息作为两个独立的包发送,操作系统也不会将它们视为两条消息,而只是一堆字节。因此,不能保证您所读的内容与远程对等程序所写的内容完全相同。例如,假设操作系统的TCP/IP栈收到了三个包:

发送时收到三个包:

由于基于流的协议的这种一般属性,在您的应用程序中很可能以以下片段形式读取它们:

三个包被分割并合并成四个缓冲区

因此,接收部分,无论它是服务器端还是客户端,都应该将接收到的数据整理成一个或多个有意义的框架,以便应用程序逻辑能够轻松理解。对于上面的例子,接收到的数据应该像下面这样框起来:

四个缓冲区碎片整理成三个

第一个解决方案

现在让我们回到时间客户端示例。我们有同样的问题。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()。只要不阻塞很长时间,就可以执行任意(de)初始化任务。
  2. 首先,所有接收到的数据都要累积到buf中。
  3. 然后,处理程序必须检查buf是否有足够的数据,在本例中是4字节,并继续实际的业务逻辑。否则,当更多数据到达时,Netty将再次调用channelRead()方法,最终将累积所有4个字节。

第二个解决方案

尽管第一个解决方案解决了时间客户机的问题,但是修改后的处理程序看起来并没有那么清晰。想象一个更复杂的协议,它由多个字段组成,比如可变长度字段。您的ChannelInboundHandler实现将很快变得不可维护。

正如您可能已经注意到的,您可以向ChannelPipeline添加多个ChannelHandler,因此,您可以将单个ChannelHandler分割为多个模块处理程序,以降低应用程序的复杂性。例如,您可以将TimeClientHandler分成两个处理程序:

  • 时间解码器,其中处理的碎片问题,和最初的简单版本的TimeClientHandler。

幸运的是,Netty提供了一个可扩展的类,它可以帮助您编写第一个开箱即用的类:

package io.netty.example.time;

public class TimeDecoder extends ByteToMessageDecoder { // (1)
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> 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()方法,直到它没有向out添加任何内容为止。

现在我们有另一个handler要插入到ChannelPipeline中,我们应该在TimeClient中修改ChannelInitializer实现:

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

如果你是一个喜欢冒险的人,你可能会想要尝试ReplayingDecoder,这将进一步简化解码器。不过,您需要参考API参考以获得更多信息。

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

此外,Netty还提供了开箱即用的解码器,它使您能够非常容易地实现大多数协议,并帮助您避免以无法维护的整体处理程序实现告终。如需更详细的例子,请参阅以下套件:

二进制协议的io.netty.example.factorial和基于文本行的协议的io.netty.example.telnet

用POJO而不是ByteBuf交互

到目前为止,我们讨论的所有示例都使用ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进时间协议客户机和服务器示例,以使用POJO而不是ByteBuf。

在通道处理程序中使用POJO的优点是显而易见的;通过分离从处理程序中提取ByteBuf信息的代码,您的处理程序变得更加可维护和可重用。在时间客户机和服务器示例中,我们只读取一个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();
    }
}

现在我们可以修改时间解码器来生成UnixTime,而不是ByteBuf。

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> 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<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}

最后一项任务是在TimeServerHandler之前将TimeEncoder插入服务器端ChannelPipeline,这是一个简单的练习。

关闭应用程序

关闭Netty应用程序通常与关闭通过shutdown()创建的所有EventLoopGroups一样简单。它返回一个Future,当EventLoopGroup已经完全终止并且属于该group的所有通道已经关闭时,它将通知您。

总结

在这一章中,我们快速浏览了Netty,并演示了如何在Netty之上编写一个完全工作的网络应用程序。

在接下来的章节中有更多关于Netty的详细信息。我们也鼓励您回顾一下iot .net .example包中的Netty示例。

请注意,社区一直在等待您的问题和想法,以帮助您并根据您的反馈不断改进Netty及其文档。

猜你喜欢

转载自blog.csdn.net/AnY11/article/details/84869498