Netty 4.x 用户指南

该文翻译自Netty的官网文章,查看英文原文请点这里;文章正在翻译中,希望能帮助到你,感谢支持!

前言

问题

当今,我们使用一般性目的的应用或库来相互交流。例如,我们经常使用HTTP客户端库来从服务器端获取信息以及借助网页服务调用一个远程的过程。当然,一个一般目的的协议或者它的实现在有些时候并不能很好地“缩放”。这就像我们通常不会使用一般目的的HTTP服务器来交换大文件,邮件消息以及像金融信息、多玩家游戏的数据这样接近实时的消息。这需要专用于特殊目的而高度优化的协议实现。举个例子,你可能想实现一个HTTP服务器,它是为AJAX-based聊天应用、媒体流或者大文件传输而最佳优化的。你甚至想实现一个全新的非常精确地适用于你的需求的协议。另一种不可避免的情况是当你必须处理旧系统专有的协议来确保和旧系统的互操作性。在这种情况中的问题是我们该如何快速地实现那样的协议而不牺牲已有应用的稳定性和性能。


解决方案

Netty项目是为提供一个异步的事件驱动的网络应用框架和快速开发可维护的高性能高缩放性协议服务器和客户端的工具而做出的努力。

换句话说,Netty是一个使协议服务器和客户端的开发变得快速且容易的NIO客户端服务器网络框架。它很好地简化了诸如TCP和UDP等socket server开发中的网络编程。

“快速且容易”并不意味着生成的应用的程序将遭受一个可维护性或性能问题。Netty借助诸如FTP、SMTP、HTTP以及各种基于二进制和文本的旧有的协议的实现中获得的经验来仔细设计。最终,Netty成功地找到了不用任何折中就能实现开发、性能、稳定性、以及灵活性上自在的方法。

一些用户可能已经发现了一些其他的声称具有相同的优势的网络应用框架,你可能想问是什么让Netty如此地与众不同?答案是它所依赖的哲学。Netty旨在给你带来API联系和第一天的实现中就有的最舒适的体验。它不是有形的东西。你会意识到,这种哲学将使你阅读本指南和与Netty玩耍的生活更加容易。


着手开发

在开始之前

运行在这个章节中介绍的示例的最低要求只有两个:要用最新版本的Netty且JDK版本要在1.6及以上。最新版本的Netty在这个页面也可以找到。要下载版本正确的JDK,请参考你的喜欢的JDK的提供商网站。

就如你所读到的,你可能对这个章节介绍到的类有很多的疑问,你可以在任何你想有更多了解的时候参考API指南的文档。为了方便你,在这个文档中提及的所有类都链接到在线的API指南。同时,如果有任何语法或打印排版方面不正确的信息或错误,或者你有一个可以更好地改善文档的主意,请及时联系Netty项目团队以让我们知道。

写一个丢弃服务器

世界上最简单的协议不是“Hello world”而是“DISCARD”。这个协议对它接收到的数据不做任何响应,而是直接丢弃。

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. DiscardServerHandler 继承自 ChannelInboundHandlerAdapter,它实现了ChannelInboundHandler接口。ChannelInboundHandler接口提供多种事件处理方法,在使用时你可以重写这些方法。当前,继承
    ChannelInboundHandlerAdapter就已经足够了,而无需自己实现handler接口。

  2. 在这里我们重写了channelRead()事件处理方法,每当从客户端接收到新数据,就会调用这个方法,调用时带有接收到的消息,在这个实例中,接收到的消息的类型是Bytebuf

  3. 为了实现DISCARD协议,handler必须忽略接收到的消息。Bytebuf是一个引用计数的对象,必须通过调用release()方法明确地释放它。请记住,释放任何传入到handler内的引用计数的对象是handler的责任。channelRead()方法通常实现如下:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // Do something with msg
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
  4. 当一个异常由于I/O错误或由于Handler的实现在处理事件过程中抛出了异常而举起时,exceptionCaught()事件处理方法会被调用,调用时会传入相应的Throwable对象。在大多数情况下,捕获的异常应当被记录在日志中并且与之关联的channel都应当在这里关闭,即便这个方法的实现可能因你处理异常的方式的不同而不同,比如,你可能想在关闭连接之前发送带有错误码的响应。

截止目前,已经相当不错了!我们已经实现了DISCARD服务器的一半。现在剩下的是编写启动带有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;

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

            // 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为不同种类的传输提供多种实现。在示例中,我们在实现一个服务侧的应用,因此,将使用两个EventLoopGroup。第一个,通常叫做“老板”,它接收到来的连接。第二个,通常叫做“工人”,一旦老板接受一个连接并将这个被接受的连接注册到工人,它就来处理这个被接受的连接的流量。多少线程被使用以及如何将他们映射到创建的channel决定于EventLoopGroup的实现甚至是可通过构造器来配置的。

  2. ServerBootstrap是一个用于建立服务器的辅助类。你可以使用Channel直接创建一个服务器。不过,请注意那是一个单调乏味的过程,大多数情况下,你没必要那么做。

  3. 这里,我们指定使用NioServerSocketChannel类来实例化一个新的Channel,让该Channel来接受到来的连接。

  4. 指定在这里的handler将总是被新接受的连接“评估”。ChannelInitializer是一个特殊的handler,用于帮助用户配置新的Channel。最有可能你想通过添加一些诸如DiscardServerHandler这样的处理器来配置新一个新Channel的ChannelPipeline,从而实现你的应用。随着应用变得复杂,很有可能你将会对pipeline添加更过的handlers并最终将这个匿名类抽取到一个顶级的类中。

  5. 你可以设置一些对Channel的实现有特殊作用的参数。我们在写一个TCP/IP服务器,因此,我们可以设置诸如tcpNoDelay和keepAlive这样的socket选项。请参考ChannelOption的API文档和指定的ChannelConfig实现来获得关于所有支持的ChannelOptions的概览。

  6. 你有没注意到option()和childOption()?option()针对专门负责接收到来的连接的NioServerSocketChannel。而childOption()是针对那些被父类ServerChannel所接受的Channels。在这种情况下,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协议来学习如何写一个响应到客户端,在该协议中,任何接收到的数据都会被回送回去。

相比在前一部分我们实现的丢弃服务器,唯一的不同是它将接收到的数据重新发送回去而不是打印到控制台中。因此,再次修改下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()将其冲洗到电线上。

如果你再次运行telnet命令,你讲看到服务器将你发个他的内容发送了回来。

这个回送服务器的全部源代码位于发布的 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. 和往常一样,我们写入结构化的数据。

    等等,flip在哪里?我们不是习惯于在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()将会返回这个对象,并且在写操作完成时它会通知它的事件监听器。

  4. 我们如何得到一个写请求完成后的通知?这和添加一个ChannelFutureListener监听器到返回的ChannelFuture一样简单。这里我们新创建一个匿名的ChannelFutureListener,当操作完成后由它关闭Channel。

    二选一的,你可以通过使用一个预定义的监听器来简化代码。
    f.addListener(ChannelFutureListener.CLOSE);

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

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

这里的<port> 是你在main()方法中指定的端口号而<host>通常为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. BootstrapServerBootstrap是很相似的,除了它是用于非服务器channel的,比如客户侧或无连接的channel。

  2. 如果你只指定唯一的EventLoopGroup,那它将既是一个老板group,也是一个工人组。尽管在客户侧老板group是没有用的。

  3. NioSocketChannel被用来创建一个客户侧的Channel而不是用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();
    }
}

处理基于流的传输

……

用POJO交流而非ByteBuf

……

关闭你的应用

……

总结

……

猜你喜欢

转载自blog.csdn.net/YSSJZ960427031/article/details/53064226
今日推荐