Netty 4.x 用户指南

前言

原文链接:https://netty.io/wiki/user-guide-for-4.x.html

问题

现在我们使用通用的应用程序或库相互通信。例如,我们经常使用一个HTTP客户端库来从一个web服务器上检索信息和通过web服务进行远程过程调用。然而,一个通用的协议或者它的实现有时候伸缩性并不是很好。这就像我们不能使用通用的HTTP服务器去交换大文件、e-mail信息和接近实时的消息,比如金融信息和多人游戏数据。所需要的是一个高度优化的协议实现,它专门用于一个特殊用途。例如,你可能想要实现针对基于AJAX的聊天应用程序,媒体流或大型文件传输进行优化的HTTP服务器。你甚至想设计和实现一种完全根据自己的需要定制的全新协议。另一个不可避免的情况是,您必须处理遗留的专有协议,以确保与旧系统的互操作性。在这种情况下,重要的是我们能够在不牺牲最终应用程序的稳定性和性能下多快地实现该协议。

解决方案

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

换句话说,Netty是一个NIO客户端服务器框架,使开发者能够快速并且容易地开发网络应用程序,比如协议服务器和客户端。它极大地简化了网络编程,比如TCP和UDP套接字服务器端程序开发。

‘快速和简单’并不意味着将导致应用程序出现可维护性和性能方面的问题。Netty的设计借鉴了许多协议的实现经验,例如FTP, SMTP, HTTP,以及各种二进制和基于文本的遗留协议。因此,Netty成功地找到了一种方法可以实现轻松的开发,并保证程序的性能,稳定性和灵活性,而无需妥协。

一些用户可能已经找到了其他声称拥有相同优势的网络应用程序框架,你可能会问,是什么让Netty与他们如此不同。答案就是它是以哲学为基础的。从第一天开始Netty就为您提供最舒适的API和实现。这不是什么有形的东西,但你会意识到,当你阅读这本指南并与使用Netty时,这种哲学会让你的生活变得更加容易。

开始

本章介绍了Netty的核心构造,通过简单的例子,让您快速入门。当你看到本章最后时,你将立即可以使用Netty写一个客户端和一个服务器。

如果你更喜欢自顶向下的方式学习知识,那么可以从第2章开始,它主要概述了Netty的结构,然后再回到这里。

在开始之前

在本章中运行示例的最低要求只有两个:最新版本的Netty和JDK 1.6或以上版本。Netty的最新版本在项目下载页可以获得。要下载正确版本的JDK,请到JDK供应商的官网下载。

当你阅读时,可能会对本章介绍的类有很多疑问。当你想了解它们更多信息时,请查阅API文档。这个文档中的所有类都已经链接到在线的API文档上,你可以很方便的查看。并且,如果有任何的错误信息、错误的语法和排版,或者有好多主意来改善文档,请不要犹豫地联系Netty项目社区让我们知道。

写一个丢弃服务

世界上最简单的协议不是’Hello, World!’,而是DISCARD。他就是一个丢弃任何接收到的数据和没有任何响应的协议。

为了实现DISCARD协议,你惟一需要做的就是忽略所有接收到的数据。让我们直接从handler实现开始,它处理由Netty生成的I/O事件。

package io.netty.example.discard;

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 1. 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. DiscardServerHandle继承了实现了ChannelInboundHandler接口的ChannelInboundHandlerAdapterChannelInboundHandler提供了可以重写的各种事件处理方法。目前,继承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);
    }
}
  1. 当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;

/**
 1. 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)

            // 绑定并开始接受进来的连接
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 等待直到server socket被关闭。
            // 在这个例子中,这是不会发生的,但是你可以优雅地关闭你的服务器。
            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。第一个经常被称为’boss’,接受传入的连接。第二个经常被称为’worker’,当boss接受连接,并且注册这个连接到worker时,处理接受连接的流量。使用多少线程以及如何将它们映射到创建的Channels依赖于EventLoopGroup的实现,甚至可以通过构造器进行配置。
  2. ServerBootStrap是一个帮助器类,用于设置服务器。你可以直接使用Channel来设置服务器。然而,请注意,这个一个冗长的过程,在大多数情况下你不需要这样做。
  3. 这里,我们指定使用NioServerSocketChannel类来实例化一个新的Channel,用于接受传入的连接。
  4. 这里被指定的处理器将总是由最新接受的Channel评估。ChannelInitializer是个特殊的处理器,用于帮助用户配置一个新的Channel。你很可能想配置新ChannelChannelPipeline,通过添加一些处理器(比如DiscardServerHandler)来实现你的网络应用程序。随着应用程序变得复杂,您可能会向管道中添加更多的处理程序,并最终将这个匿名类提取到顶级类中。
  5. 你也可以设置Channel实现的相关的参数。我们写了一个TCP/IP服务器,因此我们可以设置socket选项,例如tcpNoDelay和keepAlive。请参考ChannelOption的API文档和具体的ChannelConfig实现,从而有一个关于被支持的ChannelOptionS的总体认识。
  6. 你有注意到option()和chaildOption()吗?option()是用于接受传入连接的NioServerSocketChannel。childOption()是用于通过父ServerChannel接受的Channels,在这个例子中是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.netty.util.CharsetUtli.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事件和操作的不同操作。这里,我们调用write(Object)来逐字写入接收到的消息。请注意,我们并没有像在DISCARD例子中那样释放接收到的消息。当它被写出来时,Netty会为你释放它。
  2. ctx.write(Object)不会将消息写出来。它会内部缓存,然后通过ctx.flush()刷新出去。或者你可以使用更简洁的方式调用:ctx.writeAndFlush(msg)。

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

echo服务器的完整源代码存储在io.netty.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. 想往常一样,我们写入了构造的消息。

    但是等等,翻转在哪里?在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()方法,ChannelFuture由write()方法返回,当写操作完成时它将通知它的监听者。请注意,close()也可能不会立即关闭连接,并且它将返回一个ChannelFuture。

  4. 当一个写请求完成时,我们如何得到通知?这非常简单,通过向返回的ChannelFuture添加一个ChannelFutureListener来实现。这里,我们创建了一个新的匿名的ChannelFutureListener,当操作完成时它将关闭Channel。

    或者,你可以通过使用预定义的监听者来简化代码:

    f.addListener(ChannelFutureListener.CLOSE);

为了测试我们的时间服务器是否能够像预期的那样工作,你可以使用UNIX rdate 命令:

$ rdate -o <port> -p <host>

是你在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<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. BootstrapServerBootstrap是相似的,除了它是无服务器通道,例如一个客户端或者无连接通道。
  2. 如果你仅仅指定一个EventLoopGroup,它将即被用作boss组也被用作worker组。即使boss不使用在客户端。
  3. 代替NioServerSocketChannel的是NioSocketChannel,它被用于创建客户端的Channel
  4. 注意,这里我们不使用childOption(),不像之前的ServerBootstrap,因为客户端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的数据。

它看起来非常简单,并且看不出与服务端例子有任何不同。然而,这个处理器有时候会拒绝服务,并抛出一个IndexOutOfBoundException异常。我们将在下一节中讨论为什么会发生这种情况。

处理基于流的传输
套接字缓冲区的一个小警告

在一个基于流的传输中,例如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个字节,并继续进行实际的业务逻辑。否则,当更多数据到达时,Netty将再次调用channelRead()方法,最终所有的4个字节都会被累积。
第二种解决方案

虽然第一种方案已经解决了TIME客户端的问题,但是修改后的处理器看起来并不整洁。想象一个更复杂的协议,它由多个字段(如可变长度字段)组成。你的ChannelInboundHandler实现将很快变得不可维护。

你可能已经注意到,一个ChannelPipeline可以添加多个ChannelHandler,因此,您可以将一个庞大的ChannelHandler拆分为多个模块,以减少应用程序的复杂性。例如你可以拆分TimeClientHandler为两个handlers:

  • TimeDecoder用于处理分片问题,和
  • TimeClientHandler最初的简单版本

幸运地,Netty提供了一个可扩展的类,可以帮助你写出TimeDecoder文件:

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. ByteToMessageDecoderChannelInBoundHandler的一个实现类,使它能够容易的处理分片问题。
  2. 每当有新数据接收时,ByteToMessageDecoder调用decode()方法,它是一个内部维持累积缓冲区的方法。
  3. 在累积缓冲区没有足够的数据时,decode方法能够决定不添加任何东西到out中。当有更多的数据收到时,ByteToMessageDecoder将再次调用decode()方法。
  4. 如果decode()方法添加了一个对象到out中,这意味着解码器成功地解码了一个消息。ByteToMessageDecoder将丢弃累计缓冲区的read部分。请记住,你不需要解码多个消息。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提供了开箱即用的解码器,它们使你能够非常容易地实现大多数协议,并且帮助你避免以不可维护的处理程序实现结束。详情请参阅以下资料:

用POJO代替ByteBuf

到目前为止,我们看到的所有的例子都是使用ByteBuf作为一个协议消息的主要数据结构。在这个章节中,我们将通过使用一个POJO代替ByteBuf来改善TIME协议的客户端和服务端例子。

在你的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:

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)
    }
}

剩下的最后一步是在服务器端,在插入TimeServerHandler之前插入一个TimeEncoder到ChannelPipeline,这只是一个小练习。

关闭应用程序

关闭一个Netty应用程序通常就像通过shutdownGrancefully()方法关闭EventLoopGroup一样简单。当EventLoopGroup已经被完全关闭,并且属于这个组的所有Channels也被关闭,它会返回一个Future通知你。

总结

在本章中,我们快速浏览了Netty,并演示了如何使用Netty编写一个完整的网络应用程序。

在接下来的章节中有更多关于Netty的详细信息。我们还鼓励您查看io.netty.example包中的Netty示例。

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


文中涉及到的代码地址:https://github.com/ByrsH/Netty-user-guide

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值