Java NIO-Netty4.x入门

4 篇文章 0 订阅

原文:http://netty.io/wiki/user-guide-for-4.x.html

0.引言

问题

现在我们使用通用的应用程序或库来进行通信。例如,我们经常使用HTTP客户端库从Web服务器检索信息,并通过Web服务调用远程过程调用(RPC)。然而,一个通用的协议或其实现有时并不能很方便的进行扩展。比如,我们不会使用通用HTTP服务器来完成传输体积很大的文件、电子邮件和实时通信,例如财务信息和多人游戏数据。有时我们需要的是一个专门用于特殊目的,高度专用的协议实现。例如,你可能需要实现为基于Ajax的聊天应用程序、媒体流或大文件传输等功能专门优化的HTTP服务器。你甚至可以设计并实现一个完全符合你的需求的新协议。另一个不可避免的情况是你有时可能必须处理遗留的专用协议确保与旧系统的互操作性;在这种情况下重要的是我们可以在不牺牲所得到的应用程序的稳定性和性能的情况下如何快速实现该协议。

解决方案

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

换句话说,Netty是一个NIO客户机-服务器框架,它能够快速方便地开发网络应用程序,例如协议服务器和客户端。它极大地简化和简化了网络编程,如TCP和UDP socket服务器的开发。
“快速简便”并不意味着所得到的应用程序将存在可维护性或性能问题的影响。Netty是经过精心设计的,从实现许多协议(如FTP、SMTP、HTTP和各种基于二进制和基于文本的遗留协议)获得的经验。因此,Netty已经成功地找到了一种不妥协的方式来实现开发、性能、稳定性和灵活性。
有些用户可能已经找到了声称拥有相同优势的其他网络应用程序框架,你可能想问一下为什么Netty与它们有什么不同。答案是建立在Netty之上的设计哲学。Netty可以为你提供最舒适的体验,无论是从API还是从第一天开始的实现。这不是有形的,但你会意识到,这一哲学将使你的生活更容易,因为你读过这篇指南,并开始使用Netty。

1.Netty入门

本文以简单的例子围绕Netty的核心构造,让你快速入门Netty。当你在本文的结尾时,你将能够基于Netty实现一个属于自己的客户端和一个服务器。

准备工作

在本文中运行示例的最低要求只有两个:Netty和JDK 1.6或以上的最新版本。Netty的最新版本在项目下载页面中可用。若要下载JDK的正确版本,请参阅您首选的JDK供应商的网站。

下载页面:http://netty.io/downloads.html

当你阅读时,你可能会对本文所介绍的内容有更多的疑问。当您想了解更多API时,请参阅API引用。为了方便起见,此文档中的所有类名链接到联机API引用。

1.1.写一个“丢弃消息的服务器”

世界上最简单的协议就是“Hello World!”然后放弃。
这里我们实现一个丢弃任何接收的数据而没有任何响应的协议。

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

package com.mcy.netty.discard;

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

/**
* 写一个类继承 ChannelInboundHandlerAdapter 处理服务端的 channel
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter{

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        // 丢掉所有数据
        ((ByteBuf)msg).release();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        // 处理异常
        cause.printStackTrace();
        ctx.close();
    }
}
  1. DiscardServerHandler 继承了 ChannelInboundHandlerAdapter类,ChannelInboundHandlerAdapter提供了可以覆写的各种事件处理方法,目前只需要继承ChannelInboundHandlerAdapter即可,不必实现处理接口。
  2. 我们这里重写了channelRead()方法,每当服务器从客户端接收到新的数据时,该方法就会被调用,例子中接收到的消息类型是ByteBuf.
  3. 为了实现“丢弃”协议,处理程序忽略了接收到的消息。ByteBuf是一个引用计数对象,必须通过显式调用release()方法进行释放。需要注意的是:释放接收的引用计数对象是每个处理器的需要完成的责任。通常,channelRead()的实现如下:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

    // 丢掉所有数据
    // ((ByteBuf)msg).release();
    try {
        // do something
    }catch (Exception e){
        // do something
    }finally {
        ReferenceCountUtil.release(msg);
    }
}
  • exceptionCaught()方法用户处理由于Netty执行处理器的实现方法时可能引起的I/O错误而抛出的异常。在大多数情况下,捕获的异常应该被记录,并且相关的通道应该在这里进行关闭,尽管这种方法的实现可以根据你处理异常的策略有所不同。例如,你可能希望在关闭连接之前发出一个带有错误代码的响应消息。

到现在为止,一直都还不错。我们已经实现了“丢弃服务器“的上半部分。现在剩下的是写main()方法,用它来启动一个使用DiscardServerHandler 处理消息的服务器。

public class DiscardServer {

    private int port;

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

    public void run() throws Exception{

        // 1.
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            // 2.
            ServerBootstrap bootstrap = new ServerBootstrap();

            bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class) // 3.
                .childHandler(new ChannelInitializer<SocketChannel>() { // 4.
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                            channel.pipeline().addLast(new DiscardServerHandler());
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128) // 5.
                .childOption(ChannelOption.SO_KEEPALIVE, true); // 6.

              // 绑定端口并启动服务器接收链接
              ChannelFuture future = bootstrap.bind(port).sync();

              // 等待直到服务器socket关闭,这里不会发生,但你可以使用这个方法优雅的关闭服务器
              future.channel().closeFuture().sync();

          }finally {
              workGroup.shutdownGracefully();
              bossGroup.shutdownGracefully();
          }
     }

    public static void main(String[] args) throws Exception{
            int port = 8080;
            new DiscardServer(port).run();
    }
}
  1. NioEventLoopGroup是一个处理I/O操作的多线程事件循环。Netty为不同的数据传输提供了大量的NioEventLoopGroup实现。这里实现两个NioEventLoopGroup对象实现了一个服务端应用。第一个通常被称为“boss”,用于接受请求的链接;第二个通常被称为“worker”用于处理链接传入的数据,一旦boss接到链接就会将接受的链接注册到worker。使用多少线程以及它们如何映射到创建的channel取决于NioEventLoopGroup的具体实现,甚至可以通过构造器来配置。
  2. ServerBootstrap一个建立服务器的辅助类。可以直接使用channel设置服务器。但是,请注意,这是一个乏味的过程,在大多数情况下,你不需要这么做。
  3. 这个例子中我们特别指定使用NioServerSocketChannel类来实例化channel接受传入的链接。
  4. 这里指定的处理程序将始终由新接受的channel使用。信道初始化器是专门用来帮助用户配置一个新的channel特殊处理程序。最有可能的是,通过添加一些处理程序(如DiscardServerHandler处理程序)来实现新的channel的pipeline,以实现网络应用程序。随着应用程序变得复杂,您可能会向pipeline中添加更多的处理程序handler,并最终将此匿名类提取到顶级类中。
  5. 还可以设置特定于channel实现的参数。我们正在编写TCP/IP服务器,因此我们可以设置套接字选项,如tcpNoDelay 和keepAlive。请参考Chhanne选项的接口文档和具体的通道配置实现,以获得关于所支持的channel选项的概述。
  6. 我们注意到有option()方法和childOption()两个方法,option()用于设置服务器接收到的链接的NioServerSocketChannel,childOption()用于设置父ServerChannel(这个例子中为NioServerSocketChannel)接收的channel。
  7. 我们现在可以准备出发了。剩下的就是绑定到端口并启动服务器。这里,我们绑定到机器中所有NIC(网络接口卡)的端口8080。现在你可以按你想要的方式调用bind()方法(使用不同的绑定地址)。
    现在你的第一个基于Netty的服务器已经完成了。

1.2.查看接收到的数据

现在我们已经写了我们第一个服务器,我们需要测试一下它是否工作,最简单的方法就是使用telnet命令,例如我们可以输入“telnet localhost 8080”并且输入一些内容。
但是,我们并不知道它是否工作的很好,因为刚刚我们写的是一个“丢弃服务器”,我们不会得到任何反馈。为了证明它确实在工作,我们需要修改一下服务器的实现,让它打印出接收到的数据。

@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);  
    }
}
  1. 这个低效的循环,实际上可以简化为
    System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
    当你再次调用telnet命令的时候,就可以看到服务器输出接收到的内容了。

1.3.写一个“回声服务器”

现在我们已经消费了接收的数据,但是没有给出任何反馈。然而,一个服务器通常需要支持响应请求。现在通过实现一个“回声协议”(将接受的任何数据都发送回去)学习如何给客户端写入响应消息。
与之前我们实现的“丢弃服务器”唯一的不同就是我们需要将接收到的数据发送回去,因此我们再次修改channelRead()方法:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ctx.write(msg); 
    ctx.flush();  
}
  1. ChannelHandlerContext提供了多种操作,使你可以出发各种I/O事件和操作,例子中我们调用write()方法逐字写入接收到的消息。需要注意的是这里我们没有释放接收的消息,那是应为当消息被写入反馈的时候Netty会自动释放它。
  2. write()方法并不能保证消息别写入通道,它只是在内部被缓存,需要调用flush()方法完成写入,也可以调用writeAndFlush()方法。

1.4.写一个“时钟服务器”

这个部分我们需要实现一个“时钟协议”;与前面的例子不同,它会发送一个32位的整数消息,不需要接收任何请求,而且一旦消息发送就关闭连接,这里例子中我们将学习如何构造并发送一个消息。
因为我们将忽略任何接收的数据,只需要在链接建立后尽快的发送一个消息,我们无需重写channelRead()方法。这里我们将重写channelActive()方法:

public class TimeServerHandler extends ChannelInboundHandlerAdapter{

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int)(System.currentTimeMillis()/1000L + 2208988800L));

        final ChannelFuture future = ctx.writeAndFlush(time);
        future.addListener( channelFuture -> {
            assert future == channelFuture;
            ctx.close();
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        cause.printStackTrace();
        ctx.close();
    }
}
  1. 如上所说一样,channelActive()会在链接建立并准备好进行数据传输时被调用,在这个方法中我们写入一个32位整数代表当前时间。
  2. 为了发送一个消息,我们需要申请一个容纳消息的缓存。我们将写入一个32bit的整数,所以我们需要一个至少包含4个字节的ByteBuf。使用ChannelHandlerContext.alloc()方法可以获得当前 ByteBufAllocator,然后使用它申请一个新的buffer。
  3. 通常情况下,我们写入的是结构化的消息。这里你可能会问flip()去哪了?在我们使用NIO发送消息之前不是要先调用 java.nio.ByteBuffer.flip()的方法么?Netty中的ByteBuf不需要这个方法,因为它有两个指针,一个用于读操作,一个用于写操作。当写入数据到ByteBuf中时,写指针索引增加,但是读指针的索引不变。读指针和写指针代表着消息的数据当前的起点和终点。相反,NIO中缓冲区不提供一种整洁的方式来明确消息内容在哪里开始和结束,而不得不调用flip翻转方法。当你忘记调用flip方法的时候,会遇到麻烦,可能没有数据或者不正确的数据被发送。而在Netty中不会发证这样的错误,因为Netty对于不同的操作使用不同的指针,这样我们的工作变得更简单,一个没有flip翻转的日子。
  4. 另外需要说明的一点是: ChannelHandlerContext.write() (和writeAndFlush())方法返回一个 ChannelFuture对象。一个 ChannelFuture对象意味着一个可能还没有发生的I/O操作,因为在Netty中所有的操作都是异步的。比如下边的而操作可能在消息发送之前就触发链接的关闭事件。因此我们需要在ChannelFuture完成之后再调用close()方法,我们可以为返回的ChannelFuture对象添加监听事件来达到这个目的。另外需要注意的是,close()也并不会立即关闭连接,也会返回一个ChannelFuture对象。
Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
  1. 我们需要调用ChannelFutrue对象的addListener方法为操作添加回调监听。
    我们像“丢弃服务器”一样再写一个启动服务器的主方法,这里我们的时钟服务器就完成了。
public class TimeServer {

    private int port;

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

    public void run() throws Exception{

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap bootstrap = new ServerBootstrap();

            bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class) // 3.
                .childHandler(new ChannelInitializer<SocketChannel>() { // 4.
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        channel.pipeline().addLast(new TimeServerHandler());
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128) // 5.
                .childOption(ChannelOption.SO_KEEPALIVE, true); // 6.

            // 绑定端口并启动服务器接收链接
            ChannelFuture future = bootstrap.bind(port).sync();

        }   finally {
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception{
        int port = 80081;
        new TimeServer(port).run();
    }
}

我们可以调用Unix系统的rdate命令来测试我们的“时钟服务器”。port是main方法中设置的port端口,host通常是localhost.

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

1.5.写一个时钟客户端

和“丢弃服务器”不一样的是,我们需要一个“时钟协议”的客户端,因为人类无法将32bit的二进制数据转换为日历上的日期。这一部分我们将讨论如何确保服务器正确的工作,并学习使用Netty写一个客户端。
在Netty中客户端(client)和服务端(server)最大的区别就是使用不同Bootstrap和Channel实现,我们可以看下边的代码:

public class TimeClient {

    public static void main(String[] args) throws InterruptedException {

        String host = args[0];
        int port = Integer.parseInt(args[1]);

        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(workerGroup);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(new TimeClientHandler());
                }
            });

            // 启动客户端
            ChannelFuture future = bootstrap.connect(host, port).sync();

            future.channel().closeFuture().sync();
        }finally {
            workerGroup.shutdownGracefully();
        }
    }
}
  1. Bootstrap和ServerBootstrap相似,除了它是用于无服务器Channel的,例如客户端或者无连接的channel。
  2. 如果你只指定了一个EventLoopGroup,那么它的角色既是boss也是worker。在客户端是不区分boss和worker的。
  3. 在客户端使用 NioSocketChannel代替 NioServerSocketChannel来创建channel实例。
  4. 不像服务端一样,这里我们没有添加childOption()方法,因为客户端的SocketChannel不会有一个父通道。
  5. 启动客户端时,使用了connect()方法代替bind()方法。

整体上来说客户端配置和启动的方法和服务端的代码没有太大的区别。那客户端的ChannelHandler应该如何实现呢?它应该能从服务端接收一个32bit的整数,并将它转化为一个人类能理解的日期格式,打印出日期,然后关闭连接。

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;
        try {
            long time = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(time));
            ctx.close();
        }finally {
            buf.release();
        }
    }    

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  1. 在TCP/IP协议中,Netty从对应连接中读取发送的数据到ByteBuf中。

这个例子看起来非常简单,和服务端例子并没有什么区别,但是这个handler又是可能会拒绝工作引起 IndexOutOfBoundsException异常,下面我们将讨论为什么会发生这样的问题。

1.6.处理基于流的数据传输

Socket Buffer的一个小注意事项

在TCP/IP协议等基于流的传输协议中,接收道德数据被保存在一个socket接收缓存中。不幸的是,在基于流的传输中缓存中并不是一个数据包的队列,而是一个字节队列。这几意味着,即使你发送作为两个独立的包发送两个消息,操作系统也不会将它们视为两个包,只会将它们当做一串字节。因此,你没办法保证所读取的内容就是远端写入的内容。例如;我们假设操作系统的TCP/IP栈已经接收了三个数据包:
这里写图片描述

由于基于流的协议的普遍属性,这里有极大的几率在你的应用中读到的数据片段是这样的:
这里写图片描述
因此,无论是客户端还是服务端都需要在接收数据的时候对接受到的数据进行整理,将其转化为更有意义的,可以被你的应用程序逻辑理解的数据帧。在上边的例子中,接收到的数据应该被整理为:
这里写图片描述

第一种解决方案

现在我们仍以“时钟服务器为例”,虽然一个32bit的整数是一个很小的数据,通常也不会被分隔,但同样存在上边讲的问题。因为,它是可能会被碎片化的,随着流量的增加,出现碎片化问题的风险就越来越大。
一个简单的解决方案就是创建一个内部计数的缓冲区buffer,它会等待直到接收到4个字节(32bit)的数据。下边的代码是修改后的TimeClientHandler:

public class TimeClientHandler1 extends ChannelInboundHandlerAdapter {
    private ByteBuf buffer;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

        buffer = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        buffer.release();
        buffer = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;

        buffer.writeBytes(buf);
        buf.release();

        if(buffer.readableBytes() >= 4) {
            long time = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(time));
            ctx.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        cause.printStackTrace();
        ctx.close();
    }
}
  1. ChannelHandler生命周期中有两个监听方法:handlerAdd()和handlerRemove(),只要不是会阻塞很长时间的初始化,就可以在这里执行任意的初始化工作。
  2. 这里将所有接受到的数据积累到一个buffer中;handler必须检查buffer是否有足够的数据(这里是4个字节),然后执行相关业务逻辑。否则会再次调用channelRead()方法直到更多的数据到达,最终累计到4个字节。

第二种解决方案

虽然第一种解决方案已经解决了“时钟客户端”的问题,但修改后的代码看起来并不优雅。想象一下有一个更复杂的协议,它由很多的字节组成,例如可变长字段。第一种方案的实现很快会变得无法维护。
也许你已经注意到了,我们可以在ChannelPipeline中添加多个ChannelHandler,因此你可以将一个单handler的处理程序拆分为多模块的处理流程来减少应用的复杂性。例如;我们可以将TimeClientHandler拆分为两个部分:

  • TimeDecoder用来处理数据碎片的问题;
  • TimeClientHandler保持原来简单的版本。

  • 幸运的是,Netty提供了一个可扩展的类来帮助我们来实现TimeDecoder:
public class TimeDecoder extends ByteToMessageDecoder{
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if(byteBuf.readableBytes() < 4){
            return;
        }
        list.add(byteBuf.readBytes(4));
    }
}
  1. ByteToMessageDecoder是一个ChannelInboundHandler的实现类,使我们处理碎片化问题变得更简单;
  2. ByteToMessageDecoder当接收到新的数据就会调用decode()方法,并将内部维护的一个累积缓冲区buffer传入方法;
  3. decode()方法来判断当数据不够的时候不向输出的列表list添加对象,等待接收新的数据调用decode()方法;
  4. 当decode()方法向list添加一个对象的时候说明已经接收到足够的数据,并成功解析为一个消息。ByteToMessageDecoder会丢弃缓冲区中已读的部分数据。需要注意的是,你不需要解析多条消息,ByteToMessageDecoder会持续的调用decode()方法的,直到没有可解析的消息添加到列表。
    然后,我们需要将新的handler添加到ChannelPipeline管道中。我们需要修改TimeClient中ChannelInitializer的实现。
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

如果你是个喜欢冒险的人,你可能想试试更加简化的Decoder实现ReplayingDecoder,你只需要像下边一样实现:

public class TimeDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
        ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

此外,Netty还提供了可以很容易实现大多数协议的开箱即用的解码器,避免你使用单一handler让程序变得不可维护。使用他们可以参考以下包获得更详细的示例:

  • io.netty.example.factorial 用于二进制协议
  • io.netty.example.telnet 用于基于文本的协议

1.7.用实体类代替ByteBuf

到目前为止,我们所有的例子都是使用ByteBuf作为协议消息的主要数据结构,接下来我们将使用简单Java对象(POJO)而不是ByteBuf来改进“时间客户端”和“时间服务器”。
在ChannelHandler中使用POJO的好处是很明显的;通过将ByteBuf中提取信息的代码分离出来,你的handler变得更加容易维护和重用。在“时间服务器”的示例中,我们只读取了一个32bit的整数,看起来使用ByteBuf并没有什么问题,但当你实现一个真实的协议的时候,你会发现这样的分离是非常必要的。
首先,我们定义一个时间类型叫做UnixTime

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

public class TimeDecoder extends ByteToMessageDecoder{

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if(byteBuf.readableBytes() < 4){
            return;
        }
        // list.add(byteBuf.readBytes(4));
        list.add(new UnixTime(byteBuf.readUnsignedInt()));
    }
}

修改解码器后,我们在TimeClientHandler中就不再需要使用ByteBuf了:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

    // ByteBuf buf = (ByteBuf) msg;
    UnixTime time = (UnixTime)msg;

    System.out.println(time);
    ctx.close();
}

是不是变得更简单和优雅了,在服务端我们也同样可以做同样的修改,首先修改TimeServerHandler:

@Override
public void channelActive(ChannelHandlerContext ctx) {
        ChannelFuture f = ctx.writeAndFlush(new UnixTime());
        f.addListener(ChannelFutureListener.CLOSE);
}

现在我们只缺一个编码器了,它需要实现 ChannelOutboundHandler将UnixTime对象转化为一个ByteBuf,这比解码器要简单很多,因为编码的时候不需要处理碎片化问题。

public class TimeEncoder extends ChannelOutboundHandlerAdapter{

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        UnixTime time = (UnixTime)msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int) time.value());
        ctx.write(encoded,  promise);
    }
}

需要注意两个问题:
1. 我们通过promise参数原始ChannelPromise,这样当编码的数据实际写入链接时,Netty会标记成功或失败;
2. 这里没有调用ctx.flush()方法,因为有一个单独的方法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());
    }
}

最后就是要把TimeEncoder添加到 TimeServerHandler的ChannelPipeline中了。

1.8.关闭应用

关闭Netty应用,只需要通过 shutdownGracefully()关闭所有创建的 EventLoopGroup,它会返回一个Futuer当所有EventLoopGroup完全停止后会发出通知。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2023-07-13 09:15:56,872 WARN org.apache.flink.runtime.dispatcher.DispatcherRestEndpoint [] - Unhandled exception java.io.IOException: Connection reset by peer at sun.nio.ch.FileDispatcherImpl.read0(Native Method) ~[?:1.8.0_372] at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39) ~[?:1.8.0_372] at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223) ~[?:1.8.0_372] at sun.nio.ch.IOUtil.read(IOUtil.java:192) ~[?:1.8.0_372] at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:379) ~[?:1.8.0_372] at org.apache.flink.shaded.netty4.io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:253) ~[flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1132) ~[flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:350) ~[flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:151) [flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719) [flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655) [flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581) [flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) [flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) [flink-dist-1.15.3.jar:1.15.3] at org.apache.flink.shaded.netty4.io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [flink-dist-1.15.3.jar:1.15.3] at java.lang.Thread.run(Thread.java:750) [?:1.8.0_372]
07-14

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值