Netty官方 入门示例详解

官网文档翻译: https://netty.io/downloads.html

User guide for 4.x

Netty下载

  • 官方网站下载 : https://netty.io/downloads.html
  • Maven 依赖可以从 Netty 官网下载页中获取:https://netty.io/downloads.html

使用依赖环境

  • Netty没有强制性的外部依赖关系。运行Netty只需JDK 1.5(对于Netty 4+,则为1.6)或更高版本。

如果使用 Maven 进行项目开发管理,则 Netty 也提供了 Maven 依赖。

Maven 依赖可以从 Netty 官网下载页中获取:https://netty.io/downloads.html,如下所示:

<dependencies>
  ...
  <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty</artifactId> <!-- Use 'netty-all' for 4.0 or above -->
    <version>X.Y.Z.Q</version>
    <scope>compile</scope>
  </dependency>
  ...
</dependencies>

netty:如果 Netty 是 4.0 以下版本,则 artifactId值写
netty,如果 Netty 是 4.0 及以上版本,则 写 netty-all。

X.Y.Z.Q:netty 版本号自己填写具体版本即可

如果进一步学习可以下载 源码 ,下载包中有更多官方示例
在这里插入图片描述

业务问题

如今,我们使用通用应用程序或库相互通信。例如,我们经常使用HTTP客户端库从Web服务器检索信息并通过Web服务调用远程过程调用。但是,通用协议其实现有时不能很好地扩展。 就像我们不使用通用HTTP服务器来交换大文件,电子邮件消息和近乎实时的消息(例如财务信息和多人游戏数据)一样。所需要的是专门用于特殊目的的高度优化的协议实现。例如,您可能想要实现针对基于AJAX的聊天应用程序,媒体流或大文件传输进行了优化的HTTP服务器。您甚至可能想要设计和实现完全适合您的需求的全新协议。另一个不可避免的情况是,您必须处理旧的专有协议以确保与旧系统的互操作性。在这种情况下重要的是,我们可以在不牺牲最终应用程序的稳定性和性能的情况下,以多快的速度实现该协议。

解决方案-Netty

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项目社区联系,并让我们知道是否有任何不正确的信息,语法或拼写错误,以及您是否有任何好的想法来帮助改进文档。

编写 Discard Server

世界上最简单的协议不是“ Hello,World!”。但是DISCARD。这是一个协议,它丢弃任何收到的数据而没有任何响应。

要实现DISCARD协议,您只需要忽略所有接收到的数据。让我们直接从处理程序实现类开始,用它处理Netty生成的I/O事件。

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 extends ChannelInboundHandlerAdapter, which is an implementation of ChannelInboundHandler. ChannelInboundHandler
    provides various event handler methods that you can override. For now,
    it is just enough to extend ChannelInboundHandlerAdapter rather than
    to implement the handler interface by yourself.
  2. We override the channelRead() event handler method here. This method is called with the received message, whenever new data is
    received from a client. In this example, the type of the received
    message is ByteBuf.
  3. To implement the DISCARD protocol, the handler has to ignore the received message. ByteBuf is a reference-counted object which has to
    be released explicitly via the release() method. Please keep in mind
    that it is the handler’s responsibility to release any
    reference-counted object passed to the handler. Usually, channelRead()
    handler method is implemented like the following:
  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. The exceptionCaught() event handler method is called with a Throwable when an exception was raised by Netty due to an I/O error or
    by a handler implementation due to the exception thrown while
    processing events. In most cases, the caught exception should be
    logged and its associated channel should be closed here, although the
    implementation of this method can be different depending on what you
    want to do to deal with an exceptional situation. For example, you
    might want to send a response message with an error code before
    closing the connection.

So far so good. We have implemented the first half of the DISCARD
server. What’s left now is to write the main() method which starts the
server with the DiscardServerHandler.

  1. Netty由于I / O错误可抛出异常时或由处理器实现类在处理事件中引发的异常 exceptionCaught()事件处理方法会被调用。在大多数情况下,应该记录捕获的异常,并在此处关闭其关联的通道,尽管此方法的实现可能会有所不同,具体取决于您要处理特殊情况时要采取的措施。例如,您可能想在关闭连接之前发送带有错误代码的响应消息。

到现在为止还挺好。我们已经实现了DISCARD服务器的前半部分。现在剩下的就是编写使用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)
        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 is a multithreaded event loop that handles I/O operation. Netty provides various EventLoopGroup implementations for
    different kind of transports. We are implementing a server-side
    application in this example, and therefore two NioEventLoopGroup will
    be used. The first one, often called ‘boss’, accepts an incoming
    connection. The second one, often called ‘worker’, handles the traffic
    of the accepted connection once the boss accepts the connection and
    registers the accepted connection to the worker. How many Threads are
    used and how they are mapped to the created Channels depends on the
    EventLoopGroup implementation and may be even configurable via a
    constructor.
  2. ServerBootstrap is a helper class that sets up a server. You can set up the server using a Channel directly. However, please note that
    this is a tedious process, and you do not need to do that in most
    cases.
  3. Here, we specify to use the NioServerSocketChannel class which is used to instantiate a new Channel to accept incoming connections.
  4. The handler specified here will always be evaluated by a newly accepted Channel. The ChannelInitializer is a special handler that is
    purposed to help a user configure a new Channel. It is most likely
    that you want to configure the ChannelPipeline of the new Channel by
    adding some handlers such as DiscardServerHandler to implement your
    network application. As the application gets complicated, it is likely
    that you will add more handlers to the pipeline and extract this
    anonymous class into a top-level class eventually.
  5. You can also set the parameters which are specific to the Channel implementation. We are writing a TCP/IP server, so we are allowed to
    set the socket options such as tcpNoDelay and keepAlive. Please refer
    to the apidocs of ChannelOption and the specific ChannelConfig
    implementations to get an overview about the supported ChannelOptions.
  6. Did you notice option() and childOption()? option() is for the NioServerSocketChannel that accepts incoming connections.
    childOption() is for the Channels accepted by the parent
    ServerChannel, which is NioServerSocketChannel in this case.
  7. We are ready to go now. What’s left is to bind to the port and to start the server. Here, we bind to the port 8080 of all NICs (network
    interface cards) in the machine. You can now call the bind() method as
    many times as you want (with different bind addresses.)

Congratulations! You’ve just finished your first server on top of
Netty

  1. NioEventLoopGroup是处理I / O操作的多线程事件循环。NettyEventLoopGroup为不同类型的传输提供了各种实现。在此示例中,我们正在实现服务器端应用程序,因此NioEventLoopGroup将使用两个。第一个通常称为“boss,接受传入的连接。第二个通常称为“worker”,一旦boss接受了连接并且向工作进程worker注册接受的连接,便处理已接受连接的流量。使用多少线程以及如何将它们映射到创建的Channels取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。
  2. ServerBootstrap是设置服务器的帮助程序类。您可以Channel直接使用直接设置服务器。但是,请注意,这是一个单调乏味的过程,在大多数情况下,您无需这样做。
  3. 在这里,我们指定使用NioServerSocketChannel用于实例化新的类Channel以接受传入的连接。
  4. 在此指定的处理程序将始终由新接受的评估Channel。该ChannelInitializer是意要帮助用户配置一个新的特殊处理程序Channel。您很可能希望通过添加一些处理程序(例如实现您的网络应用程序)来配置ChannelPipeline新Channel的DiscardServerHandler。随着应用程序变得复杂,您可能会向管道添加更多处理程序,并最终将此匿名类提取到顶级类中。
  5. 您还可以设置特定于Channel实现的参数。我们正在编写一个TCP / IP服务器,因此可以设置诸如tcpNoDelay和的套接字选项keepAlive。请参考的apidocsChannelOption和特定的ChannelConfig实现以获取有关support的概述ChannelOption。
  6. 您注意到option()和childOption()了吗?option()用于接受传入连接的NioServerSocketChannel。childOption()用于父ServerChannel接受的通道,在本例中是NioServerSocketChannel。
  7. 我们现在准备开始。剩下的就是绑定到端口并启动服务器。在这里,我们绑定到计算机8080中所有NIC(网络接口卡)的端口。现在,您可以根据需要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. A ChannelHandlerContext object provides various operations that enable you to trigger various I/O events and operations. Here, we
    invoke write(Object) to write the received message in verbatim. Please
    note that we did not release the received message unlike we did in the
    DISCARD example. It is because Netty releases it for you when it is
    written out to the wire.
  2. ctx.write(Object) does not make the message written out to the wire. It is buffered internally and then flushed out to the wire by
    ctx.flush(). Alternatively, you could call ctx.writeAndFlush(msg) for
    brevity.

If you run the telnet command again, you will see the server sends
back whatever you have sent to it.

The full source code of the echo server is located in the
io.netty.example.echo package of the distribution.

  1. 一个ChannelHandlerContext对象提供的各种操作,使您可以触发各种I / O的事件和操作。在这里,我们调用write(Object)以逐字书写接收到的消息。请注意,我们没有像DISCARD示例中那样释放收到的消息。这是因为Netty在将其写到网络时会为您释放它。
  2. ctx.write(Object)不会将消息写到网络上。它在内部进行缓冲,然后通过ctx.flush()冲洗到网络上。或者,为了简洁些,您可以调用ctx.writeAndFlush(msg)。

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

回显服务器的完整源代码位于io.netty.example.echo分发包中。

编写时间服务器

The protocol to implement in this section is the TIME protocol. It is
different from the previous examples in that it sends a message, which
contains a 32-bit integer, without receiving any requests and closes
the connection once the message is sent. In this example, you will
learn how to construct and send a message, and to close the connection
on completion.

Because we are going to ignore any received data but to send a message
as soon as a connection is established, we cannot use the
channelRead() method this time. Instead, we should override the
channelActive() method. The following is the implementation:

本节中要实现的协议是TIME协议。它与前面的示例不同之处在于,它不包含任何请求就发送包含32位整数的消息,并在发送消息后关闭连接。在此示例中,您将学习如何构造和发送消息,以及如何在完成时关闭连接。

因为我们将忽略任何接收到的数据,而是在建立连接后立即发送消息,所以这次我们不能使用channelRead()该方法。相反,我们应该重写该channelActive()方法。以下是实现:

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. 和往常一样,我们编写构造的消息。
    但是,等等,翻转在哪里?java.nio.ByteBuffer.flip()在NIO中发送消息之前,我们不是已经调用过了java.nio.ByteBuffer.flip()在NIO中发送消息之前?ByteBuf没有这样的方法,因为它有两个指针:一个用于读操作,另一个用于写操作。当您向ByteBuf写入内容时,writer索引会增加,而reader索引不变。reader索引和writer索引分别表示消息的开始和结束位置。
    相比之下,NIO缓冲区没有提供一种清晰的方法来确定消息内容在哪里开始和结束,而不用调用flip方法。如果您忘记翻转缓冲区,因为不会发送任何内容或不正确的数据,您将陷入麻烦。这样的错误在Netty中不会发生,因为我们对不同的操作类型有不同的指针。你会发现,当你习惯它时,它会让你的生活变得更轻松——一种没有flip的环境!
    需要注意的另一点是ChannelHandlerContext.write()(和writeAndFlush())方法返回一个ChannelFuture。AChannelFuture表示尚未发生的I / O操作。这意味着,由于Netty中的所有操作都是异步的,因此可能尚未执行任何请求的操作。例如,以下代码甚至在发送消息之前就可能关闭连接:

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

因此,您需要在ChannelFuture完成后调用close()方法,该方法通过write()方法返回,并在完成写操作后通知其侦听器。请注意,close()也可能不会立即关闭连接,并且返回ChannelFuture。

How do we get notified when a write request is finished then? This is
as simple as adding a ChannelFutureListener to the returned
ChannelFuture. Here, we created a new anonymous ChannelFutureListener
which closes the Channel when the operation is done.

Alternatively, you could simplify the code using a pre-defined
listener:

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

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

f.addListener(ChannelFutureListener.CLOSE);

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

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

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

编写时间客户端

与DISCARD和ECHO服务器不同,我们需要该TIME协议的客户端,因为人类无法将32位二进制数据转换为日历中的日期。在本节中,我们讨论如何确保服务器正常工作,并学习如何使用Netty编写客户端。

服务器和Netty中的客户机之间的最大和唯一的区别是不同的Bootstrap,与 使用的Channel实现类。请看下面的代码:

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,它将同时用作老板组和工人组。但是,老板工人并不用于客户端。
  3. 相反的NioServerSocketChannel,NioSocketChannel被用来创建一个客户端Channel。
  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堆栈已收到三个数据包:

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

因此,无论是服务器端还是客户端,接收方都应将接收到的数据整理到一个或多个有意义的帧中,以使应用程序逻辑易于理解。在上面的示例中,接收到的数据应采用以下格式:

第一个解决方案

现在让我们回到TIME客户示例。我们在这里有同样的问题。32位整数是非常少量的数据,并且不太可能经常碎片化。但是,问题在于它可以被碎片化,并且碎片化的可能性会随着流量的增加而增加。

一种简单的解决方案是创建一个内部累积缓冲区,然后等待直到所有4个字节都被接收到内部缓冲区中为止。以下是修改后的TimeClientHandler实现,可以解决此问题:

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. A ChannelHandler has two life cycle listener methods: handlerAdded() and handlerRemoved(). You can perform an arbitrary
    (de)initialization task as long as it does not block for a long time.
  2. First, all received data should be cumulated into buf.
  3. And then, the handler must check if buf has enough data, 4 bytes in this example, and proceed to the actual business logic. Otherwise,
    Netty will call the channelRead() method again when more data arrives,
    and eventually all 4 bytes will be cumulated.
  1. 一个ChannelHandler有两个生命周期侦听器方法:handlerAdded()和handlerRemoved()。您可以执行任意(取消)初始化任务,只要它不会长时间阻塞即可。
  2. 首先,应将所有接收到的数据累加到buf中。
  3. 然后,处理程序必须检查是否buf有足够的数据(在此示例中为4个字节),然后继续进行实际的业务逻辑。否则,当有更多数据到达时,Netty将再次调用channelRead()方法,最终所有4个字节将被累加。

第二种解决方案

Although the first solution has resolved the problem with the TIME
client, the modified handler does not look that clean. Imagine a more
complicated protocol which is composed of multiple fields such as a
variable length field. Your ChannelInboundHandler implementation will
become unmaintainable very quickly.

As you may have noticed, you can add more than one ChannelHandler to a
ChannelPipeline, and therefore, you can split one monolithic
ChannelHandler into multiple modular ones to reduce the complexity of
your application. For example, you could split TimeClientHandler into
two handlers:

  • TimeDecoder which deals with the fragmentation issue, and
  • the initial simple version of TimeClientHandler.

Fortunately, Netty provides an extensible class which helps you write
the first one out of the box:

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

正如你可能已经注意到,您可以添加多个ChannelHandler到ChannelPipeline,因此,您可以拆分一个单片ChannelHandler成多个模块 减少了应用程序的复杂性。例如,您可以分为TimeClientHandler两个处理程序:

  • TimeDecoder 处理碎片问题
  • TimeClientHandler的初始简单版本

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

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()``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参考以获取更多信息。

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

到目前为止,我们已经回顾的所有示例都使用aByteBuf作为协议消息的主要数据结构。在本节中,我们将改进TIME协议客户端和服务器示例,以使用POJO而不是ByteBuf。

在ChannelHandlers中使用POJO的优势是显而易见的;通过将从ByteBuf中提取信息的代码从处理程序中分离出来,处理程序变得更加可维护和可重用。在TIME客户机和服务器示例中,我们只读取一个32位整数,直接使用ByteBuf不是一个主要问题。但是,您会发现在实现一个真实的协议时,有必要进行分离。

首先,让我们定义一个名为的新类型UnixTime。

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冲洗(). 有一个单独的处理程序方法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中,这是一个简单的练习。

关闭您的应用程序

Shutting down a Netty application is usually as simple as shutting
down all EventLoopGroups you created via shutdownGracefully(). It
returns a Future that notifies you when the EventLoopGroup has been
terminated completely and all Channels that belong to the group have
been closed.

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

概要

在本章中,我们快速浏览了Netty,并演示了如何在Netty之上编写功能全面的网络应用程序。

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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值