Netty实战二 | Netty应用程序快速开发

免责声明:本人最近在研读《Netty实战》书籍,对于里面内容颇感兴趣,本文旨在于技术学习交流,不存在盈利性目的。

在文中, 我们将展示如何构建一个基于 Netty 的客户端和服务器。 应用程序很简单:客户端将消息发送给服务器,而服务器再将消息回送给客户端。

Netty 客户端/服务器概览

下图展示了一个将要编写的 Echo 客户端和服务器应用程序的总体结构图,本文主要的目的是编写基于 Web 的用于被浏览器访问的应用程序:

Echo 客户端和服务器

该结构支持多个客户端同时连接到一台服务器。所能够支持的客户端数量,在理论上,仅受限于系统的可用资源(以及所使用的 JDK 版本可能会施加的限制)。Echo 客户端和服务器之间的交互是非常简单的;在客户端建立一个连接之后,它会向服务
器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。虽然它本身看起来好像用处不大,但它充分地体现了客户端/服务器系统中典型的请求-响应交互模式。

编写 Echo 服务器

所有的 Netty 服务器都需要以下两部分。

  1. 至少一个 ChannelHandler—该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑
  2. 引导—这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上(相当于程序入口)。

ChannelHandler 和业务逻辑

ChannelHandler是一个接口族的父接口,它的实现负责接收并响应事件通知。在 Netty 应用程序中,所有的数据处理逻辑都包含在这些核心抽象的实现中。Echo 服务器会响应传入的消息,它需要实现 ChannelInboundHandler 接口, 用来定义响应入站事件的方法。这个简单的应用程序只需要用到少量的这些方法,所以继承 ChannelInboundHandlerAdapter 类也就足够了, 它提供了ChannelInboundHandler 的默认实现

ChannelHandler 实现类代码如下:

@Sharable //该注解表示Channel可以被多个进程共享,否则只能连接一个客户端
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override //从Channel中读取消息
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println(
            "Server received: " + in.toString(CharsetUtil.UTF_8));
        ctx.write(in); //将消息写给写给发送者(缓冲区中,而不是发送出去),而不冲刷出站消息
    }

    @Override  //读消息完成之后的事件
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) //将缓冲区中剩余消息都刷出去
            .addListener(ChannelFutureListener.CLOSE);
    }

    @Override  //异常处理方法
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

}

该业务逻辑类主要实现了三个方法:

  1. channelRead()—对于每个传入的消息都要调用;
  2. channelReadComplete()—通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息;
  3. exceptionCaught()—在读取操作期间, 有异常抛出时会调用。

ChannelInboundHandlerAdapter 有一个直观的 API,并且它的每个方法都可以被重写以挂钩到事件生命周期的恰当点上。因为需要处理所有接收到的数据,所以重写了 channelRead()方法。在这个服务器应用程序中,将数据简单地回送给了远程节点。重写 exceptionCaught()方法允许你对 Throwable 的任何子类型做出反应, 在这里记录了异常并关闭了连接。虽然一个更加完善的应用程序也许会尝试从异常中恢复,但在这个场景下,只是通过简单地关闭连接来通知远程节点发生了错误。

如果不捕获异常,会发生什么呢?

每个 Channel 都拥有一个与之相关联的 ChannelPipeline,其持有一个 ChannelHandler 的实例链。在默认的情况下, ChannelHandler 会把对它的方法的调用转发给链中的下一个 ChannelHandler。因此,如果 exceptionCaught()方法没有被该链中的某处实现,那么所接收的异常将会被传递到 ChannelPipeline 的尾端并被记录。为此,你的应用程序应该提供至少有一个实现了
exceptionCaught()方法的 ChannelHandler。

配置服务器的启动代码

服务器的启动代码相当于是应用程序的启动类,在启动前需要配置服务器的地址和端口号等信息。具体涉及以下内容:

  • 绑定到服务器将在其上监听并接受传入连接请求的端口;
  • 配置 Channel,以将有关的入站消息通知给 EchoServerHandler 实例。

启动类代码如下:

public class EchoServer {

    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println(
            "Usage: " + EchoServer.class.getSimpleName() +
            " <port>");
        }

        int port = Integer.parseInt(args[0]);
        new EchoServer(port).start();
    }


    public void start() throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup(); //1、创建EventLoopGroup
        try {
            ServerBootstrap b = new ServerBootstrap();  //2、创建ServerBootstrap
            b.group(group)
            .channel(NioServerSocketChannel.class)      //3、指定使用NIO传输Channel
            .localAddress(new InetSocketAddress(port))  //4、使用指定的端口设置套接字地址
            .childHandler(new ChannelInitializer<SocketChannel>(){
                @Override     //5、添加一个EchoServerHandler 到子Channel的ChannelPipeline
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(serverHandler); //EchoServerHandler 被标注为@Shareable,所以可以总是使用同一个的实例。
                }
            });
            ChannelFuture f = b.bind().sync(); //6、异步地绑定服务器;调用 sync()方法阻塞
等待直到绑定完成
            f.channel().closeFuture().sync(); //7、获取 Channel 的CloseFuture,并
且阻塞当前线
        } finally {
            group.shutdownGracefully().sync(); //8、阻塞式关闭
        }
    }
}

因为使用的是 NIO 传输, 所以指定了 NioEventLoopGroup 来接受和处理新的连接,并且将 Channel 的类型指定为NioServerSocketChannel 。将本地地址设置为一个具有选定端口的 InetSocketAddress 。服务器将绑定到这个地址以监听新的连接请求。

ChannelInitializer类作用:当一个新的连接被接受时,一个新的子 Channel 将会被创建,而 ChannelInitializer 将会把一个你的
EchoServerHandler 的实例(如果设置了共享,则可以重复利用一个实例)添加到该 Channel 的 ChannelPipeline 中。这个 ChannelHandler 将会收到有关入站消息的通知。

虽然 NIO 是可伸缩的, 但是其适当的尤其是关于多线程处理的配置并不简单。 Netty 的设计封装了大部分的复杂性。该示例使用了NIO,因为得益于它的可扩展性和彻底的异步性,它是目前使用最广泛的传输。但是也可以使用一个不同的传输实现。如果你想要在自己的服务器中使用 OIO 传输,将需要指定 OioServerSocketChannel 和 OioEventLoopGroup。

总结引导过程中所需要的步骤如下:

  1. 创建一个 ServerBootstrap 的实例以引导和绑定服务器;
  2. 创建并分配一个 NioEventLoopGroup 实例以进行事件的处理,如接受新连接以及读/写数据;
  3. 指定服务器绑定的本地的 InetSocketAddress;
  4. 使用一个 EchoServerHandler 的实例初始化每一个新的 Channel;
  5. 调用 ServerBootstrap.bind()方法以绑定服务器。

至此,服务器已经初始化,并且已经就绪能被使用了。

编写 Echo 客户端

Echo 客户端主要有以下几个流程:

  1. 连接到服务器;
  2. 发送一个或者多个消息;
  3. 对于每个消息,等待并接收从服务器发回的相同的消息;
  4. 关闭连接。

编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,和在服务器中的一样。

通过 ChannelHandler 实现客户端逻辑

如同服务器,客户端将拥有一个用来处理数据的 ChannelInboundHandler。在这个场景下,你将扩展 SimpleChannelInboundHandler 类以处理所有必须的任务。客户端的客户端的 ChannelHandler代码如下:

@Sharable //设置可共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    
    @Override   //当被通知 Channel是活跃的时候,发送一条消息
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    @Override  //记录已接收消息的转储
    public void channelRead(ChannelHandlerContext ctx, ByteBuf in) {
        System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
    }

    @Override  //在发生异常时,记录错误并关闭Channel
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

首先,重写了 channelActive()方法,其将在一个连接建立时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"Netty rocks!"的字节缓冲区。

接下来,重写了 channelRead0()方法。 每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。 也就是说,如果服务器发送了 5 字节, 那么不能保证这 5 字节会被一次性接收。 即使是对于这么少量的数据, channelRead0()方法也可能会被调用两次,第一次使用一个持有 3 字节的 ByteBuf( Netty 的字节容器),第二次使用一个持有 2 字节的 ByteBuf。作为一个面向流的协议, TCP 保证了字节数组将会按照服务器发送它们的顺序被接收。

重写的第三个方法是 exceptionCaught()。如同在 EchoServerHandler中所示,记录 Throwable, 关闭 Channel,在这个场景下, 终止到服务器的连接

总结ChannelHandle中使用到的方法:

  1. channelActive()——在到服务器的连接已经建立之后将被调用;
  2. channelRead0()——当从服务器接收到一条消息时被调用;
  3. exceptionCaught()——在处理过程中引发异常时被调用。

SimpleChannelInboundHandler 与 ChannelInboundHandler:

为什么我们在客户端使用的是 SimpleChannelInboundHandler,而不是在 EchoServerHandler 中所使用的ChannelInboundHandlerAdapter 呢?这和两个因素的相互作用有关:业务逻辑如何处理消息以及 Netty 如何管理资源。

在客户端,当 channelRead0()方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler 负责释放指向保存该消息的 ByteBuf 的内存引用。

在 EchoServerHandler 中,你仍然需要将传入消息回送给发送者,而 write()操作是异步的,直到 channelRead()方法返回后可能仍然没有完成。为此, EchoServerHandler扩展了 ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。消息在 EchoServerHandler 的 channelReadComplete()方法中,当 writeAndFlush()方法被调用时被释放。

引导客户端

引导客户端类似于引导服务器,不同的是, 客户端是使用主机和端口参数来连接远程地址,也就是这里的 Echo 服务器的地址,而不是绑定到一个一直被监听的端口。引导客户端代码如下所示:

public class EchoClient {

    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();  //创建 Bootstrap
            b.group(group)
            .channel(NioSocketChannel.class) //指定 EventLoopGroup 以处理客户端事件;需要适用于 NIO 的实现。适用于 NIO 传输的Channel 类型
            .remoteAddress(new InetSocketAddress(host, port)) //设置服务器的InetSocketAddress
            .handler(new ChannelInitializer<SocketChannel>() { //在创建Channel时,向 ChannelPipeline中添加一个 EchoClientHandler 实例
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new EchoClientHandler());
                }
            });
            ChannelFuture f = b.connect().sync(); //连接到远程节点, 阻塞等待直到连接完成
            f.channel().closeFuture().sync(); //阻塞, 直到Channel 关闭
        } finally {
        group.shutdownGracefully().sync(); //关闭线程池并且释放所有的资源
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("Usage: " + EchoClient.class.getSimpleName() +
            " <host> <port>");
        return;
        }

        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

在这里,使用了 NIO 传输。注意,也可以在客户端和服务器上分别使用不同的传输。例如,在服务器端使用 NIO 传输,而在客户端使用 OIO 传输。

总结该部分的步骤:

  1. 为初始化客户端, 创建了一个 Bootstrap 实例;
  2. 为进行事件处理分配了一个 NioEventLoopGroup 实例, 其中事件处理包括创建新的连接以及处理入站和出站数据;
  3. 为服务器连接创建了一个 InetSocketAddress 实例;
  4. 当连接被建立时,一个 EchoClientHandler 实例会被安装到(该 Channel 的)ChannelPipeline 中;
  5. 在一切都设置完成后,调用 Bootstrap.connect()方法连接到远程节点。

运行结果

同时启动服务端和客户端,在客户端的控制台中将显示:

客户端运行结果

在服务端将显示:

服务端显示结果

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值