Netty 4.x 用户手册

2 篇文章 0 订阅
1 篇文章 0 订阅

User guide for 4.x

Did you know this page is automatically generated from   a Github Wiki page?  You can improve it by yourself   here!

Preface

The Problem

Nowadays we use general purpose applications or libraries to communicate with each other. For example, we often use an HTTP client library to retrieve information from a web server and to invoke a remote procedure call via web services.

However, a general purpose protocol or its implementation sometimes does not scale very well. It is like we don't use a general purpose HTTP server to exchange huge files, e-mail messages, and near-realtime messages such as financial information and multiplayer game data. What's required is a highly optimized protocol implementation which is dedicated to a special purpose. For example, you might want to implement an HTTP server which is optimized for AJAX-based chat application, media streaming, or large file transfer. You could even want to design and implement a whole new protocol which is precisely tailored to your need.

Another inevitable case is when you have to deal with a legacy proprietary protocol to ensure the interoperability with an old system. What matters in this case is how quickly we can implement that protocol while not sacrificing the stability and performance of the resulting application.

The Solution

The Netty project is an effort to provide an asynchronous event-driven network application framework and tooling for the rapid development of maintainable high-performance · high-scalability protocol servers and clients.

In other words, Netty is an NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server development.

'Quick and easy' does not mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.

Some users might already have found other network application framework that claims to have the same advantage, and you might want to ask what makes Netty so different from them. The answer is the philosophy it is built on. Netty is designed to give you the most comfortable experience both in terms of the API and the implementation from the day one. It is not something tangible but you will realize that this philosophy will make your life much easier as you read this guide and play with Netty.

Getting Started

This chapter tours around the core constructs of Netty with simple examples to let you get started quickly. You will be able to write a client and a server on top of Netty right away when you are at the end of this chapter.

If you prefer top-down approach in learning something, you might want to start from Chapter 2, Architectural Overview and get back here.

Before Getting Started

The minimum requirements to run the examples which are introduced in this chapter are only two; the latest version of Netty and JDK 1.6 or above. The latest version of Netty is available in the project download page. To download the right version of JDK, please refer to your preferred JDK vendor's web site.

As you read, you might have more questions about the classes introduced in this chapter. Please refer to the API reference whenever you want to know more about them. All class names in this document are linked to the online API reference for your convenience. Also, please don't hesitate to contact the Netty project community and let us know if there's any incorrect information, errors in grammar and typo, and if you have a good idea to improve the documentation.

写一个Discard Server

这个世界上最简单的案例并不是’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;

/**
 * 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 , ChannelInboundHandlerAdapterChannelInboundHandler的一个实现.到目前为止,相对于你自己实现一个handler接口,其实继承ChannelInboundHandlerAdapter已经够用了
  2. 我们重写了channelRead()这个事件方法, 不管新的数据什么时候从客户端到我们这个服务器,这个方法都会在数据接受完毕之后被执行。在这个实例中,接受到的消息类型是ByteBuf
  3. 为了实现一个DISCARD协议,handler必须忽略它收到的消息.ByteBuf本身是一个必须通过release()方法来释放它自己的引用计数的对象。因此请务必记住,handler方法有责任释放任何传过来的引用计数对象。一般来说,channelRead()这个handler方法会按照以下方式来实现:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // Do something with msg
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

  4. 当Netty抛出I/O异常或者某个handler方法在执行过程中抛出异常的时候,便会执行exceptionCaught()方法。当然,因为你要根据你自身的情况去处理这些异常,所以方法本身的实现可能会多种多样,但是在大多数情况下,捕获到异常需要被记录下来,同时相关联的通道也要在这被关闭。举例来说,在你关闭连接之前,你可能会返回一个标识异常的协议码回去。


目前还算顺利。我们已经是实现了这个DISCARD server的前半部分。接下来要写在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;
        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的时候,将由它来处理这个连接。要使用多少个线程以及他们怎样跟新建通道映射起来取决于EventLoopGroup的具体实现或者由其构造函数来配置.
  2. ServerBootstrap是一个建立服务器的帮助类。你也可以用一个Channel直接建一个服务器。然而,请你注意这是一个很冗长的过程,大多数情况下你没必要这样做.
  3. 这里,我们指定使用 NioServerSocketChannel类来实例化一个新的通道,用来接收请求的连接.
  4. 这个Handler的实现取决于新加的接收通道。这个 ChannelInitializer是被设定用来帮助配置新通道的一个特殊的handler。它很像你通过增加DiscardServerHandler到新通道这种方式来配置 ChannelPipeline实现你的程序。当程序变得越来越复杂,你最终很有可能会增加更多的handler到通道上并且提取这个匿名类到顶级类里去.
  5. 你也可以针对通道的实现设置参数,我们在写一个TCP/IP服务器,因此我们允许设置一些参数比如tcpNoDelay和keepAlive。请参考 ChannelOptionapi文档和具体ChannelConfig的实现来看一下所支持的ChannelOptions.
  6. 你是否注意到option() 和 childOption()?option()对应的是 NioServerSocketChannel接受到的那些请求过来的连接。childOption()对应的是来自父ServerChannel通道的请求,在这个例子里ServerChannelNioServerSocketChannel.
  7. 我们已经准备好运行了。剩下的就是绑定端口启动服务了。这里,我们绑定这个机器的所有网卡的8080端口上,你现在可以执行bind()方法,想绑定多少个就可以绑定多少个,只要是不同的ip.

恭喜!你已经完成了你在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命令,你会看到服务器会打印出它收到的消息,完整的discard server的源代码你可以在 io.netty.example.discard里找到

写一个响应服务器

目前为止,我们已经提交了很多完全不会被响应的数据到服务器。但作为一个服务器,通常都会对请求有所响应才行。让我们学习一下如何实现一个 ECHO 协议用以发送响应消息到客户端,它会将所有收到的数据原封不动退回去。它跟上个章节的discard server唯一的区别的就是它不再将数据打印到控制台而是原样返回。因此,改一下这个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 server的源代码放在io.netty.example.echo包里。

写一个Time Server

这个章节会实现一个TIME协议。它跟之前例子的区别就是,它包括一个32-bit的整型数据,不需要任何请求驱动,当发送完毕数据之后会自动断开。在这个例子里,你会学习如何构建和发送一个消息体,以及当数据发送完毕之后怎样关闭这个链接。

因为当连接建立之后我们需要尽可能快的发送一条消息并忽略任何收到的数据,所以我们这里不能使用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-bit的整型。
  2. 为了发送一条新的消息,我们需要分配一个包含这条消息的新的buffer。我们会发送一条32-bit的整型,因此我们需要一个至少有4个bytes大小的ByteBuf。通过ChannelHandlerContext.alloc()方法取得ByteBufAllocator的一个实现,并申请一条新的buffer。
  3. 和之前一样,我们开始发送组装好的数据。但是等等,flip方法在哪儿?我们在使用NIO发送一条消息之前,通常不是要执行java.nio.ByteBuffer.flip()吗?ByteBuf没有这样一个方法,因为如下两点原因;一个是因为读操作,一个是因为写操作。当读标志位不变的同时你写一些数据到ByteBuf之后写标志位会增大。读标志位和写标志位各自指明了消息从那儿开始到那儿结束。对比而言,除非你使用flip方法,否则NIO的buffer没法提供一条明确的方法来指出这条消息的起点和终点。当你忘记filp就会出问题,你会发送空的或者错误的数据出去。但这种错误不会在Netty里发生,因为我们针对不同的操作类型有不同的指针。你会发现它会让你生活的变得更加简单容易--这是一种没有flipping out的生活!另一个需要注意的点就是ChannelHandlerContext.write()(以及writeAndFlush())方法返回了一个ChannelFuture对象。ChannelFuture代表了一个还未发生的I/O操作。它的意思是,任何请求执行的操作都有可能还并没有被执行,因为Netty里的所有操作都是异步的。举例来说,下面的这段代码中,消息可能还没发送,连接就已经关闭了。

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

    因此,你需要将close()方法的执行放在 ChannelFuture完成以后,即得到write()方法返回值以后,同时当写操作完毕之后它会唤醒监听器。请注意close()方法也不一定会立即关闭这个连接,它也会返回一个ChannelFuture

  4. 当一个请求完成之后我们如何得到通知呢?这是一个附加在ChannelFuture上的再简单不过的ChannelFutureListener。在这里,我们创建了一个ChannelFutureListener匿名类,当操作完成之后用来关闭Channel。

    或者,你可以用预定义好的这个监听器来简化代码:

    f.addListener(ChannelFutureListener.CLOSE);

可以测试下我们的服务器是否按预想的那样工作,你可以用UNIX rdate 命令:

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

<port>是在main()方法里定义好的端口号,<host>一般是localhost。

写一个Time Client

跟DISCARD和ECHO服务器不一样,我们需要一个为TIME协议做一个客户端,因为人是没法将一个32-bit的2进制数据转换成一个历法表上的日期的。本章,我们会讨论如何保证服务器能正确的工作,并且学习如何用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,它将会被既作为boos group又作为worker group。但boss worker并不会在客户端被用到。
  3. 作为NioServerSocketChannel的替代,NioSocketChannel用来新建一个客户端通道
  4. 注意,与ServerBootstrap不同的是我们并没有在这里使用childOption(),因为客户端SocketChannel并没有父类。
  5. 我们应该使用connect()替代bind()方法。

如你所见到的这样,其实跟服务器端代码并没有多大不同。那么ChannelHandler又该如何来实现呢?它必须接受从服务器发过来的32-bit整型数据,转换成普通人可读的格式,打印出这个时间,然后关闭连接。

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‘之后的数据

它看起来很简单并且看起来和服务器端代码没多大不同。然而,这个handler有时间会拒绝工作并且抛出IndexOutOfBoundsException异常。我们会在下章讨论为什么会出现这样的问题。

处理一个基于流的传输协议

一处的关于Socket Buffer的警告

在一个像TCP/IP这种基于流的传输协议里,接受到的数据会被存储在一个套接字接受缓存里。不幸的是,这些流传输协议的缓存并不是一个个包队列,而是比特队列。这意味着,就算你发送了分别放在两个独立的包里的两条消息,操作系统也不会将他们处理成两条消息,而是当成一整串比特流。因此,没法保证你读到的就是你的远端发送的。举个例子,我们假设一个操作系统的TCP/IP栈接受到了三个包:

[ A B C ][ D E F ][ G H I ]

由于基于流协议的一贯特点,有极大的可能你的程序读到的是以下的碎片

[ A B ][ C D E F G ][ H I ]

因此,对于接受到的这段数据,不管是在服务器端还是客户端,都必须将这些碎片转成一个或多个可以被程序逻辑理解的有意义的帧。万一出现以上情况,接受到的数据必须定制为下面的样子:

[ A B C ][ D E F ][ G H I ]

第一种解决方案

现在让我们回到TIME客户端例子。我们在这里也遇到了同样的问题。一个32-bit的整型是一个非常小的数据,看起来它应该不会被频繁的碎片化。然而,真正的问题在于它可以被碎片化,而且这个几率会随着通信量的增加而增加。

最简单的解决办法是创建一个内部累加buffer并且等待所有4个byte都接受到这个内部buffer里。下面是修改并解决了这个问题之后的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. 接下来,handler必须检查buf中是否有没有装满,在这个例子里是4byte,然后再在业务逻辑里处理。否则,当更多的数据到达之后,Netty会再次执行channelRead()方法,直到所有的4个比特都被完整累积。
The Second Solution第二种解决方案

虽然第一种解决方法已经解决了TIME客户端遇到的问题,但是修改过的这个handler看来并不显得完美。想象一下出现一个由多个字段比如长度可变字段组成的更复杂的协议,你的这个ChannelInboundHandler的实现将立即变得不可维护。

如果你已经注意到的那样,你可以加不止一个ChannelHandlerChannelPipeline上面去,因此,你可以将一个ChannelHandler分割成多个组件以降低程序复杂度。比如,你可以把TimeClientHandler分割成两个handler:

  • 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都会调用包含一个内部的可维护的累积式的buffer的decode()方法
  3. decode()可以决定当没有足够的数据在累加buffer里的时候不添加任何可输出的数据,当有更多数据过来的时候,ByteToMessageDecoder会再次执行decode()方法。
  4. 如果decode()方法添加了一个可输出的对象,意味着decoder成功的decode了一条消息。ByteToMessageDecoder会丢弃已累加的buffer中的可读部分。请记住你不需要decode任何消息,ByteToMessageDecoder会在没有数据可输出的时候一直执行decode()方法。

现在我们有了另外一个handler插入到ChannelPipeline中,我们可以修改TimeClient中的 ChannelInitializer的实现:

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

如果你是一个富有冒险精神的人,你也许会尝试更简单的decoder方法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提供的开箱即用的decoder方法让你更轻松的实现大部分协议成为可能并且帮你避免最终写出巨大的不可维护的handler的实现

用POJO替代ByteBuf

回顾目前为止所有的例子我们均使用的是ByteBuf作为我们主要的协议消息体的数据结构。在本章,我们将改进TIME protocol客户端和服务器端的实例,用POJO来替换ByteBuf

在你的ChannelHandler内使用POJO的优点是显而易见的;通过将ByteBuf中提取信息的这部分的代码从handler中剥离出来,你的handler的扩展性和重用性会变得更好。在TIME客户端和服务器的例子里,我们仅仅读取了一条32-bit的整型数据而且它也并没有遇到使用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()));
}

升级版的decoder,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(newUnixTime());
    f.addListener(ChannelFutureListener.CLOSE);
}

现在,唯一还欠缺的就是一个encoder,作为ChannelOutboundHandler的一个实现,它将会把一个UnixTime转成一个ByteBuf。这比写一个decoder简单多了,因为当encode一条消息的时候,没有必要处理包碎片和重组。

packageio.netty.example.time;

public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    publicvoidwrite(ChannelHandlerContextctx, Objectmsg, 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在encode的数据真正发送到链路之后可以标识它成功或者失败

    第二,我们没有执行ctx.flush()。这里有一个单独的handler方法 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一样简单,它会返回一个Future,以便EventLoopGroup被完全关闭并且所有在这个群组里的通道都被关闭之后提醒你。

Summary

In this chapter, we had a quick tour of Netty with a demonstration on how to write a fully working network application on top of Netty.

There is more detailed information about Netty in the upcoming chapters. We also encourage you to review the Netty examples in the io.netty.example package.

Please also note that the community is always waiting for your questions and ideas to help you and keep improving Netty and its documentation based on your feedback.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值