netty 4.x用户使用指南

旧时模样 提交于 2020-01-16 23:33:28

引言

问题
  现在我们使用通用的应用程序或库来相互通信。例如,我们经常使用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供应商的网站。
 
编写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、要实现丢弃协议,处理程序必须忽略接收到的消息。ByteBuf是一个引用计数对象,必须通过release()方法显式地释放它。请记住,处理程序有责任释放传递给处理程序的任何引用计数对象。通常,channelRead()处理程序方法是这样实现的:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        // Do something with msg
    } finally {
        ReferenceCountUtil.release(msg);
    }
}
  4、异常捕获()事件处理程序方法在Netty由于I/O错误或处理程序实现由于在处理事件时抛出异常而引发异常时被一次性调用。在大多数情况下,应该对捕获的异常进行日志记录,并关闭其关联的通道,尽管此方法的实现可能因您希望如何处理异常情况而有所不同。例如,您可能希望在关闭连接之前发送带有错误代码的响应消息。
  到目前为止还不错。我们已经实现了废弃服务器的前一半。现在剩下的是编写main()方法,该方法使用DiscardServerHandler启动服务器。
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) 该对象相当于Socket中使用一个线程专门用户监听一个socket端口,然后将监听到的socket对象传入另一对象
        EventLoopGroup workerGroup = new NioEventLoopGroup();// 该对象相当于Socket中对于每个socket连接都都单独开辟了一个线程进行数据解析出处理
        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;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
    }
}

 注:1、NioEventLoopGroup是一个处理I/O操作的多线程事件循环。Netty为不同类型的传输提供了各种EventLoopGroup实现。在本例中,我们正在实现一个服务器端应用程序,因此将使用两个NioEventLoopGroup。第一个,通常被称为“老板”,接受进入的连接。第二个通常称为“worker”,在boss接受连接并将接受的连接注册给worker时,它将处理已接受连接的流量。使用多少线程以及如何将它们映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。

        2、ServerBootstrap是一个设置服务器的助手类。您可以直接使用Channel设置服务器。但是,请注意,这是一个冗长的过程,在大多数情况下不需要这样做。
        3、在这里,我们指定使用NioServerSocketChannel类,该类用于实例化一个新通道以接受传入连接。
        4、这里指定的处理程序将总是由新接受的通道进行计算。ChannelInitializer是一个用于帮助用户配置新通道的特殊处理程序。您很可能希望通过添加一些处理程序(如DiscardServerHandler)来实现网络应用程序来配置新通道的ChannelPipeline。随着应用程序变得复杂,您可能会向管道中添加更多的处理程序,并最终将这个匿名类提取到顶级类中。
        5、您还可以设置特定于通道实现的参数。我们正在编写一个TCP/IP服务器,所以我们可以设置套接字选项,如tcpNoDelay和keepAlive。请参考ChannelOption的apidocs以及特定的ChannelConfig实现,以获得关于支持的ChannelOptions的概述。
        6、你注意到option()和childOption()吗?option()用于接受传入连接的NioServerSocketChannel。childOption()是父ServerChannel接受的通道,在本例中是NioServerSocketChannel
        7、我们准备好了。剩下的就是绑定到端口并启动服务器。在这里,我们绑定到机器中所有NICs(网络接口卡)的端口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.net .util. charsetutil.us_ascii))
        2、或者,您可以在这里执行in.release()。
 
问题编写ECHO服务
  到目前为止,我们一直在使用数据,而没有做出任何响应。然而,服务器通常应该响应请求。让我们学习如何通过实现ECHO协议向客户端写入响应消息,其中所有接收到的数据都被发回。
  与我们在前几节中实现的Discards服务器的唯一区别是,它将接收到的数据发送回服务器,而不是将接收到的数据打印到控制台。因此,再次修改channelRead()方法就足够了: 
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ctx.write(msg); // (1)
    ctx.flush(); // (2)
}
注: 1、ChannelHandlerContext对象提供各种操作,使您能够触发各种I/O事件和操作。在这里,我们调用write(Object)以逐字记录接收到的消息。请注意,我们没有释放接收到的消息,这与我们在丢弃示例中所做的不同。这是因为当它被写在网上时,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位整数,因此我们需要一个ByteBuf,它的容量至少是4字节。通过ChannelHandlerContext.alloc()获取当前的ByteBufAllocator,并分配一个新的缓冲区。

        3、像往常一样,我们编写构造好的消息。

             但是等等,抛硬币在哪里?在使用NIO发送消息之前,我们不是曾经调用java.nio.ByteBuffer.flip()吗?ByteBuf没有这样的方法,因为它有两个指针;一个用于读操作,另一个用于写操作。当您向ByteBuf写入内容时,写入器索引会增加,而读取器索引不会改变。阅读器索引和写入器索引分别表示消息开始和结束的位置。

              相反,NIO缓冲区没有提供一种干净的方法来确定消息内容在哪里开始和结束,而不调用flip方法。当您忘记翻转缓冲区时,您将遇到麻烦,因为不会发送任何或不正确的数据。在Netty中不会发生这样的错误,因为对于不同的操作类型,我们有不同的指针。当你习惯了它,你会发现它让你的生活变得更容易——一个没有翻转的生活!

              要注意的另一点是ChannelHandlerContext.write()(和writeAndFlush())方法返回ChannelFuture。ChannelFuture表示尚未发生的I/O操作。这意味着,由于Netty中的所有操作都是异步的,因此可能还没有执行任何请求的操作。例如,以下代码可能会在发送消息之前关闭连接:

Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
  因此,您需要在ChannelFuture完成之后调用close()方法,这个方法由write()方法返回,当写操作完成时,它会通知它的侦听器。请注意,close()也可能不会立即关闭连接,它将返回ChannelFuture。
        4、那么,当写请求完成时,我们如何得到通知?这就像在返回的通道未来中添加一个ChannelFutureListener一样简单。在这里,我们创建了一个新的匿名通道futurelistener,它在操作完成时关闭通道。
              或者,您可以使用预定义的侦听器简化代码:
f.addListener(ChannelFutureListener.CLOSE);

  要测试我们的时间服务器是否按预期工作,您可以使用UNIX rdate命令:

$ rdate -o <port> -p <host>
  其中<port>是main()方法中指定的端口号,<host>通常是本地主机。
 
编写一个时间客户端
  与Discard服务器和ECHO服务器不同,我们需要一个时间协议客户机,因为人不能将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组和工作者组使用。但是,老板员工并不用于客户端
    3、与NioServerSocketChannel不同,NioSocketChannel用于创建客户端通道。
    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。我们将在下一节讨论为什么会发生这种情况。
 
处理基于流的传输
  套接字缓冲区的一个小警告
  在基于流的传输(如TCP/IP)中,接收到的数据存储在套接字接收缓冲区中。不幸的是,基于流的传输的缓冲区不是包的队列,而是字节的队列。这意味着,即使您将两个消息作为两个独立的信息包发送,操作系统也不会将它们视为两个消息,而只是一堆字节。因此,不能保证您所阅读的内容就是您的远程对等者所写的内容。例如,假设一个操作系统的
TCP/IP栈接收了三个包:
   由于基于流的协议的一般特性,在您的应用程序中很有可能以以下片段形式阅读它们:
  因此,无论接收部分是服务器端还是客户端,都应该将接收到的数据碎片整理成应用程序逻辑可以容易理解的一个或多个有意义的帧,应用程序逻辑可以很容易地理解这些帧。对于上面的例子,接收到的数据应该像下面这样构造:
第一个解决方案:
  现在让我们回到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()。您可以执行任意(de)初始化任务,只要它不会长时间阻塞。
    2、首先,应将所有收到的数据累积到buf。
    3、然后,处理程序必须检查buf是否有足够的数据,在此示例中为4个字节,然后继续执行实际的业务逻辑。否则,channelRead()当更多数据到达时,Netty将再次调用该方法,最终将累计所有4个字节。
 
第二种解决方案
  虽然第一个解决方案已经解决了TIME客户端的问题,但修改后的处理程序看起来并不干净。想象一个更复杂的协议,它由多个字段组成,例如可变长度字段。您的ChannelInboundHandler实施将很快变得无法维护。
  您可能已经注意到,您可以ChannelHandler为a 添加多个ChannelPipeline,因此,您可以将一个单片拆分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<Object> out) { // (2)
        if (in.readableBytes() < 4) {
            return; // (3)
        }
        out.add(in.readBytes(4)); // (4)
    }
}
注:1、ByteToMessageDecoder是一种实现ChannelInboundHandler,可以很容易地处理碎片问题。
       2、ByteToMessageDecoderdecode()每当收到新数据时,都会使用内部维护的累积缓冲区调用该方法。
       3、decode()可以决定不向累积缓冲区中没有足够数据的地方添加任何内容。当接收到更多数据时,ByteToMessageDecoder将再次调用decode()。
      4、如果decode()向out添加一个对象,则表示解码器成功解码一条消息。ByteToMessageDecoder将丢弃累积缓冲区的读取部分。请记住,您不需要解码多个消息。ByteToMessageDecoder将继续调用decode()方法,直到它没有向out添加任何内容为止。
  现在我们有另一个处理程序插入到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提供了开箱即用的解码器,它使您能够非常容易地实现大多数协议,并帮助您避免最终出现难以维护的单块处理程序实现。更详细的例子请参考以下包:   
用POJO代替ByteBuf
  到目前为止,我们所审查的所有示例都使用了ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进TIME协议客户端和服务器示例以使用POJO而不是ByteBuf
在你的ChannelHandler中使用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<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应用程序通常与关闭通过shutdowndowns()创建的所有EventLoopGroups一样简单。当EventLoopGroup完全终止并且属于该组的所有通道都已关闭时通知您,它返回一个Future
 
概要
  在本章中,我们快速浏览了Netty,并演示了如何在Netty上编写完整的网络应用程序。
  在接下来的章节中有关于Netty的更多详细信息。我们还鼓励您查看io.netty.example包中的Netty示例。
  另请注意,社区始终在等待您的问题和想法,以帮助您并根据您的反馈不断改进Netty及其文档。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!