Netty 4.X的用户指南(翻译)

前言

问题

目前,我们都是使用通用的应用或者程序库(这里可以理解为框架)来进行通信。例如,我们经常使用http-client这个框架从web服务端中检索数据以及远程过程调用web服务(通过RPC调用web服务)。然而一个通用协议以及它的实现并不能完全满足我们的需要。就像我们不会去使用通用的HTTP服务端去做传输大文件、电子邮件和像金融信息、多人游戏之类的实时通讯。而这时候就需要一个高度优化的协议实现来满足我们特定的需求了。例如,你可能需要实现一个针对AJAX的聊天应用程序、媒体流或者大型文件传输进行优化的HTTP服务端。你甚至可以设计和实现一个完全根据你的需要定制的新协议。另外一种不可避免的情况就是,你不得不为了兼容旧系统而改造传统的协议。在这种情况下多久能够实现一个应用程序的稳定性和性能的协议才是我们最关心的。

解决方案

Netty是一个异步事件驱动的网络应用程序框架以及用于快速开发可维护的高性能高可伸缩性协议服务器和客户端的工具。
换句话说,Netty是一个NIO客户端服务器框架,它能够快速而简单地开发网络应用程序,比如协议服务端和客户端。它极大地简化了网络编程,如TCP和UDP套接字服务端的开发。
“快速而简单”并不意味着最终的应用程序会受到可维护性或性能问题的影响。Netty已经精心设计了许多协议的实现,包括FTP、SMTP、HTTP和各种二进制和基于文本的传统协议。因此,Netty成功地找到了一种方法,在没有妥协的情况下,实现了发展、表现、稳定和灵活性。
一些用户可能已经发现了其他声称拥有同样的优势网的络应用程序框架,你可能想问是什么让Netty与他们如此不同。
答案是它的设计理念。Netty的设计是为了给你提供最舒适的体验,无论是从API还是从第一次的实现。这不是什么有形的东西,但你会意识到,当你读了这本指南,使用Netty的时候,这种设计理念会让你的开发变得更容易。


入门指南

这一章围绕着Netty的核心结构,以简单的例子让你快速上手。当你学习完这章之后就可以使用Netty实现一个客户端和服务端程序了。
如果你更喜欢自上而下的学习方法,可以从第2章-架构概述开始学习,然后学习这一章。

学习之前

运行本章的示例需要满足以下两个要求:Netty版本为4.X(原文为最新版本的Netty,但是实际上这个指南是针对Netty4的)和版本为1.6或以上的JDK。4.X版本(原文为最新版本)的Netty可以在项目下载页面中找到。请到JDK官网上下载正确版本的JDK。
当你阅读本文时,你可能会对本章中所介绍的类有更多的疑问。当你想了解更多关于它们的信息时,请参考API文档。本文档中的所有类名都与在线API文档进行关联,以方便你的查阅。另外,如果有错误的信息、语法错误和拼写错误,请随时与Netty项目社区联系,如果你有改进文档的好主意,请告诉我们。

编写一个丢弃服务端

世界上最简单的协议不是“Hello, World!”而是丢弃协议。它是一种在没有任何响应的情况下丢弃任何接收到的数据的协议。
要实现丢弃协议,唯一需要做的就是忽略所有接收到的数据。让我们直接从处理程序的实现开始,它处理由Netty生成的输入/输出事件。

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继承了ChannelInboundHandler接口的实现类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);
    }
}
  1. 当Netty抛出一个异常的时候,exceptionCaught() 这个事件处理方法就会被调用。通常,Netty抛出的异常是由于I/O错误或者是处理器的实现在处理事件的时候抛出的异常引起的。在大多数情况下,捕获的异常应该被记录下来,并且它的抛出这个异常通道应该在捕获到异常的时候进行关闭。尽管这个方法的实现会因为你对这个异常的处理而不相同。例如,在关闭连接之前,你可能想要发送一个带有错误代码的响应消息。

目前为止一切都很顺利。我们已经实现了丢弃服务端的前半部分。现在剩下的是编写一个启动使用DiscardServerHandler的服务端的main()方法。

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;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
    }
}
  1. NioEventLoopGroup 是一个处理I/O操作的多线程的事件循环组(概念可以参考JDK的ThreadGroup)。Netty为不同类型的数据传输提供了各种各样的EventLoopGroup接口实现类。在这个例子中,我们正在实现一个客户端应用程序,因此使用了两个NioEventLoopGroup对象。第一个NioEventLoopGroup对象通常被称为“老板”,接受一个又一个连接。第二个则通常被称为“工人”,老板接受了连接并将连接分配(原文是register)给工人,则工人开始处理这个连接。使用了多少个线程,以及如何将它们映射到创建好的通道,这取决于EventLoopGroup实现,甚至可以通过构造器进行配置。(这个模型可以参考Linux Socket的I/O复用模型以及Java的NIO
  2. ServerBootstrap是一个帮助建立服务的类。你也可以直接使用一个通道来建立服务。但是,请注意,这是一个乏味的过程,而且在大多数情况下你不需要这样做。
  3. 在这里,我们指定使用NioServerSocketChannel类,该类用于实例化一个接收传入的连接的新通道。
  4. 这里指定的处理程序将始终由一个新接受的通道对象进行处理。ChannelInitializer是特殊的处理器,它用于帮助用户配置一个新的通道。它常用于以下情况,你需要配置新通道的ChannelPipeline(通道管道)用于添加一些处理器(例如DiscardServerHandler这种自定义的处理器)到自己的网络应用程序。当应用程序变得复杂时,很可能你将向管道中添加更多的处理程序,并最终将这个匿名类提取到顶级类中。
  5. 你还可以设置特定于通道实现的参数。我们正在编写一个TCP/IP服务端,因此我们可以设置诸如tcpNoDelay 和keepAlive之类的套接字配置。请参考ChannelOption 的API和特定的ChannelConfig实现类,以获得支持ChannelOption的概述。
  6. 你注意到了option()和childOption()这两个方法吗?option()是用于接受传入连接的NioServerSocketChannel,而childOption()从父ServerChannel中接受连接的通道,而在例子中父ServerChannel就是NioServerSocketChannel。
  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命令,你将看到服务打印接收到的数据。
丢弃服务器的完整源代码位于io.netty.example.discard 包中。

编写一个回显服务端

到目前为止,我们一直在接收数据而没有回应。然而,服务端通常应该响应请求。接下来让我们学习如何通过实现ECHO协议将响应消息返回给客户端,其中ECHO协议是一个将任何接收到的数据都会被发送回去的协议。
回显服务与我们在前几节中实现的丢弃服务器的唯一区别是,它将接收到的数据发送回去,而不是将接收到的数据打印到控制台。因此,可以再次修改channelRead()方法:

@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) {
    ctx.write(msg); // (1)
    ctx.flush(); // (2)
}
  1. ChannelHandlerContext对象提供了各种操作,使你能够触发各种I/O事件和操作。
  2. ctx.write(Object)不会将消息写入到报文中。它在内部进行缓冲,然后通过ctx.flush()将其写入到报文中。或者,你可以调用ctx.writeAndFlush(msg)简便地把消息写入到报文。

如果你再次运行telnet命令,你将看到服务端发回你发送给它的任何消息。
echo服务的完整源代码位于发行版的io.netty.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个字节。获取当前ByteBufAllocator通过ChannelHandlerContext.alloc()和分配新缓冲区。
  3. 和往常一样,我们写入构造的消息。
    但是,等等,flip方法在哪里呢?在NIO中发送消息之前,我们不是会调用java.nio.bytebuffer.flip()吗?ByteBuf没有这样的方法,因为它有两个指针;一种用于读操作,另一种用于写操作。当你写东西给ByteBuf时,写索引会增加,而读索引不会改变。读索引和写索引分别表示消息的开始和结束。
    相反,NIO缓冲区并没有提供一种简洁的方法来判断消息内容在哪里开始和结束,如果不调用flip方法的话。当你忘记flip缓冲区时,你将会遇到麻烦,因为这样做可能不会发送任何数据或者是发送错误的数据。这样的错误不会在Netty中发生,因为我们有不同的指针针对不同的操作类型。你会发现,当你习惯了Netty的时候,你的开发就会变得更容易——一种不需要翻转的人生!
    另外要注意的一点是,ChannelHandlerContext.write()(和writeAndFlush()方法返回一个ChannelFuture。ChannelFuture表示一个尚未发生的输入/输出操作。这意味着,任何请求的操作可能都不会被执行,因为所有操作在Netty中都是异步的。例如,以下代码可能在发送消息之前关闭连接:
    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();

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

要测试我们的时间服务器是否符合预期,您可以使用UNIX rdate命令:
$ rdate -o <port> -p <host>

是你在main()方法中指定的端口号,而通常是localhost

编写一个获取当前时间服务对应的客户端

与丢弃和ECHO服务不同,我们需要一个用于时间协议的客户端,因为一个人不能将32位二进制数据转换为日历上的日期。在这一节中,我们将讨论如何确保服务器正确工作,并学习如何用Netty编写客户端。
在Netty中,服务端和客户端之间最大且唯一的区别是使用了不同的Bootstrap和Channel 的实现。请看下面的代码:

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,那么它将同时作为老板组和员工组使用。但是,老板组实际并没有被用于客户端。
  3. 与NioServerSocketChannel不同,NioSocketChannel被用于创建客户端的Channel的实现。
  4. 注意,我们像使用ServerBootstrap的childOption()那样使用Bootstrap的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();
    }
}

在TCP/IP协议中,Netty读取匹配的ByteBuf中的数据。
它看起来非常简单,与服务器端示例并没有什么不同。然而,这个处理程序有时会由于IndexOutOfBoundsException而停止运行。我们将讨论在下一节中为什么会发生这种情况。

处理基于流的传输

套接字缓冲区的一个小警告(TCP粘包和拆包问题)

在诸如TCP/IP协议这样的基于流的数据传输协议中,接收到的数据被存储到套接字接收缓冲区中。不幸的是,基于流的传输的缓冲区不是一个数据包队列,而是一个字节队列。而是一个字节队列。这意味着,即使您将两个消息作为两个独立的包发送,操作系统也不会将它们视为两个消息,而只是作为一组字节。因此,不能保证您所读的内容正是您的远程客户端所写的。例如,让我们假设一个操作系统的TCP/IP栈已经收到了三个包:
Netty 4.X的TCP粘包和拆包问题图片
由于基于流的协议的通用属性,在您的应用程序中有很可能会以以下片段形式读取它们:
Netty 4.X的TCP粘包和拆包问题图片
因此,对于接收这个部分,无论它是服务器端还是客户端,都应该将接收到的数据放入一个或多个有意义的框架中,这样可以很容易地理解应用程序逻辑。在上面的例子中,接收到的数据应该像下面这样:
Netty 4.X的TCP粘包和拆包问题图片

第一个解决方案

现在让我们回到客户端示例。我们也有同样的问题。一个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个字节,当写入的数据达到4个字节,则读取4个字节的数据进行实际的业务处理。否则,Netty将再次调用channelRead()方法直到写入的数据达到4个字节。

第二个解决方案

尽管第一个解决方案已经解决了时间客户端的问题,但是修改后的处理程序看起来不那么简洁。想象一个更复杂的协议,它由多个字段组成,比如可变长度字段。您的ChannelInboundHandler 实现类将很快变得难以维护。
正如您可能已经注意到的,您可以将多个ChannelHandler添加到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. ByteToMessageDecoder在接收到新数据时,调用了decode方法,它具有内部维护的累积缓冲区(其实就是方法入参中带有缓存对象)。
  3. decode()可以在累积缓冲区中没有足够的数据的情况下不向out数组添加数据。当接收到更多的数据时,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文档以获得更多信息。
此外,Netty提供了开箱即用的解码器,这使你能够很容易地实现大多数协议,并帮助你避免使用一个单一的不可维护的处理程序实现。请参阅下面的包,以获得更详细的示例:

  • io.netty.example.factorial:使用二进制协议的例子
  • io.netty.example.telnet:基于文本的协议的例子

使用POJO代替ByteBuf

到目前为止,我们所介绍的所有示例都使用ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进时间协议客户端和服务端示例,以使用POJO代替ByteBuf。
在你的ChannelHandler中使用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();
    }
}

现在我们可以修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()));
}

修改了TimeDecoder之后,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. 首先,这样就会在编码的数据被写入到网络的时候,我们通过使用Netty提供的ChannelPromise将其作为成功或失败的标志。
其次,我们没有调用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应用通常就像你通过调用shutdownGracefully()来关闭所有创建的EventLoopGroup对象一样简单。shutdownGracefully()会返回一个Future对象,这个对象会在EventLoopGroup已经完全关闭且属于它的通道已经关闭的时候通知你。

总结

在这一章中,我们对Netty进行了一次简短的介绍,演示了如何在Netty的基础上编写一个完整的网络应用程序。
在即将到来的章节中,Netty有更多的详细信息。我们还建议你可以参考io.netty.example中提供的Netty的例子。
请注意,你可以把自己的想法和问题发到Netty的社区,我们会根据你的反馈不断改善Netty和它的文档。


原文地址:http://netty.io/wiki/user-guide-for-4.x.html
Netty官方示例:https://github.com/netty/netty/tree/4.1/example

展开阅读全文

没有更多推荐了,返回首页