netty(1)

问题:

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

解决方案:

Netty项目致力于提供一个异步事件驱动的网络应用程序框架和工具,用于快速开发可维护的高性能和高可扩展性协议服务器和客户端。

换句话说,Netty是一个NIO客户机-服务器框架,它支持快速、轻松地开发网络应用程序,如协议服务器和客户机。它极大地简化了网络编程,如TCP和UDP套接字服务器开发。

“快速简便”并不意味着生成的应用程序将受到可维护性或性能问题的影响。Netty经过精心设计,从FTP、SMTP、HTTP以及各种二进制和基于文本的遗留协议的实现中获得了经验。因此,Netty成功地找到了一种不妥协地实现易开发性、性能、稳定性和灵活性的方法。

一些用户可能已经发现了其他声称具有相同优势的网络应用程序框架,我们可能想问是什么让Netty与他们如此不同。答案是它所建立的哲学。Netty旨在从第一天起就为我们提供最舒适的API和实现体验。这不是什么有形的东西,但我们会意识到,当我们阅读本指南并与Netty玩耍时,这种哲学会让我们的生活更轻松。

开始:

本章通过简单的示例介绍Netty的核心结构,让我们快速入门。在本章结束时,我们将能够立即在Netty上编写客户端和服务器。

开始之前:

运行本章示例的最低要求只有两个;Netty和JDK 1.6或更高版本的最新版本。Netty的最新版本可在项目下载页面中获得。

写个Discard服务器

世界上最简单的协议不是“hello,world!”,而是Discard。这是一个协议,在没有任何响应的情况下丢弃任何接收到的数据。

要实现丢弃协议,唯一需要做的就是忽略所有接收到的数据。让我们直接从处理程序实现开始,它处理Netty生成的I/O事件。

package io.netty.example.discard;

import io.netty.buffer.ByteBuf;

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

/**
 * 处理服务器端通道
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // 以静默方式丢弃接收到的数据
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // 引发异常时关闭连接
        cause.printStackTrace();
        ctx.close();
    }
}

1、DiscardServerHandler扩展了ChannelInboundHandlerAdapter,它是ChannelInboundHandler的一个实现。ChannelInboundHandler提供了各种可以重写的事件处理程序方法。现在,扩展ChannelInboundHandlerAdapter而不是自己实现处理程序接口就足够了。

2、我们在这里重写channelRead()事件处理程序方法。每当从客户机接收到新数据时,就会使用接收到的消息调用此方法。在本例中,接收到的消息的类型为ByteBuf。

3、要实现丢弃协议,处理程序必须忽略接收到的消息。ByteBuf是一个引用计数对象,必须通过release()方法显式释放。请记住,处理程序有责任释放传递给处理程序的任何引用计数对象。通常,channelRead()处理程序方法的实现方式如下:

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

4、当Netty因I/O错误或处理程序实现因处理事件时引发的异常而引发异常时,exceptionCaught()事件处理程序方法将使用Throwable调用。在大多数情况下,应记录捕获的异常,并在此处关闭其关联的通道,尽管此方法的实现可能会有所不同,具体取决于我们要如何处理异常情况。例如,我们可能希望在关闭连接之前发送带有错误代码的响应消息。

到现在为止,我们已经实现了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;
    
/**
 * 丢弃任何传入数据
 */
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)
    
            // 等待服务器套接字关闭
            // 在本例中,这种情况不会发生,但我们可以优雅地这样做
            // 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。第一个通常被称为“boss”,接受传入连接。第二个,通常称为“worker”,在boss接受连接并向worker注册接受的连接后,处理接受连接的通信量。使用多少线程以及它们如何映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。

2、ServerBootstrap是一个帮助器类,用于设置服务器。我们可以直接使用通道设置服务器。但是,请注意,这是一个乏味的过程,在大多数情况下我们不需要这样做。

3、这里,我们指定使用NioServerSocketChannel类,该类用于实例化新通道以接受传入连接。

4、此处指定的处理程序将始终由新接受的通道进行评估。ChannelInitializer是一个特殊的处理程序,用于帮助用户配置新通道。我们很可能希望通过添加一些处理程序(如DiscardServerHandler)来配置新通道的ChannelPipeline,以实现网络应用程序。随着应用程序变得复杂,我们可能会向管道中添加更多处理程序,并最终将该匿名类提取到顶级类中。

5、我们还可以设置特定于通道实现的参数。我们正在编写一个TCP/IP服务器,因此可以设置套接字选项,如tcpNoDelay和keepAlive。

6、注意到option()和childOption()了吗?option()用于接受传入连接的NioServerSocketChannel。childOption()用于父服务器通道接受的通道,在本例中为NioSocketChannel。

7、剩下的就是绑定到端口并启动服务器。这里,我们绑定到机器中所有NIC(网络接口卡)的端口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命令,我们将看到服务器打印它接收到的内容。

编写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示例中不同,我们没有释放收到的消息。这是因为当它被写在wire上时,Netty会为我们释放它。

2、ctx.write(Object)不会将消息写入到wire。它在内部缓冲,然后通过ctx.flush()来刷新到导线上。为简洁起见,我们可以使用ctx.writeAndFlush(msg)。

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

编写时间服务器

本节中要实现的协议是时间协议。它与前面的示例不同,它发送一条包含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 via  ,并分配一个新的缓冲区。

3、像往常一样,我们编写构造的消息。

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

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

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

4、当写请求完成时,我们如何得到通知?这是简单的,在返回ChannelFuture添加一个ChannelFutureListener。在这里,我们创建了一个新的匿名ChannelFutureListener,它在操作完成时关闭通道。

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

f.addListener(ChannelFutureListener.CLOSE);

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

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

其中,<port>是在main()方法中指定的端口号,<host>通常是localhost。

编写时间Client

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

Netty中服务器和客户端之间最大也是唯一的区别是使用了不同的引导和通道实现。请查看以下代码:

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)

            // 等待连接关闭。
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

1、Bootstrap与ServerBootstrap类似,只是它适用于非服务器通道,如客户端或无连接通道。

2、如果只指定一个EventLoopGroup,则它将同时用作boss组和worker组。不过,boss-worker并不用于客户端。

3、NioSocketChannel被用来创建客户端通道,而不是NioServerSocketChannel。

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。我们将在netty(2)讨论为什么会发生这种情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值