netty - 简单使用与介绍

问题

现在我们使用通用应用程序或库来相互通信。例如,我们经常使用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项目社区,并让我们知道。

写丢弃服务器

世界上最简单的协议不是“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);
    }
}
  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;
    
/**
 * 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。第一个,通常被称为“老板”,接受一个传入的连接。第二个通常称为“worker”,在老板接受连接并向worker注册接受的连接后,处理已接受连接的流量。使用了多少线程以及如何将它们映射到创建的通道,这取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
  2. ServerBootstrap是一个建立服务器的助手类。您可以直接使用Channel设置服务器。然而,请注意,这是一个乏味的过程,在大多数情况下您不需要这样做。
    这里,我们指定使用NioServerSocketChannel类,该类用于实例化一个新的Channel以接受传入的连接。
  3. 这里指定的处理程序将始终由新接受的Channel进行计算。ChannelInitializer是一个特殊的处理程序,用于帮助用户配置新Channel。您很可能希望通过添加一些处理程序(如DiscardServerHandler)来配置新Channel的ChannelPipeline,以实现您的网络应用程序。随着应用程序变得复杂,您很可能会向管道中添加更多的处理程序,并最终将这个匿名类提取到顶级类中。
  4. 您还可以设置特定于Channel实现的参数。我们正在编写一个TCP/IP服务器,因此允许设置套接字选项,如tcpNoDelay和keepAlive。请参阅ChannelOption的apidocs和特定的ChannelConfig实现,以获得有关受支持的ChannelOptions的概述。
  5. 你注意到option()和childOption()了吗?option()用于接受传入连接的NioServerSocketChannel。childOption()用于父ServerChannel接受的通道,在本例中是NioSocketChannel。
  6. 我们现在可以开始了。剩下的就是绑定到端口并启动服务器。这里,我们绑定到机器中所有网卡(网络接口卡)的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)
    }
}

这个低效的循环实际上可以简化为:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
或者,您可以在这里使用in.release()。
如果再次运行telnet命令,您将看到服务器打印它所接收到的内容。

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

编写Echo服务器

到目前为止,我们一直在消费数据而没有任何响应。然而,服务器通常是响应请求的。让我们学习如何通过实现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. 像往常一样,我们编写构造的消息。

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

    相比之下,如果不调用flip方法,NIO缓冲区并没有提供一种清晰的方法来确定消息内容的开始和结束位置。当您忘记翻转缓冲区时,您将遇到麻烦,因为没有任何数据或不正确的数据将被发送。这样的错误不会在Netty中发生,因为不同的操作类型有不同的指针。你会发现,当你习惯它时,它会让你的生活更容易——一种没有失控的生活!

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

Channel ch = ...;
ch.writeAndFlush(message);
ch.close();

因此,您需要在ChannelFuture完成之后调用close()方法,该方法由write()方法返回,当写操作完成时,它会通知它的侦听器。请注意,close()也可能不会立即关闭连接,它会返回一个ChannelFuture。

  1. 当写请求完成时,我们如何得到通知?这就像向返回的ChannelFuture添加一个ChannelFutureListener一样简单。在这里,我们创建了一个新的匿名ChannelFutureListener,它在操作完成时关闭Channel。

    或者,您可以使用预定义的侦听器简化代码:

f.addListener(ChannelFutureListener.CLOSE);

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

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

其中是main()方法中指定的端口号,通常是localhost。

撰写时间客户端

与DISCARD和ECHO服务器不同,我们需要TIME协议的客户端,因为人类无法将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,它将同时用作老板组和工作者组。但是,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将从对等体发送的数据读入字节缓存器。

它看起来非常简单,与服务器端示例没有任何不同。然而,这个处理程序有时会拒绝引发IndexOutOfBoundsException。我们将在下一节讨论为什么会发生这种情况。

处理基于流的传输

Socket Buffer的一个小警告

在基于流的传输(如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()。只要不长时间阻塞,就可以执行任意(反)初始化任务。

  2. 首先,所有接收到的数据都应该被累积到buf中。

  3. 然后,处理程序必须检查buf是否有足够的数据(本例中为4字节),然后继续执行实际的业务逻辑。否则,当更多数据到达时,Netty将再次调用channelRead()方法,最终将累积所有4个字节。

第二个解决方案

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

正如您可能已经注意到的,您可以向ChannelPipeline添加多个ChannelHandler,因此,您可以将一个单独的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()方法,直到它不向外添加任何内容。

现在我们有了另一个要插入到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。

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

在这一行中有很多重要的东西。

首先,我们按原样传递原始的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应用程序通常和关闭所有你通过shutdownGracefully()创建的EventLoopGroups一样简单。当EventLoopGroup被完全终止,并且属于该组的所有channel已经被关闭时,它会返回一个Future通知你。

总结

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty是一个基于NIO的客户端、服务器端编程框架,它提供了一组易于使用的API,可以帮助开发人员快速地开发出高性能、可伸缩的网络应用程序。Netty框架提供了很多编码器,其中最常用的就是ByteToMessageCodec和MessageToByteCodec,它们分别用于将字节流转换为消息对象和将消息对象转换为字节流。 ByteToMessageCodec和MessageToByteCodec的使用方式非常简单,只需要继承它们,并实现其中的抽象方法即可。以下是使用ByteToMessageCodec和MessageToByteCodec的示例代码: ```java public class MyMessageDecoder extends ByteToMessageCodec<MyMessage> { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < 4) { return; } int length = in.readInt(); if (in.readableBytes() < length) { in.resetReaderIndex(); return; } byte[] data = new byte[length]; in.readBytes(data); MyMessage message = new MyMessage(length, data); out.add(message); } @Override protected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception { out.writeInt(msg.getLength()); out.writeBytes(msg.getData()); } } public class MyMessageEncoder extends MessageToByteCodec<MyMessage> { @Override protected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception { out.writeInt(msg.getLength()); out.writeBytes(msg.getData()); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < 4) { return; } int length = in.readInt(); if (in.readableBytes() < length) { in.resetReaderIndex(); return; } byte[] data = new byte[length]; in.readBytes(data); MyMessage message = new MyMessage(length, data); out.add(message); } } ``` MyMessage是自定义的消息对象,包含了消息的长度和数据。MyMessageDecoder和MyMessageEncoder分别用于将字节流转换为MyMessage对象和将MyMessage对象转换为字节流。在decode方法中,首先从ByteBuf中读取消息的长度,然后再读取消息的数据,最后将它们封装成MyMessage对象,加入到解码结果列表中。在encode方法中,首先将消息的长度写入到ByteBuf中,然后再将消息的数据写入到ByteBuf中。 使用ByteToMessageCodec和MessageToByteCodec时,只需要将它们注册到ChannelPipeline中即可。以下是注册的示例代码: ```java ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(new MyMessageDecoder()); pipeline.addLast(new MyMessageEncoder()); ``` 这样,在ChannelPipeline中的所有ChannelHandler都可以使用MyMessage对象,而无需关心它们与字节流的转换过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值