• w3cschoolNetty 实战精髓篇1


    https://www.w3cschool.cn/essential_netty_in_action/

    Netty 异步和数据驱动

    2021-04-22 14:57 更新

    在学习Netty的异步和数据驱动之前,我们先来简单的了解一下Netty

    Netty是什么

    Netty 是一个利用 Java 的高级网络的能力,隐藏了Java背后的复杂性然后提供了一个易于使用的 API 的客户端/服务器框架。Netty 的高性能和可扩展性,可以作为你自己的独特的应用,让你更用心的花时间在你有兴趣的东西上。

    Netty处理一些高并发的网络问题的时候会体现出怎样的价值呢?想知道的话,答案就在本章中。本章除了解释上述问题,还将为大家讲解Netty的基本概念以及Netty的工具包的构成成分。

    Netty的历史

    在网络发展初期,需要花很多时间来学习 socket 的复杂,寻址等等,在 C socket 库上进行编码,并需要在不同的操作系统上做不同的处理。

    Java 早期版本(1995-2002)介绍了足够的面向对象的糖衣来隐藏一些复杂性,但实现复杂的客户端-服务器协议仍然需要大量的样板代码(和进行大量的监视才能确保他们是对的)。

    这些早期的 Java API(java.net)只能通过原生的 socket 库来支持所谓的“blocking(阻塞)”的功能。一个简单的例子

    Listing 1.1 Blocking I/O Example

    ServerSocket serverSocket = new ServerSocket(portNumber);//1
    Socket clientSocket = serverSocket.accept();             //2
    BufferedReader in = new BufferedReader(                     //3
            new InputStreamReader(clientSocket.getInputStream()));
    PrintWriter out =
            new PrintWriter(clientSocket.getOutputStream(), true);
    String request, response;
    while ((request = in.readLine()) != null) {                 //4
        if ("Done".equals(request)) {                         //5
            break;
        }
    }
    response = processRequest(request);                        //6
    out.println(response);                                    //7
    }                                                        //8
    

    1.ServerSocket 创建并监听端口的连接请求

    2.accept() 调用阻塞,直到一个连接被建立了。返回一个新的 Socket 用来处理 客户端和服务端的交互

    3.流被创建用于处理 socket 的输入和输出数据。BufferedReader 读取从字符输入流里面的文本。PrintWriter 打印格式化展示的对象读到本文输出流

    4.处理循环开始 readLine() 阻塞,读取字符串直到最后是换行或者输入终止。

    5.如果客户端发送的是“Done”处理循环退出

    6.执行方法处理请求,返回服务器的响应

    7.响应发回客户端

    8.处理循环继续

    显然,这段代码限制每次只能处理一个连接。为了实现多个并行的客户端我们需要分配一个新的 Thread 给每个新的客户端 Socket(当然需要更多的代码)。但考虑使用这种方法来支持大量的同步,长连接。在任何时间点多线程可能处于休眠状态,等待输入或输出数据。这很容易使得资源的大量浪费,对性能产生负面影响。当然,有一种替代方案。

    除了示例中所示阻塞调用,原生 socket 库同时也包含了非阻塞 I/O 的功能。这使我们能够确定任何一个 socket 中是否有数据准备读或写。我们还可以设置标志,因为读/写调用如果没有数据立即返回;就是说,如果一个阻塞被调用后就会一直阻塞,直到处理完成。通过这种方法,会带来更大的代码的复杂性成本,其实我们可以获得更多的控制权来如何利用网络资源。

    JAVA NIO

    在 2002 年,Java 1.4 引入了非阻塞 API 在 java.nio 包(NIO)。

    "New"还是"Nonblocking"?

    NIO 最初是为 New Input/Output 的缩写。然而,Java 的 API 已经存在足够长的时间,它不再是新的。现在普遍使用的缩写来表示Nonblocking I/O (非阻塞 I/O)。另一方面,一般(包括作者)指阻塞 I/O 为 OIO 或 Old Input/Output。你也可能会遇到普通 I/O。

    我们已经展示了在 Java 的 I/O 阻塞一例例子。图 1.1 展示了方法 必须扩大到处理多个连接:给每个连接创建一个线程,有些连接是空闲的!显然,这种方法的可扩展性将是受限于可以在 JVM 中创建的线程数。

    Figure 1.1 Blocking I/O

    Figure%201

    当你的应用中连接数比较少,这个方案还是可以接受。当并发连接超过10000 时,context-switching(上下文切换)开销将是明显的。此外,每个线程都有一个默认的堆栈内存分配了 128K 和 1M 之间的空间。考虑到整体的内存和操作系统需要处理 100000 个或更多的并发连接资源,这似乎是一个不理想的解决方案。

    SELECTOR

    相比之下,图1.2 显示了使用非阻塞I/O,主要是消除了这些方法 约束。在这里,我们介绍了“Selector”,这是 Java 的无阻塞 I/O 实现的关键。

    Figure 1.2 Nonblocking I/O

    v2-d2cbae084dd6d8babb9fb9da7351334f_720w

    Selector 最终决定哪一组注册的 socket 准备执行 I/O。正如我们之前所解释的那样,这 I/O 操作设置为非阻塞模式。通过通知,一个线程可以同时处理多个并发连接。(一个 Selector 由一个线程通常处理,但具体实施可以使用多个线程。)因此,每次读或写操作执行能立即检查完成。总体而言,该模型提供了比 阻塞 I/O 模型 更好的资源使用,因为

    • 可以用较少的线程处理更多连接,这意味着更少的开销在内存和上下文切换上
    • 当没有 I/O 处理时,线程可以被重定向到其他任务上。

    如果你要直接使用这些 Java API 构建的 NIO 建立你的应用程序,只是这样做正确和安全是无法保证的。实现可靠和可扩展的 event-processing(事件处理器)来处理和调度数据并保证尽可能有效地,这是一个繁琐和容易出错的任务,最好留给专家 - Netty。

    下面展示了 Netty 技术和方法的特点

    • 设计
      • 针对多种传输类型的统一接口 - 阻塞和非阻塞
      • 简单但更强大的线程模型
      • 真正的无连接的数据报套接字支持
      • 链接逻辑支持复用
    • 易用性
      • 大量的 Javadoc 和 代码实例
      • 除了在 JDK 1.6 + 额外的限制。(一些特征是只支持在Java 1.7 +。可选的功能可能有额外的限制。)
    • 性能
      • 比核心 Java API 更好的吞吐量,较低的延时
      • 资源消耗更少,这个得益于共享池和重用
      • 减少内存拷贝
    • 健壮性
      • 消除由于慢,快,或重载连接产生的 OutOfMemoryError
      • 消除经常发现在 NIO 在高速网络中的应用中的不公平的读/写比
    • 安全
      • 完整的 SSL / TLS 和 StartTLS 的支持
      • 运行在受限的环境例如 Applet 或 OSGI
    • 社区
      • 发布的更早和更频繁
      • 社区驱动

    异步和事件驱动

    所有的网络应用程序需要被设计为可扩展性,可以被界定为“一个系统,网络能力,或过程中能够处理越来越多的工作方式或可扩大到容纳增长的能力”(见 Bondi, André B. (2000). "Characteristics of scalability and their impact on performance")。我们已经说过,Netty 帮助您利用非阻塞 I/O 完成这一目标,通常称为“异步 I/O”

    我们将使用“异步”和其同源词在这本书中大量的使用,所以这是介绍他们的一个很好的时候。异步,即非同步事件,当然是跟你日常生活的类似。例如,您可以发送电子邮件;可能得到或者得不到任何回应,或者当你发送一个您可能会收到一个消息。异步事件也可以有一个有序的关系。例如,你通常不会收到一个问题的答案直到提出一个问题,但是你并没有阻止同时一些其他的东西。

    在日常生活中异步就这样发生了,所以我们不会经常想到。但让计算机程序的工作方式,来实现我们提出了的特殊的问题,会有一点复杂。在本质上,一个系统是异步和“事件驱动”将会表现出一个特定的,对我们来说,有价值的 行为:它可以响应在任何时间以任何顺序发生的事件

    这是我们要建立一种制度,正如我们将会看到,这是典范的 Netty 自底向上的支持。

    Netty由哪几个部分构成?

    2020-10-16 17:04 更新

    在前面的章节中我们已经对非阻塞I/O进行解释了,它不会强制要求我们等待操作完成。那么基于这个条件,真正的异步I/O的作用更进一步:一个异步方法完成时立即返回并直接或稍后通知用户。所以,在一个网络环境的异步模型中可以更加有效地利用资源,可以快速连续执行多个调用。除了Netty 的异步模型,本章还将介绍回调,future 及其组合使用。

    Channel

    Channel 是 NIO 基本的结构。它代表了一个用于连接到实体如硬件设备、文件、网络套接字或程序组件,能够执行一个或多个不同的 I/O 操作(例如读或写)的开放连接。

    现在,把 Channel 想象成一个可以“打开”或“关闭”,“连接”或“断开”和作为传入和传出数据的运输工具。

    Callback (回调)

    callback (回调)是一个简单的方法,提供给另一种方法作为引用,这样后者就可以在某个合适的时间调用前者。这种技术被广泛使用在各种编程的情况下,最常见的方法之一通知给其他人操作已完成。

    Netty 内部使用回调处理事件时。一旦这样的回调被触发,事件可以由接口 ChannelHandler 的实现来处理。如下面的代码,一旦一个新的连接建立了,调用 channelActive(),并将打印一条消息。

    Listing 1.2 ChannelHandler triggered by a callback

    public class ConnectHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {   //1
            System.out.println(
                    "Client " + ctx.channel().remoteAddress() + " connected");
        }
    }
    

    1.当建立一个新的连接时调用 ChannelActive()

    Future

    Future 提供了另外一种通知应用操作已经完成的方式。这个对象作为一个异步操作结果的占位符,它将在将来的某个时候完成并提供结果。

    JDK 附带接口 java.util.concurrent.Future ,但所提供的实现只允许您手动检查操作是否完成或阻塞了。这是很麻烦的,所以 Netty 提供自己了的实现,ChannelFuture,用于在执行异步操作时使用。

    ChannelFuture 提供多个附件方法来允许一个或者多个 ChannelFutureListener 实例。这个回调方法 operationComplete() 会在操作完成时调用。事件监听者能够确认这个操作是否成功或者是错误。如果是后者,我们可以检索到产生的 Throwable。简而言之, ChannelFutureListener 提供的通知机制不需要手动检查操作是否完成的。

    每个 Netty 的 outbound I/O 操作都会返回一个 ChannelFuture;这样就不会阻塞。这就是 Netty 所谓的“自底向上的异步和事件驱动”。

    下面例子简单的演示了作为 I/O 操作的一部分 ChannelFuture 的返回。当调用 connect() 将会直接是非阻塞的,并且调用在背后完成。由于线程是非阻塞的,所以无需等待操作完成,而可以去干其他事,因此这令资源利用更高效。

    Listing 1.3 Callback in action

    Channel channel = ...;
    //不会阻塞
    ChannelFuture future = channel.connect(
        new InetSocketAddress("192.168.0.1", 25));
    

    1.异步连接到远程地址

    下面代码描述了如何利用 ChannelFutureListener 。首先,连接到远程地址。接着,通过 ChannelFuture 调用 connect() 来 注册一个新ChannelFutureListener。当监听器被通知连接完成,我们检查状态。如果是成功,就写数据到 Channel,否则我们检索 ChannelFuture 中的Throwable。

    注意,错误的处理取决于你的项目。当然,特定的错误是需要加以约束 的。例如,在连接失败的情况下你可以尝试连接到另一个。

    Listing 1.4 Callback in action

    Channel channel = ...;
    //不会阻塞
    ChannelFuture future = channel.connect(            //1
            new InetSocketAddress("192.168.0.1", 25));
    future.addListener(new ChannelFutureListener() {  //2
    @Override
    public void operationComplete(ChannelFuture future) {
        if (future.isSuccess()) {                    //3
            ByteBuf buffer = Unpooled.copiedBuffer(
                    "Hello", Charset.defaultCharset()); //4
            ChannelFuture wf = future.channel().writeAndFlush(buffer);                //5
            // ...
        } else {
            Throwable cause = future.cause();        //6
            cause.printStackTrace();
        }
    }
    });
    

    1.异步连接到远程对等节点。调用立即返回并提供 ChannelFuture。

    2.操作完成后通知注册一个 ChannelFutureListener 。

    3.当 operationComplete() 调用时检查操作的状态。

    4.如果成功就创建一个 ByteBuf 来保存数据。

    5.异步发送数据到远程。再次返回ChannelFuture。

    6.如果有一个错误则抛出 Throwable,描述错误原因。

    Event 和 Handler

    Netty 使用不同的事件来通知我们更改的状态或操作的状态。这使我们能够根据发生的事件触发适当的行为。

    这些行为可能包括:

    • 日志
    • 数据转换
    • 流控制
    • 应用程序逻辑

    由于 Netty 是一个网络框架,事件很清晰的跟入站或出站数据流相关。因为一些事件可能触发传入的数据或状态的变化包括:

    • 活动或非活动连接
    • 数据的读取
    • 用户事件
    • 错误

    出站事件是由于在未来操作将触发一个动作。这些包括:

    • 打开或关闭一个连接到远程
    • 写或冲刷数据到 socket

    每个事件都可以分配给用户实现处理程序类的方法。这说明了事件驱动的范例可直接转换为应用程序构建块。

    图1.3显示了一个事件可以由一连串的事件处理器来处理

    Figure 1.3 Event Flow

    Figure%201

    Netty 的 ChannelHandler 是各种处理程序的基本抽象。想象下,每个处理器实例就是一个回调,用于执行对各种事件的响应。

    在此基础之上,Netty 也提供了一组丰富的预定义的处理程序,您可以开箱即用。比如,各种协议的编解码器包括 HTTP 和 SSL/TLS。在内部,ChannelHandler 使用事件和 future 本身,创建具有 Netty 特性抽象的消费者。

    整合

    FUTURE, CALLBACK 和 HANDLER

    Netty 的异步编程模型是建立在 future 和 callback 的概念上的。所有这些元素的协同为自己的设计提供了强大的力量。

    拦截操作和转换入站或出站数据只需要您提供回调或利用 future 操作返回的。这使得链操作简单、高效,促进编写可重用的、通用的代码。一个 Netty 的设计的主要目标是促进“关注点分离”:你的业务逻辑从网络基础设施应用程序中分离。

    SELECTOR, EVENT 和 EVENT LOOP

    Netty 通过触发事件从应用程序中抽象出 Selector,从而避免手写调度代码。EventLoop 分配给每个 Channel 来处理所有的事件,包括

    • 注册感兴趣的事件
    • 调度事件到 ChannelHandler
    • 安排进一步行动

    该 EventLoop 本身是由一个线程驱动,它给一个 Channel 处理所有的 I/O 事件,并且在 EventLoop 的生命周期内不会改变。这个简单而强大的线程模型消除你可能对你的 ChannelHandler 同步的任何关注,这样你就可以专注于提供正确的回调逻辑来执行。该 API 是简单和紧凑。

    Netty如何写一个 echo 服务器

    2018-08-02 14:09 更新

    Netty 实现的 echo 服务器都需要下面这些:

    • 一个服务器 handler:这个组件实现了服务器的业务逻辑,决定了连接创建后和接收到信息后该如何处理
    • Bootstrapping: 这个是配置服务器的启动代码。最少需要设置服务器绑定的端口,用来监听连接请求。

    通过 ChannelHandler 来实现服务器的逻辑

    Echo Server 将会将接受到的数据的拷贝发送给客户端。因此,我们需要实现 ChannelInboundHandler 接口,用来定义处理入站事件的方法。由于我们的应用很简单,只需要继承 ChannelInboundHandlerAdapter 就行了。这个类 提供了默认 ChannelInboundHandler 的实现,所以只需要覆盖下面的方法:

    • channelRead() - 每个信息入站都会调用
    • channelReadComplete() - 通知处理器最后的 channelread() 是当前批处理中的最后一条消息时调用
    • exceptionCaught()- 读操作时捕获到异常时调用

    EchoServerHandler 代码如下:

    Listing 2.2 EchoServerHandler

    @Sharable                                        //1
    public class EchoServerHandler extends
            ChannelInboundHandlerAdapter {
    
        @Override
        public void channelRead(ChannelHandlerContext ctx,
            Object msg) {
            ByteBuf in = (ByteBuf) msg;
            System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));        //2
            ctx.write(in);                            //3
        }
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)//4
            .addListener(ChannelFutureListener.CLOSE);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx,
            Throwable cause) {
            cause.printStackTrace();                //5
            ctx.close();                            //6
        }
    }
    

    1.@Sharable 标识这类的实例之间可以在 channel 里面共享

    2.日志消息输出到控制台

    3.将所接收的消息返回给发送者。注意,这还没有冲刷数据

    4.冲刷所有待审消息到远程节点。关闭通道后,操作完成

    5.打印异常堆栈跟踪

    6.关闭通道

    这种使用 ChannelHandler 的方式体现了关注点分离的设计原则,并简化业务逻辑的迭代开发的要求。处理程序很简单;它的每一个方法可以覆盖到“hook(钩子)”在活动周期适当的点。很显然,我们覆盖 channelRead因为我们需要处理所有接收到的数据。

    覆盖 exceptionCaught 使我们能够应对任何 Throwable 的子类型。在这种情况下我们记录,并关闭所有可能处于未知状态的连接。它通常是难以 从连接错误中恢复,所以干脆关闭远程连接。当然,也有可能的情况是可以从错误中恢复的,所以可以用一个更复杂的措施来尝试识别和处理 这样的情况。

    如果异常没有被捕获,会发生什么?

    每个 Channel 都有一个关联的 ChannelPipeline,它代表了 ChannelHandler 实例的链。适配器处理的实现只是将一个处理方法调用转发到链中的下一个处理器。因此,如果一个 Netty 应用程序不覆盖exceptionCaught ,那么这些错误将最终到达 ChannelPipeline,并且结束警告将被记录。出于这个原因,你应该提供至少一个 实现 exceptionCaught 的 ChannelHandler。

    关键点要牢记:

    • ChannelHandler 是给不同类型的事件调用
    • 应用程序实现或扩展 ChannelHandler 挂接到事件生命周期和 提供自定义应用逻辑。

    引导服务器

    了解到业务核心处理逻辑 EchoServerHandler 后,下面要引导服务器自身了。

    • 监听和接收进来的连接请求
    • 配置 Channel 来通知一个关于入站消息的 EchoServerHandler 实例

    Transport(传输)

    在本节中,你会遇到“transport(传输)”一词。在网络的多层视图协议里面,传输层提供了用于端至端或主机到主机的通信服务。互联网通信的基础是 TCP 传输。当我们使用术语“NIO transport”我们指的是一个传输的实现,它是大多等同于 TCP ,除了一些由 Java NIO 的实现提供了服务器端的性能增强。Transport 详细在第4章中讨论。

    Listing 2.3 EchoServer

    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>");
                return;
            }
            int port = Integer.parseInt(args[0]);        //1
            new EchoServer(port).start();                //2
        }
    
        public void start() throws Exception {
            NioEventLoopGroup group = new NioEventLoopGroup(); //3
            try {
                ServerBootstrap b = new ServerBootstrap();
                b.group(group)                                //4
                 .channel(NioServerSocketChannel.class)        //5
                 .localAddress(new InetSocketAddress(port))    //6
                 .childHandler(new ChannelInitializer<SocketChannel>() { //7
                     @Override
                     public void initChannel(SocketChannel ch) 
                         throws Exception {
                         ch.pipeline().addLast(
                                 new EchoServerHandler());
                     }
                 });
    
                ChannelFuture f = b.bind().sync();            //8
                System.out.println(EchoServer.class.getName() + " started and listen on " + f.channel().localAddress());
                f.channel().closeFuture().sync();            //9
            } finally {
                group.shutdownGracefully().sync();            //10
            }
        }
    
    }
    

    1.设置端口值(抛出一个 NumberFormatException 如果该端口参数的格式不正确)

    2.呼叫服务器的 start() 方法

    3.创建 EventLoopGroup

    4.创建 ServerBootstrap

    5.指定使用 NIO 的传输 Channel

    6.设置 socket 地址使用所选的端口

    7.添加 EchoServerHandler 到 Channel 的 ChannelPipeline

    8.绑定的服务器;sync 等待服务器关闭

    9.关闭 channel 和 块,直到它被关闭

    10.关机的 EventLoopGroup,释放所有资源。

    在这个例子中,代码创建 ServerBootstrap 实例(步骤4)。由于我们使用在 NIO 传输,我们已指定 NioEventLoopGroup(3)接受和处理新连接,指定 NioServerSocketChannel(5)为信道类型。在此之后,我们设置本地地址是 InetSocketAddress 与所选择的端口(6)如。服务器将绑定到此地址来监听新的连接请求。

    第7步是关键:在这里我们使用一个特殊的类,ChannelInitializer 。当一个新的连接被接受,一个新的子 Channel 将被创建, ChannelInitializer 会添加我们EchoServerHandler 的实例到 Channel 的 ChannelPipeline。正如我们如前所述,如果有入站信息,这个处理器将被通知。

    虽然 NIO 是可扩展性,但它的正确配置是不简单的。特别是多线程,要正确处理也非易事。幸运的是,Netty 的设计封装了大部分复杂性,尤其是通过抽象,例如 EventLoopGroup,SocketChannel 和 ChannelInitializer,其中每一个将在更详细地在第3章中讨论。

    在步骤8,我们绑定的服务器,等待绑定完成。 (调用 sync() 的原因是当前线程阻塞)在第9步的应用程序将等待服务器 Channel 关闭(因为我们 在 Channel 的 CloseFuture 上调用 sync())。现在,我们可以关闭下 EventLoopGroup 并释放所有资源,包括所有创建的线程(10)。

    NIO 用于在本实施例,因为它是目前最广泛使用的传输,归功于它的可扩展性和彻底的不同步。但不同的传输的实现是也是可能的。例如,如果本实施例中使用的 OIO 传输,我们将指定 OioServerSocketChannel 和 OioEventLoopGroup。 Netty 的架构,包括更关于传输信息,将包含在第4章。在此期间,让我们回顾下在服务器上执行,我们只研究重要步骤。

    服务器的主代码组件是

    • EchoServerHandler 实现了的业务逻辑
    • 在 main() 方法,引导了服务器

    执行后者所需的步骤是:

    • 创建 ServerBootstrap 实例来引导服务器并随后绑定
    • 创建并分配一个 NioEventLoopGroup 实例来处理事件的处理,如接受新的连接和读/写数据。
    • 指定本地 InetSocketAddress 给服务器绑定
    • 通过 EchoServerHandler 实例给每一个新的 Channel 初始化
    • 最后调用 ServerBootstrap.bind() 绑定服务器

    这样服务器的初始化就完成了,并可以被使用。

    Netty写一个 echo 客户端

    2018-08-02 14:20 更新

    在上节的内容中我们完成了echo服务器的编写,接下来就让我们一起来学习Netty中如何写一个echo的客户端,这样才能让连接客户端,并完成信息的传送。

    客户端的工作内容:

    • 连接服务器
    • 发送信息
    • 发送的每个信息,等待和接收从服务器返回的同样的信息
    • 关闭连接

    用 ChannelHandler 实现客户端逻辑

    跟写服务器一样,我们提供 ChannelInboundHandler 来处理数据。下面例子,我们用 SimpleChannelInboundHandler 来处理所有的任务,需要覆盖三个方法:

    • channelActive() - 服务器的连接被建立后调用
    • channelRead0() - 数据后从服务器接收到调用
    • exceptionCaught() - 捕获一个异常时调用

    Listing 2.4 ChannelHandler for the client

    @Sharable                                //1
    public class EchoClientHandler extends
            SimpleChannelInboundHandler<ByteBuf> {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", //2
            CharsetUtil.UTF_8));
        }
    
        @Override
        public void channelRead0(ChannelHandlerContext ctx,
            ByteBuf in) {
            System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));    //3
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx,
            Throwable cause) {                    //4
            cause.printStackTrace();
            ctx.close();
        }
    }
    

    1.@Sharable标记这个类的实例可以在 channel 里共享

    2.当被通知该 channel 是活动的时候就发送信息

    3.记录接收到的消息

    4.记录日志错误并关闭 channel

    建立连接后该 channelActive() 方法被调用一次。逻辑很简单:一旦建立了连接,字节序列被发送到服务器。该消息的内容并不重要;在这里,我们使用了 Netty 编码字符串 “Netty rocks!” 通过覆盖这种方法,我们确保东西被尽快写入到服务器。

    接下来,我们覆盖方法 channelRead0()。这种方法会在接收到数据时被调用。注意,由服务器所发送的消息可以以块的形式被接收。即,当服务器发送 5 个字节是不是保证所有的 5 个字节会立刻收到 - 即使是只有 5 个字节,channelRead0() 方法可被调用两次,第一次用一个ByteBuf(Netty的字节容器)装载3个字节和第二次一个 ByteBuf 装载 2 个字节。唯一要保证的是,该字节将按照它们发送的顺序分别被接收。 (注意,这是真实的,只有面向流的协议如TCP)。

    第三个方法重写是 exceptionCaught()。正如在 EchoServerHandler (清单2.2),所述的记录 Throwable 并且关闭通道,在这种情况下终止 连接到服务器。

    SimpleChannelInboundHandler vs. ChannelInboundHandler

    何时用这两个要看具体业务的需要。在客户端,当 channelRead0() 完成,我们已经拿到的入站的信息。当方法返回时,SimpleChannelInboundHandler 会小心的释放对 ByteBuf(保存信息) 的引用。而在 EchoServerHandler,我们需要将入站的信息返回给发送者,由于 write() 是异步的,在 channelRead() 返回时,可能还没有完成。所以,我们使用 ChannelInboundHandlerAdapter,无需释放信息。最后在 channelReadComplete() 我们调用 ctxWriteAndFlush() 来释放信息。详见第5、6章

    引导客户端

    客户端引导需要 host 、port 两个参数连接服务器。

    Listing 2.5 Main class for the client

    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();                //1
                b.group(group)                                //2
                 .channel(NioSocketChannel.class)            //3
                 .remoteAddress(new InetSocketAddress(host, port))    //4
                 .handler(new ChannelInitializer<SocketChannel>() {    //5
                     @Override
                     public void initChannel(SocketChannel ch) 
                         throws Exception {
                         ch.pipeline().addLast(
                                 new EchoClientHandler());
                     }
                 });
    
                ChannelFuture f = b.connect().sync();        //6
    
                f.channel().closeFuture().sync();            //7
            } finally {
                group.shutdownGracefully().sync();            //8
            }
        }
    
        public static void main(String[] args) throws Exception {
            if (args.length != 2) {
                System.err.println(
                        "Usage: " + EchoClient.class.getSimpleName() +
                        " <host> <port>");
                return;
            }
    
            final String host = args[0];
            final int port = Integer.parseInt(args[1]);
    
            new EchoClient(host, port).start();
        }
    }
    

    1.创建 Bootstrap

    2.指定 EventLoopGroup 来处理客户端事件。由于我们使用 NIO 传输,所以用到了 NioEventLoopGroup 的实现

    3.使用的 channel 类型是一个用于 NIO 传输

    4.设置服务器的 InetSocketAddress

    5.当建立一个连接和一个新的通道时,创建添加到 EchoClientHandler 实例 到 channel pipeline

    6.连接到远程;等待连接完成

    7.阻塞直到 Channel 关闭

    8.调用 shutdownGracefully() 来关闭线程池和释放所有资源

    与以前一样,在这里使用了 NIO 传输。请注意,您可以在 客户端和服务器 使用不同的传输 ,例如 NIO 在服务器端和 OIO 客户端。在第四章中,我们将研究一些具体的因素和情况,这将导致 您可以选择一种传输,而不是另一种。

    本节要点回顾:

    • 创建一个 Bootstrap 来初始化客户端
    • 一个 NioEventLoopGroup 实例被分配给处理该事件的处理,这包括创建新的连接和处理入站和出站数据
    • 创建一个 InetSocketAddress 以连接到服务器
    • 连接好服务器之时,将安装一个 EchoClientHandler 在 pipeline 
    • 之后 Bootstrap.connect()被调用连接到远程的 - 本例就是 echo(回声)服务器。

    Netty 快速入门

    2018-08-02 14:48 更新

    下文中介绍的是Netty应用程序的全部基本构建模块,其中客户端和服务器也包含在内。

    BOOTSTRAP

    Netty 应用程序通过设置 bootstrap(引导)类的开始,该类提供了一个 用于应用程序网络层配置的容器。

    CHANNEL

    底层网络传输 API 必须提供给应用 I/O操作的接口,如读,写,连接,绑定等等。对于我们来说,这是结构几乎总是会成为一个“socket”。 Netty 中的接口 Channel 定义了与 socket 丰富交互的操作集:bind, close, config, connect, isActive, isOpen, isWritable, read, write 等等。 Netty 提供大量的 Channel 实现来专门使用。这些包括 AbstractChannel,AbstractNioByteChannel,AbstractNioChannel,EmbeddedChannel, LocalServerChannel,NioSocketChannel 等等。

    CHANNELHANDLER

    ChannelHandler 支持很多协议,并且提供用于数据处理的容器。我们已经知道 ChannelHandler 由特定事件触发。 ChannelHandler 可专用于几乎所有的动作,包括将一个对象转为字节(或相反),执行过程中抛出的异常处理。

    常用的一个接口是 ChannelInboundHandler,这个类型接收到入站事件(包括接收到的数据)可以处理应用程序逻辑。当你需要提供响应时,你也可以从 ChannelInboundHandler 冲刷数据。一句话,业务逻辑经常存活于一个或者多个 ChannelInboundHandler。

    CHANNELPIPELINE

    ChannelPipeline 提供了一个容器给 ChannelHandler 链并提供了一个API 用于管理沿着链入站和出站事件的流动。每个 Channel 都有自己的ChannelPipeline,当 Channel 创建时自动创建的。 ChannelHandler 是如何安装在 ChannelPipeline? 主要是实现了ChannelHandler 的抽象 ChannelInitializer。ChannelInitializer子类 通过 ServerBootstrap 进行注册。当它的方法 initChannel() 被调用时,这个对象将安装自定义的 ChannelHandler 集到 pipeline。当这个操作完成时,ChannelInitializer 子类则 从 ChannelPipeline 自动删除自身。

    EVENTLOOP

    EventLoop 用于处理 Channel 的 I/O 操作。一个单一的 EventLoop通常会处理多个 Channel 事件。一个 EventLoopGroup 可以含有多于一个的 EventLoop 和 提供了一种迭代用于检索清单中的下一个。

    CHANNELFUTURE

    Netty 所有的 I/O 操作都是异步。因为一个操作可能无法立即返回,我们需要有一种方法在以后确定它的结果。出于这个目的,Netty 提供了接口 ChannelFuture,它的 addListener 方法注册了一个 ChannelFutureListener ,当操作完成时,可以被通知(不管成功与否)。

    更多关于 ChannelFuture

    想想一个 ChannelFuture 对象作为一个未来执行操作结果的占位符。何时执行取决于几个因素,因此不可能预测与精确。但我们可以肯定的是,它会被执行。此外,所有的操作返回 ChannelFuture 对象和属于同一个 Channel 将在以正确的顺序被执行,在他们被调用后。

    Netty的Channel, Event 和 I/O

    2018-08-08 10:26 更新

    Netty是一个异步事件驱动的NIO框架,Netty的所有IO操作都是异步非阻塞的。Netty 实际上是使用 Threads(多线程)处理 I/O 事件,熟悉多线程编程的读者可能会需要关注同步代码。但是这么做不好,因为同步会影响程序的性能,Netty 的设计保证程序处理事件不会有同步。图 Figure 3.1 展示了,你不需要在 Channel 之间共享 ChannelHandler 实例的原因:

    Figure%203

    Figure 3.1

    该图显示,一个 EventLoopGroup 具有一个或多个 EventLoop。想象 EventLoop 作为一个 Thread 给 Channel 执行工作。 (事实上,一个 EventLoop 是势必为它的生命周期一个线程。)

    当创建一个 Channel,Netty 通过 一个单独的 EventLoop 实例来注册该 Channel(并同样是一个单独的 Thread)的通道的使用寿命。这就是为什么你的应用程序不需要同步 Netty 的 I/O操作;所有 Channel 的 I/O 始终用相同的线程来执行。

    Netty中 Bootstrapping 的作用

    2018-08-08 10:27 更新

    Bootstrapping是什么?它在Netty中有什么作用呢?Bootstrapping(引导) 是出现在Netty 配置程序的过程中,Bootstrapping在给服务器绑定指定窗口或者要连接客户端的时候会使用到。

    Bootstrapping 有以下两种类型:

    • 一种是用于客户端的Bootstrap
    • 一种是用于服务端的ServerBootstrap

    不管程序使用哪种协议,创建的是一个客户端还是服务器,“引导”都是必须要使用到的。

    面向连接 vs. 无连接

    请记住,这个讨论适用于 TCP 协议,它是“面向连接”的。这样协议保证该连接的端点之间的消息的有序输送。无连接协议发送的消息,无法保证顺序和成功性

    两种 Bootstrapping 之间有一些相似之处,也有一些不同。Bootstrap 和 ServerBootstrap 之间的差异如下:

    Table 3.1 Comparison of Bootstrap classes

    分类 Bootstrap ServerBootstrap
    网络功能 连接到远程主机和端口 绑定本地端口
    EventLoopGroup 数量 1 2

    Bootstrap用来连接远程主机,有1个EventLoopGroup

    ServerBootstrap用来绑定本地端口,有2个EventLoopGroup

    事件组(Groups),传输(transports)和处理程序(handlers)分别在本章后面讲述,我们在这里只讨论两种"引导"的差异(Bootstrap和ServerBootstrap)。第一个差异很明显,“ServerBootstrap”监听在服务器监听一个端口轮询客户端的“Bootstrap”或DatagramChannel是否连接服务器。通常需要调用“Bootstrap”类的connect()方法,但是也可以先调用bind()再调用connect()进行连接,之后使用的Channel包含在bind()返回的ChannelFuture中。

    一个 ServerBootstrap 可以认为有2个 Channel 集合,第一个集合包含一个单例 ServerChannel,代表持有一个绑定了本地端口的 socket;第二集合包含所有创建的 Channel,处理服务器所接收到的客户端进来的连接。下图形象的描述了这种情况:

    Figure%203

    Figure 3.2 Server with two EventLoopGroups

    与 ServerChannel 相关 EventLoopGroup 分配一个 EventLoop 是 负责创建 Channels 用于传入的连接请求。一旦连接接受,第二个EventLoopGroup 分配一个 EventLoop 给它的 Channel。

    认识Netty的ChannelHandler 和 ChannelPipeline

    2018-08-08 10:29 更新

    ChannelHandlerChannelPipeline看着差不多,不仔细看还可能会混淆,那么它们是什么关系呢?其实,ChannelPipeline 就是 ChannelHandler 链的容器。

    在我们的应用程序中ChannelHandler是许多方面的核心,或许它作为核心的表现并不明显。ChannelHandler的用途广泛,所以我们难以对其进行界定,也正因为如此,在使用它的时候最好是当成一个通用的容器,处理进来的事件(包括数据)并且通过ChannelPipeline。

    下图展示了 ChannelInboundHandler 和 ChannelOutboundHandler 继承自父接口 ChannelHandler。

    Figure%203

    Figure 3.3 ChannelHandler class hierarchy

    Netty 中有两个方向的数据流,图3.4 显示的入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之间有一个明显的区别:若数据是从用户应用程序到远程主机则是“出站(outbound)”,相反若数据时从远程主机到用户应用程序则是“入站(inbound)”。

    为了使数据从一端到达另一端,一个或多个 ChannelHandler 将以某种方式操作数据。这些 ChannelHandler 会在程序的“引导”阶段被添加ChannelPipeline中,并且被添加的顺序将决定处理数据的顺序。

    Figure%203

    Figure 3.4 ChannelPipeline with inbound and outbound ChannelHandlers

    图 3.4 同样展示了进站和出站的处理器都可以被安装在相同的 pipeline 。本例子中,如果消息或任何其他入站事件被读到,将从 pipeline 头部开始,传递到第一个 ChannelInboundHandler。该处理器可能会或可能不会实际修改数据,取决于其特定的功能,在这之后 该数据将被传递到链中的下一个 ChannelInboundHandler。最后,将数据 到达 pipeline 的尾部,此时所有处理结束。

    数据的出站运动(即,数据被“写入”)在概念上是相同的。在这种情况下的数据从尾部流过 ChannelOutboundHandlers 的链,直到它到达头部。超过这点,出站数据将到达的网络传输,在这里显示为一个 socket。通常,这将触发一个写入操作。

    更多 Inbound 、 Outbound Handler

    在当前的链(chain)中,事件可以通过 ChanneHandlerContext 传递给下一个 handler。Netty 为此提供了抽象基类ChannelInboundHandlerAdapter 和 hannelOutboundHandlerAdapter,用来处理你想要的事件。 这些类提供的方法的实现,可以简单地通过调用 ChannelHandlerContext 上的相应方法将事件传递给下一个 handler。在实际应用中,您可以按需覆盖相应的方法即可。

    所以,如果出站和入站操作是不同的,当 ChannelPipeline 中有混合处理器时将发生什么?虽然入站和出站处理器都扩展了 ChannelHandler,Netty 的 ChannelInboundHandler 的实现 和 ChannelOutboundHandler 之间的是有区别的,从而保证数据传递只从一个处理器到下一个处理器保证正确的类型。

    当 ChannelHandler 被添加到的 ChannelPipeline 它得到一个 ChannelHandlerContext,它代表一个 ChannelHandler 和 ChannelPipeline 之间的“绑定”。它通常是安全保存对此对象的引用,除了当协议中的使用的是不面向连接(例如,UDP)。而该对象可以被用来获得 底层 Channel,它主要是用来写出站数据。

    实际上,在 Netty 发送消息可以采用两种方式:直接写消息给 Channel 或者写入 ChannelHandlerContext 对象。这两者主要的区别是, 前一种方法会导致消息从 ChannelPipeline的尾部开始,而后者导致消息从 ChannelPipeline 下一个处理器开始。

    近距离观察Netty的ChannelHandler

    2018-08-02 15:31 更新

    ChannelHandler的类型是有很多的,我们在前面的章节中已经提过了,而且每个ChannelHandler需要做什么都取决与它的超类。为了能够让开发处理逻辑变得简单,Netty提供了一些默认的处理程序来实现形式的“adapter(适配器)”类。pipeline 中每个的 ChannelHandler 主要负责转发事件到链中的下一个处理器。这些适配器类(及其子类)会自动帮你实现,所以你只需要实现该特定的方法和事件。

    为什么用适配器?

    有几个适配器类,可以减少编写自定义 ChannelHandlers ,因为他们提供对应接口的所有方法的默认实现。(也有类似的适配器,用于创建编码器和解码器,这我们将在稍后讨论。)这些都是创建自定义处理器时,会经常调用的适配器:ChannelHandlerAdapter、ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter、ChannelDuplexHandlerAdapter

    下面解释下三个 ChannelHandler 的子类型:编码器、解码器以及 ChannelInboundHandlerAdapter 的子类SimpleChannelInboundHandler

    编码器、解码器

    当您发送或接收消息时,Netty 数据转换就发生了。入站消息将从字节转为一个Java对象;也就是说,“解码”。如果该消息是出站相反会发生:“编码”,从一个Java对象转为字节。其原因是简单的:网络数据是一系列字节,因此需要从那类型进行转换。

    不同类型的抽象类用于提供编码器和解码器的,这取决于手头的任务。例如,应用程序可能并不需要马上将消息转为字节。相反,该​​消息将被转换 一些其他格式。一个编码器将仍然可以使用,但它也将衍生自不同的超类,

    在一般情况下,基类将有一个名字类似 ByteToMessageDecoder 或 MessageToByteEncoder。在一种特殊类型的情况下,你可能会发现类似 ProtobufEncoder 和 ProtobufDecoder,用于支持谷歌的 protocol buffer。

    严格地说,其他处理器可以做编码器和解码器能做的事。但正如适配器类简化创建通道处理器,所有的编码器/解码器适配器类 都实现自 ChannelInboundHandler 或 ChannelOutboundHandler。

    对于入站数据,channelRead 方法/事件被覆盖。这种方法在每个消息从入站 Channel 读入时调用。该方法将调用特定解码器的“解码”方法,并将解码后的消息转发到管道中下个的 ChannelInboundHandler。

    出站消息是类似的。编码器将消息转为字节,转发到下个的 ChannelOutboundHandler。

    SimpleChannelHandler

    也许最常见的处理器是接收到解码后的消息并应用一些业务逻辑到这些数据。要创建这样一个 ChannelHandler,你只需要扩展基类SimpleChannelInboundHandler 其中 T 是想要进行处理的类型。这样的处理器,你将覆盖基类的一个或多个方法,将获得被作为输入参数传递所有方法的 ChannelHandlerContext 的引用。

    在这种类型的处理器方法中的最重要是 channelRead0(ChannelHandlerContext,T)。在这个调用中,T 是将要处理的消息。 你怎么做,完全取决于你,但无论如何你不能阻塞 I/O线程,因为这可能是不利于高性能。

    阻塞操作

    I/O 线程一定不能完全阻塞,因此禁止任何直接阻塞操作在你的 ChannelHandler, 有一种方法来实现这一要求。你可以指定一个 EventExecutorGroup。当添加 ChannelHandler 到ChannelPipeline。此 EventExecutorGroup 将用于获得EventExecutor,将执行所有的 ChannelHandler 的方法。这EventExecutor 将从 I/O 线程使用不同的线程,从而释放EventLoop。

    Netty核心之Transport(传输)

    2021-11-19 10:50 更新

    本章将着重介绍Netty核心功能中的transport(传输),讲一些他们的用例以及 API:

    • NIO
    • OIO
    • Local(本地)
    • Embedded(内嵌)

    网络应用程序让人与系统之间可以进行通信,当然网络应用程序也可以将大量的数据从一个地方转移到另一个地方。如何做到这一点取决于具体的网络传输,但转移始终是相同的:字节通过线路。传输的概念帮助我们抽象掉的底层数据转移的机制。所有人都需要知道的是,字节在被发送和接收。

    当你做过Java中的网络编程的时候,你应该会发现要支持的并发连接会比预期中要多得多,当然这只是在某些时候会出现的情况。如果你再尝试从阻塞切换到非阻塞传输,则可能遇会到的问题,因为 Java 的公开的网络 API 来处理这两种情况有很大的不同。

    Netty 在传输层的API是统一的,这使得比你用 JDK 实现更简单。你无需重构整个代码库,然后将时间花到其他更值得去做的事情上。

    在本章接下来的内容中,我们将对这个统一的API进行研究并将它与JDK对比一下,让大家了解它比JDK更具易用性。我们将介绍不同的捆绑在 Netty 的传输实现和适当的用例。吸收这些信息后,你就知道如何选择适合您的应用的最佳选择。

    本章的唯一前提是 Java 编程语言的知识。最好是有网络框架或网络编程的经验,但也不是必需的。

    接下来就让我们来看看现实世界传输是如何工作的。

    Netty案例研究:Transport 的迁移

    2021-12-02 09:42 更新

    本节将会通过一个简单的应用程序来让您看看在 Netty  Transport(传输) 是如何工作的,这个应用程序要做的事情非常简单,只需要连接好客户端,并且向客户端发送字符串“Hi!”的信息,信息发送完之后连接就断开了。

    不用 Netty 实现 I/O 和 NIO

    我们将不用 Netty 实现 I/O 和 NIO,而是使用 JDK API 来实现 I/O 和 NIO。下面这个例子,是使用阻塞 IO 实现的例子:

    Listing 4.1 Blocking networking without Netty

    public class PlainOioServer {
    
        public void serve(int port) throws IOException {
            final ServerSocket socket = new ServerSocket(port);     //1
            try {
                for (;;) {
                    final Socket clientSocket = socket.accept();    //2
                    System.out.println("Accepted connection from " + clientSocket);
    
                    new Thread(new Runnable() {                        //3
                        @Override
                        public void run() {
                            OutputStream out;
                            try {
                                out = clientSocket.getOutputStream();
                                out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));                            //4
                                out.flush();
                                clientSocket.close();                //5
    
                            } catch (IOException e) {
                                e.printStackTrace();
                                try {
                                    clientSocket.close();
                                } catch (IOException ex) {
                                    // ignore on close
                                }
                            }
                        }
                    }).start();                                        //6
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    1.绑定服务器到指定的端口。

    2.接受一个连接。

    3.创建一个新的线程来处理连接。

    4.将消息发送到连接的客户端。

    5.一旦消息被写入和刷新时就 关闭连接。

    6.启动线程。

    上面的方式可以工作正常,但是这种阻塞模式在大连接数的情况就会有很严重的问题,如客户端连接超时,服务器响应严重延迟,性能无法扩展。为了解决这种情况,我们可以使用异步网络处理所有的并发连接,但问题在于 NIO 和 OIO 的 API 是完全不同的,所以一个用 OIO 开发的网络应用程序想要使用 NIO 重构代码几乎是重新开发。

    下面代码是使用 NIO 实现的例子:

    Listing 4.2 Asynchronous networking without Netty

    public class PlainNioServer {
        public void serve(int port) throws IOException {
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            ServerSocket ss = serverChannel.socket();
            InetSocketAddress address = new InetSocketAddress(port);
            ss.bind(address);                                            //1
            Selector selector = Selector.open();                        //2
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);    //3
            final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
            for (;;) {
                try {
                    selector.select();                                    //4
                } catch (IOException ex) {
                    ex.printStackTrace();
                    // handle exception
                    break;
                }
                Set<SelectionKey> readyKeys = selector.selectedKeys();    //5
                Iterator<SelectionKey> iterator = readyKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    try {
                        if (key.isAcceptable()) {                //6
                            ServerSocketChannel server =
                                    (ServerSocketChannel)key.channel();
                            SocketChannel client = server.accept();
                            client.configureBlocking(false);
                            client.register(selector, SelectionKey.OP_WRITE |
                                    SelectionKey.OP_READ, msg.duplicate());    //7
                            System.out.println(
                                    "Accepted connection from " + client);
                        }
                        if (key.isWritable()) {                //8
                            SocketChannel client =
                                    (SocketChannel)key.channel();
                            ByteBuffer buffer =
                                    (ByteBuffer)key.attachment();
                            while (buffer.hasRemaining()) {
                                if (client.write(buffer) == 0) {        //9
                                    break;
                                }
                            }
                            client.close();                    //10
                        }
                    } catch (IOException ex) {
                        key.cancel();
                        try {
                            key.channel().close();
                        } catch (IOException cex) {
                            // 在关闭时忽略
                        }
                    }
                }
            }
        }
    }
    

    1.绑定服务器到制定端口

    2.打开 selector 处理 channel

    3.注册 ServerSocket 到 ServerSocket ,并指定这是专门用来接受连接的。

    4.等待新的事件来处理。这将阻塞,直到一个事件是传入。

    5.从收到的所有事件中 获取 SelectionKey 实例。

    6.检查该事件是一个新的连接准备好接受。

    7.接受客户端,并用 selector 进行注册。

    8.检查 socket 是否准备好写数据。

    9.将数据写入到所连接的客户端。如果网络饱和,连接是可写的,那么这个循环将写入数据,直到该缓冲区是空的。

    10.关闭连接。

    如你所见,即使它们实现的功能是一样,但是代码完全不同。下面我们将用 Netty 来实现相同的功能。

    采用 Netty 实现 I/O 和 NIO

    下面代码是使用 Netty 作为网络框架编写的一个阻塞 IO 例子:

    Listing 4.3 Blocking networking with Netty

    public class NettyOioServer {
    
        public void server(int port) throws Exception {
            final ByteBuf buf = Unpooled.unreleasableBuffer(
                    Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
            EventLoopGroup group = new OioEventLoopGroup();
            try {
                ServerBootstrap b = new ServerBootstrap();        //1
    
                b.group(group)                                    //2
                 .channel(OioServerSocketChannel.class)
                 .localAddress(new InetSocketAddress(port))
                 .childHandler(new ChannelInitializer<SocketChannel>() {//3
                     @Override
                     public void initChannel(SocketChannel ch) 
                         throws Exception {
                         ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {            //4
                             @Override
                             public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                 ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
                             }
                         });
                     }
                 });
                ChannelFuture f = b.bind().sync();  //6
                f.channel().closeFuture().sync();
            } finally {
                group.shutdownGracefully().sync();        //7
            }
        }
    }
    

    1.创建一个 ServerBootstrap

    2.使用 OioEventLoopGroup 允许阻塞模式(OIO)

    3.指定 ChannelInitializer 将给每个接受的连接调用

    4.添加的 ChannelHandler 拦截事件,并允许他们作出反应

    5.写信息到客户端,并添加 ChannelFutureListener 当一旦消息写入就关闭连接

    6.绑定服务器来接受连接

    7.释放所有资源

    下面代码是使用 Netty NIO 实现。

    Netty NIO 版本

    下面是 Netty NIO 的代码,只是改变了一行代码,就从 OIO 传输 切换到了 NIO。

    Listing 4.4 Asynchronous networking with Netty

    public class NettyNioServer {
    
        public void server(int port) throws Exception {
            final ByteBuf buf = Unpooled.unreleasableBuffer(
                    Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
            NioEventLoopGroup group = new NioEventLoopGroup();
            try {
                ServerBootstrap b = new ServerBootstrap();    //1
                b.group(new NioEventLoopGroup(), new NioEventLoopGroup())   //2
                 .channel(NioServerSocketChannel.class)
                 .localAddress(new InetSocketAddress(port))
                 .childHandler(new ChannelInitializer<SocketChannel>() {    //3
                     @Override
                     public void initChannel(SocketChannel ch) 
                         throws Exception {
                         ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {    //4
                             @Override
                             public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                 ctx.writeAndFlush(buf.duplicate())                //5
                                    .addListener(ChannelFutureListener.CLOSE);
                             }
                         });
                     }
                 });
                ChannelFuture f = b.bind().sync();                    //6
                f.channel().closeFuture().sync();
            } finally {
                group.shutdownGracefully().sync();                    //7
            }
        }
    }
    

    1.创建一个 ServerBootstrap

    2.使用 NioEventLoopGroup 允许非阻塞模式

    3.指定 ChannelInitializer 将给每个接受的连接调用

    4.添加的 ChannelInboundHandlerAdapter() 接收事件并进行处理

    5.写信息到客户端,并添加 ChannelFutureListener 当一旦消息写入就关闭连接

    6.绑定服务器来接受连接

    7.释放所有资源

    我们之前提到过 Netty 使用的是统一的 API,所以 Netty 中实现的每个传输都是用了同样的 API,你使用什么来实现并不在它的关心范围内。Netty 通过操作接口 Channel 、ChannelPipeline 和 ChannelHandler 来实现。

    使用基于 Netty 传输的好处显而易见了吧,那么接下来我们就来看看传输的 API

    基于Netty传输的API

    2018-08-08 10:29 更新

    在上节中我们通过对比了解到了使用基于Netty传输的好处,那么在本节中我们就来看看Transport API是如何工作的。

    Transport API 的核心是 Channel 接口,用于所有的出站操作,见下图

    Figure%204

    如上图所示,每个 Channel 都会分配一个 ChannelPipeline 和ChannelConfig。ChannelConfig 负责设置并存储 Channel 的配置,并允许在运行期间更新它们。传输一般有特定的配置设置,可能实现了 ChannelConfig. 的子类型。

    ChannelPipeline 容纳了使用的 ChannelHandler 实例,这些ChannelHandler 将处理通道传递的“入站”和“出站”数据以及事件。ChannelHandler 的实现允许你改变数据状态和传输数据。

    现在我们可以使用 ChannelHandler 做下面一些事情:

    • 传输数据时,将数据从一种格式转换到另一种格式
    • 异常通知
    • Channel 变为 active(活动) 或 inactive(非活动) 时获得通知* Channel 被注册或注销时从 EventLoop 中获得通知
    • 通知用户特定事件

    Intercepting Filter(拦截过滤器)

    ChannelPipeline 实现了常用的 Intercepting Filter(拦截过滤器)设计模式。UNIX管道是另一例子:命令链接在一起,一个命令的输出连接到 的下一行中的输入。

    你还可以在运行时根据需要添加 ChannelHandler 实例到ChannelPipeline 或从 ChannelPipeline 中删除,这能帮助我们构建高度灵活的 Netty 程序。例如,你可以支持 STARTTLS 协议,只需通过加入适当的 ChannelHandler(这里是 SslHandler)到的ChannelPipeline 中,当被请求这个协议时。

    此外,访问指定的 ChannelPipeline 和 ChannelConfig,你能在Channel 自身上进行操作。Channel 提供了很多方法,如下列表:

    Table 4.1 Channel main methods

    方法名称描述
    eventLoop() 返回分配给Channel的EventLoop
    pipeline() 返回分配给Channel的ChannelPipeline
    isActive() 返回Channel是否激活,已激活说明与远程连接对等
    localAddress() 返回已绑定的本地SocketAddress
    remoteAddress() 返回已绑定的远程SocketAddress
    write() 写数据到远程客户端,数据通过ChannelPipeline传输过去
    flush() 刷新先前的数据
    writeAndFlush(...) 一个方便的方法用户调用write(...)而后调用y flush()

    后面会越来越熟悉这些方法,现在只需要记住我们的操作都是在相同的接口上运行,Netty 的高灵活性让你可以以不同的传输实现进行重构。

    写数据到远程已连接客户端可以调用Channel.write()方法,如下代码:

    Listing 4.5 Writing to a channel

    Channel channel = ...; // 获取channel的引用
    ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8);            //1
    ChannelFuture cf = channel.writeAndFlush(buf); //2
    
    cf.addListener(new ChannelFutureListener() {    //3
        @Override
        public void operationComplete(ChannelFuture future) {
            if (future.isSuccess()) {                //4
                System.out.println("Write successful");
            } else {
                System.err.println("Write error");    //5
                future.cause().printStackTrace();
            }
        }
    });
    

    1.创建 ByteBuf 保存写的数据

    2.写数据,并刷新

    3.添加 ChannelFutureListener 即可写操作完成后收到通知,

    4.写操作没有错误完成

    5.写操作完成时出现错误

    Channel 是线程安全(thread-safe)的,它可以被多个不同的线程安全的操作,在多线程环境下,所有的方法都是安全的。正因为 Channel 是安全的,我们存储对Channel的引用,并在学习的时候使用它写入数据到远程已连接的客户端,使用多线程也是如此。下面的代码是一个简单的多线程例子:

    Listing 4.6 Using the channel from many threads

    final Channel channel = ...; // 获取channel的引用
    final ByteBuf buf = Unpooled.copiedBuffer("your data",
            CharsetUtil.UTF_8).retain();    //1
    Runnable writer = new Runnable() {        //2
        @Override
        public void run() {
            channel.writeAndFlush(buf.duplicate());
        }
    };
    Executor executor = Executors.newCachedThreadPool();//3
    
    //写进一个线程
    executor.execute(writer);        //4
    
    //写进另外一个线程
    executor.execute(writer);        //5
    

    1.创建一个 ByteBuf 保存写的数据

    2.创建 Runnable 用于写数据到 channel

    3.获取 Executor 的引用使用线程来执行任务

    4.手写一个任务,在一个线程中执行

    5.手写另一个任务,在另一个线程中执行

    Netty中包含的 Transport

    2020-11-17 17:07 更新

    虽然Netty不能支持所有的传输协议,但是Netty自身是携带了一些传输协议的,这些Netty自带的传输协议已经能够满足我们的使用。Netty应用程序的传输协议依赖的是底层协议,接下来我们学习的内容就是Netty中包含的传输协议。

    Netty中的传输方式有如下几种:

    Table 4.1 Provided transports

    方法名称描述
    NIO io.netty.channel.socket.nio 基于java.nio.channels的工具包,使用选择器作为基础的方法。
    OIO io.netty.channel.socket.oio 基于java.net的工具包,使用阻塞流。
    Local io.netty.channel.local 用来在虚拟机之间本地通信。
    Embedded io.netty.channel.embedded 嵌入传输,它允许在没有真正网络的传输中使用 ChannelHandler,可以非常有用的来测试ChannelHandler的实现。

    NIO-Nonblocking I/O

    NIO传输是目前最常用的方式,它通过使用选择器提供了完全异步的方式操作所有的 I/O,NIO 从Java 1.4才被提供。

    NIO 中,我们可以注册一个通道或获得某个通道的改变的状态,通道状态有下面几种改变:

    • 一个新的 Channel 被接受并已准备好
    • Channel 连接完成
    • Channel 中有数据并已准备好读取
    • Channel 发送数据出去

    处理完改变的状态后需重新设置他们的状态,用一个线程来检查是否有已准备好的 Channel,如果有则执行相关事件。在这里可能只同时一个注册的事件而忽略其他的。选择器所支持的操作在 SelectionKey 中定义,具体如下:

    Table 4.2 Selection operation bit-set

    方法名称描述
    OP_ACCEPT 有新连接时得到通知
    OP_CONNECT 连接完成后得到通知
    OP_READ 准备好读取数据时得到通知
    OP_WRITE 写入更多数据到通道时得到通知,大部分时间

    这是可能的,但有时 socket 缓冲区完全填满了。这通常发生在你写数据的速度太快了超过了远程节点的处理能力。

    Figure 4.2 Selecting and Processing State Changes

    Figure%204

    1.新信道注册 WITH 选择器

    2.选择处理的状态变化的通知

    3.以前注册的通道

    4.Selector.select()方法阻塞,直到新的状态变化接收或配置的超时 已过

    5.检查是否有状态变化

    6.处理所有的状态变化

    7.在选择器操作的同一个线程执行其他任务

    有一种功能,目前仅适用于 NIO 传输叫什么 “zero-file-copy (零文件拷贝)”,这使您能够快速,高效地通过移动数据到从文件系统传输内容 网络协议栈而无需复制从内核空间到用户空间。这可以使 FT P或 HTTP 协议有很大的不同。

    然而,并非所有的操作系统都支持此功能。此外,你不能用它实现数据加密或压缩文件系统 - 仅支持文件的原生内容。另一方面,传送的文件原本已经加密的是完全有效的。

    接下来,我们将讨论的是 OIO ,它提供了一个阻塞传输。

    OIO-Old blocking I/O

    Netty 中,该 OIO 传输代表了一种妥协。它通过了 Netty 的通用 API 访问但不是异步,而是构建在 java.net 的阻塞实现上。任何人下面讨论这一点可能会认为,这个协议并没有很大优势。但它确实有它有效的用途。

    假设你需要的端口使用该做阻塞调用库(例如 JDBC)。它可能不适合非阻塞。相反,你可以在短期内使用 OIO 传输,后来移植到纯异步的传输上。让我们看看它是如何工作的。

    在 java.net API,你通常有一个线程接受新的连接到达监听在ServerSocket,并创建一个新的线程来处理新的 Socket 。这是必需的,因为在一个特定的 socket的每个 I/O 操作可能会阻塞在任何时间。在一个线程处理多个 socket 易造成阻塞操作,一个 socket 占用了所有的其他人。

    鉴于此,你可能想知道 Netty 是如何用相同的 API 来支持 NIO 的异步传输。这里的 Netty 利用了 SO_TIMEOUT 标志,可以设置在一个 Socket。这 timeout 指定最大 毫秒 数量 用于等待 I/O 的操作完成。如果操作在指定的时间内失败,SocketTimeoutException 会被抛出。 Netty中捕获该异常并继续处理循环。在接下来的事件循环运行,它再次尝试。像 Netty 的异步架构来支持 OIO 的话,这其实是唯一的办法。当SocketTimeoutException 抛出时,执行 stack trace。

    Figure 4.3 OIO-Processing logic

    Figure%204

    1.线程分配给 Socket

    2.Socket 连接到远程

    3.读操作(可能会阻塞)

    4.读完成

    5.处理可读的字节

    6.执行提交到 socket 的其他任务

    7.再次尝试读

    同个 JVM 内的本地 Transport 通信

    Netty 提供了“本地”传输,为运行在同一个 Java 虚拟机上的服务器和客户之间提供异步通信。此传输支持所有的 Netty 常见的传输实现的 API。

    在此传输中,与服务器 Channel 关联的 SocketAddress 不是“绑定”到一个物理网络地址中,而是在服务器是运行时它被存储在注册表中,当 Channel 关闭时它会注销。由于该传输不是“真正的”网络通信,它不能与其他传输实现互操作。因此,客户端是希望连接到使用本地传输的的服务器时,要注意正确的用法。除此限制之外,它的使用是与其他的传输是相同的。

    内嵌 Transport

    Netty中 还提供了可以嵌入 ChannelHandler 实例到其他的 ChannelHandler 的传输,使用它们就像辅助类,增加了灵活性的方法,使您可以与你的 ChannelHandler 互动。

    该嵌入技术通常用于测试 ChannelHandler 的实现,但它也可用于将功能添加到现有的 ChannelHandler 而无需更改代码。嵌入传输的关键是Channel 的实现,称为“EmbeddedChannel”。

    在第10章中描述了使用 EmbeddedChannel 来测试 ChannelHandlers。

    Netty中的Buffer API

    2018-08-03 11:01 更新

    Buffer API主要包括

    • ByteBuf
    • ByteBufHolder

    Netty 根据 reference-counting(引用计数)来确定何时可以释放 ByteBuf 或 ByteBufHolder 和其他相关资源,从而可以利用池和其他技巧来提高性能和降低内存的消耗。这一点上不需要开发人员做任何事情,但是在开发 Netty 应用程序时,尤其是使用 ByteBuf 和 ByteBufHolder 时,你应该尽可能早地释放池资源。 Netty 缓冲 API 提供了几个优势:

    • 可以自定义缓冲类型
    • 通过一个内置的复合缓冲类型实现零拷贝
    • 扩展性好,比如 StringBuilder
    • 不需要调用 flip() 来切换读/写模式
    • 读取和写入索引分开
    • 方法链
    • 引用计数
    • Pooling(池)

    Netty字节数据的容器ByteBuf

    2022-04-21 09:12 更新

    既然所有的网络通信都是要基于底层的字节流来传输,那么传输所使用的数据接口就要求是效率高得、使用方便的而且容易使用的,NettyByteBuf更好能够达到这些要求。

    ByteBuf 是一个已经经过优化的很好使用的数据容器,字节数据可以有效的被添加到 ByteBuf 中或者也可以从 ByteBuf 中直接获取数据。ByteBuf中有两个索引:一个用来读,一个用来写。这两个索引达到了便于操作的目的。我们可以按顺序的读取数据,也可以通过调整读取数据的索引或者直接将读取位置索引作为参数传递给get方法来重复读取数据。

    ByteBuf 的工作原理

    写入数据到 ByteBuf 后,writerIndex(写入索引)增加写入的字节数。读取字节后,readerIndex(读取索引)也增加读取出的字节数。你可以读取字节,直到写入索引和读取索引处在相同的位置。此时ByteBuf不可读,所以下一次读操作将会抛出 IndexOutOfBoundsException,就像读取数组时越位一样。

    调用 ByteBuf 的以 "read" 或 "write" 开头的任何方法都将自动增加相应的索引。另一方面,"set" 、 "get"操作字节将不会移动索引位置,它们只会在指定的相对位置上操作字节。

    可以给ByteBuf指定一个最大容量值,这个值限制着ByteBuf的容量。任何尝试将写入超过这个值的数据的行为都将导致抛出异常。ByteBuf 的默认最大容量限制是 Integer.MAX_VALUE。

    ByteBuf 类似于一个字节数组,最大的区别是读和写的索引可以用来控制对缓冲区数据的访问。下图显示了一个容量为16的空的 ByteBuf 的布局和状态,writerIndex 和 readerIndex 都在索引位置 0 :

    11385343fbf2b2119fb390fced3374300dd78e19

    ByteBuf 使用模式

    HEAP BUFFER(堆缓冲区)

    最常用的模式是 ByteBuf 将数据存储在 JVM 的堆空间,这是通过将数据存储在数组的实现。堆缓冲区可以快速分配,当不使用时也可以快速释放。它还提供了直接访问数组的方法,通过 ByteBuf.array() 来获取 byte[]数据。 这种方法,正如清单5.1中所示的那样,是非常适合用来处理遗留数据的。

    Listing 5.1 Backing array

    ByteBuf heapBuf = ...;
    if (heapBuf.hasArray()) {                //1
        byte[] array = heapBuf.array();        //2
        int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();                //3
        int length = heapBuf.readableBytes();//4
        handleArray(array, offset, length); //5
    }
    

    1.检查 ByteBuf 是否有支持数组。

    2.如果有的话,得到引用数组。

    3.计算第一字节的偏移量。

    4.获取可读的字节数。

    5.使用数组,偏移量和长度作为调用方法的参数。

    注意:

    • 访问非堆缓冲区 ByteBuf 的数组会导致UnsupportedOperationException, 可以使用 ByteBuf.hasArray()来检查是否支持访问数组。
    • 这个用法与 JDK 的 ByteBuffer 类似

    DIRECT BUFFER(直接缓冲区)

    “直接缓冲区”是另一个 ByteBuf 模式。对象的所有内存分配发生在 堆,对不对?好吧,并非总是如此。在 JDK1.4 中被引入 NIO 的ByteBuffer 类允许 JVM 通过本地方法调用分配内存,其目的是

    • 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
    • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.(详见http://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html.)

    这就解释了为什么“直接缓冲区”对于那些通过 socket 实现数据传输的应用来说,是一种非常理想的方式。如果你的数据是存放在堆中分配的缓冲区,那么实际上,在通过 socket 发送数据之前,JVM 需要将先数据复制到直接缓冲区。

    但是直接缓冲区的缺点是在内存空间的分配和释放上比堆缓冲区更复杂,另外一个缺点是如果要将数据传递给遗留代码处理,因为数据不是在堆上,你可能不得不作出一个副本,如下:

    Listing 5.2 Direct buffer data access

    ByteBuf directBuf = ...
    if (!directBuf.hasArray()) {            //1
        int length = directBuf.readableBytes();//2
        byte[] array = new byte[length];    //3
        directBuf.getBytes(directBuf.readerIndex(), array);        //4    
        handleArray(array, 0, length);  //5
    }
    

    1.检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。

    2.获取可读的字节数

    3.分配一个新的数组来保存字节

    4.字节复制到数组

    5.将数组,偏移量和长度作为参数调用某些处理方法

    显然,这比使用数组要多做一些工作。因此,如果你事前就知道容器里的数据将作为一个数组被访问,你可能更愿意使用堆内存。

    COMPOSITE BUFFER(复合缓冲区)

    最后一种模式是复合缓冲区,我们可以创建多个不同的 ByteBuf,然后提供一个这些 ByteBuf 组合的视图。复合缓冲区就像一个列表,我们可以动态的添加和删除其中的 ByteBuf,JDK 的 ByteBuffer 没有这样的功能。

    Netty 提供了 ByteBuf 的子类 CompositeByteBuf 类来处理复合缓冲区,CompositeByteBuf 只是一个视图。

    警告

    CompositeByteBuf.hasArray() 总是返回 false,因为它可能既包含堆缓冲区,也包含直接缓冲区

    例如,一条消息由 header 和 body 两部分组成,将 header 和 body 组装成一条消息发送出去,可能 body 相同,只是 header 不同,使用CompositeByteBuf 就不用每次都重新分配一个新的缓冲区。下图显示CompositeByteBuf 组成 header 和 body:

    Figure 5.2 CompositeByteBuf holding a header and body

    Figure%205

    下面代码显示了使用 JDK 的 ByteBuffer 的一个实现。两个 ByteBuffer 的数组创建保存消息的组件,第三个创建用于保存所有数据的副本。

    Listing 5.3 Composite buffer pattern using ByteBuffer

    // 使用数组保存消息的各个部分
    ByteBuffer[] message = { header, body };
    
    // 使用副本来合并这两个部分
    ByteBuffer message2 = ByteBuffer.allocate(
            header.remaining() + body.remaining());
    message2.put(header);
    message2.put(body);
    message2.flip();
    

    这种做法显然是低效的;分配和复制操作不是最优的方法,操纵数组使代码显得很笨拙。

    下面看使用 CompositeByteBuf 的改进版本

    Listing 5.4 Composite buffer pattern using CompositeByteBuf

    CompositeByteBuf messageBuf = ...;
    ByteBuf headerBuf = ...; // 可以支持或直接
    ByteBuf bodyBuf = ...; // 可以支持或直接
    messageBuf.addComponents(headerBuf, bodyBuf);
    // ....
    messageBuf.removeComponent(0); // 移除头    //2
    
    for (int i = 0; i < messageBuf.numComponents(); i++) {                        //3
        System.out.println(messageBuf.component(i).toString());
    }
    

    1.追加 ByteBuf 实例的 CompositeByteBuf

    2.删除 索引1的 ByteBuf

    3.遍历所有 ByteBuf 实例。

    清单5.4 所示,你可以简单地把 CompositeByteBuf 当作一个可迭代遍历的容器。 CompositeByteBuf 不允许访问其内部可能存在的支持数组,也不允许直接访问数据,这一点类似于直接缓冲区模式,如图5.5所示。

    Listing 5.5 Access data

    CompositeByteBuf compBuf = ...;
    int length = compBuf.readableBytes();    //1
    byte[] array = new byte[length];        //2
    compBuf.getBytes(compBuf.readerIndex(), array);    //3
    handleArray(array, 0, length);    //4
    

    1.得到的可读的字节数。

    2.分配一个新的数组,数组长度为可读字节长度。

    3.读取字节到数组

    4.使用数组,把偏移量和长度作为参数

    Netty 尝试使用 CompositeByteBuf 优化 socket I/O 操作,消除 原生 JDK 中可能存在的的性能低和内存消耗问题。虽然这是在Netty 的核心代码中进行的优化,并且是不对外暴露的,但是作为开发者还是应该意识到其影响。

    CompositeByteBuf API

    CompositeByteBuf 提供了大量的附加功能超出了它所继承的 ByteBuf。请参阅的 Netty 的 Javadoc 文档 API。

    Netty字节级别的操作

    2020-11-11 09:03 更新

    除了基本的读写操作, ByteBuf 还提供了它所包含的数据的修改方法。

    随机访问索引

    ByteBuf 使用zero-based 的 indexing(从0开始的索引),第一个字节的索引是 0,最后一个字节的索引是 ByteBuf 的 capacity - 1,下面代码是遍历 ByteBuf 的所有字节:

    Listing 5.6 Access data

    ByteBuf buffer = ...;
    for (int i = 0; i < buffer.capacity(); i++) {
        byte b = buffer.getByte(i);
        System.out.println((char) b);
    }
    

    注意通过索引访问时不会推进 readerIndex (读索引)和 writerIndex(写索引),我们可以通过 ByteBuf 的 readerIndex(index) 或 writerIndex(index) 来分别推进读索引或写索引

    顺序访问索引

    ByteBuf 提供两个指针变量支付读和写操作,读操作是使用 readerIndex(),写操作时使用 writerIndex()。这和JDK的ByteBuffer不同,ByteBuffer只有一个方法来设置索引,所以需要使用 flip() 方法来切换读和写模式。

    ByteBuf 一定符合:0 <= readerIndex <= writerIndex <= capacity。

    Figure 5.3 ByteBuf internal segmentation

    Figure%205

    1.字节,可以被丢弃,因为它们已经被读

    2.还没有被读的字节是:“readable bytes(可读字节)”

    3.空间可加入多个字节的是:“writeable bytes(写字节)”

    可丢弃字节的字节

    标有“可丢弃字节”的段包含已经被读取的字节。他们可以被丢弃,通过调用discardReadBytes() 来回收空间。这个段的初始大小存储在readerIndex,为 0,当“read”操作被执行时递增(“get”操作不会移动 readerIndex)。

    图5.4示出了在 图5.3 中的缓冲区中调用 discardReadBytes() 所示的结果。你可以看到,在丢弃字节段的空间已变得可用写。需要注意的是不能保证对可写的段之后的内容在 discardReadBytes() 方法之后已经被调用。

    Figure 5.4 ByteBuf after discarding read bytes.

    Figure%205

    1.字节尚未被读出(readerIndex 现在 0)。 2.可用的空间,由于空间被回收而增大。

    ByteBuf.discardReadBytes() 可以用来清空 ByteBuf 中已读取的数据,从而使 ByteBuf 有多余的空间容纳新的数据,但是discardReadBytes() 可能会涉及内存复制,因为它需要移动 ByteBuf 中可读的字节到开始位置,这样的操作会影响性能,一般在需要马上释放内存的时候使用收益会比较大。

    可读字节

    ByteBuf 的“可读字节”分段存储的是实际数据。新分配,包装,或复制的缓冲区的 readerIndex 的默认值为 0 。任何操作,其名称以 "read" 或 "skip" 开头的都将检索或跳过该数据在当前 readerIndex ,并且通过读取的字节数来递增。

    如果所谓的读操作是一个指定 ByteBuf 参数作为写入的对象,并且没有一个目标索引参数,目标缓冲区的 writerIndex 也会增加了。例如:

    readBytes(ByteBuf dest);
    

    如果试图从缓冲器读取已经用尽的可读的字节,则抛出IndexOutOfBoundsException。清单5.8显示了如何读取所有可读字节。

    Listing 5.7 Read all data

    //遍历缓冲区的可读字节
    ByteBuf buffer= ...;
    while (buffer.isReadable()) {
        System.out.println(buffer.readByte());
    }
    

    这段是未定义内容的地方,准备好写。一个新分配的缓冲区的 writerIndex 的默认值是 0 。任何操作,其名称 "write"开头的操作在当前的 writerIndex 写入数据时,递增字节写入的数量。如果写操作的目标也是 ByteBuf ,且未指定源索引,则源缓冲区的 readerIndex 将增加相同的量。例如:

    writeBytes(ByteBuf dest);
    

    如果试图写入超出目标的容量,则抛出 IndexOutOfBoundException。

    下面的例子展示了填充随机整数到缓冲区中,直到耗尽空间。该方法writableBytes() 被用在这里确定是否存在足够的缓冲空间。

    Listing 5.8 Write data

    //填充随机整数到缓冲区中
    ByteBuf buffer = ...;
    while (buffer.writableBytes() >= 4) {
        buffer.writeInt(random.nextInt());
    }
    

    索引管理

    在 JDK 的 InputStream 定义了 mark(int readlimit) 和 reset()方法。这些是分别用来标记流中的当前位置和复位流到该位置。

    同样,您可以设置和重新定位ByteBuf readerIndex 和 writerIndex 通过调用 markReaderIndex(), markWriterIndex(), resetReaderIndex() 和 resetWriterIndex()。这些类似于InputStream 的调用,所不同的是,没有 readlimit 参数来指定当标志变为无效。

    您也可以通过调用 readerIndex(int) 或 writerIndex(int) 将指标移动到指定的位置。在尝试任何无效位置上设置一个索引将导致 IndexOutOfBoundsException 异常。

    调用 clear() 可以同时设置 readerIndex 和 writerIndex 为 0。注意,这不会清除内存中的内容。让我们看看它是如何工作的。 (图5.5图重复5.3 )

    Figure 5.5 Before clear() is called

    Figure%205

    调用之前,包含3个段,下面显示了调用之后

    Figure 5.6 After clear() is called

    Figure%205

    现在 整个 ByteBuf 空间都是可写的了。

    clear() 比 discardReadBytes() 更低成本,因为他只是重置了索引,而没有内存拷贝。

    查询操作

    有几种方法,以确定在所述缓冲器中的指定值的索引。最简单的是使用 indexOf() 方法。更复杂的搜索执行以 ByteBufProcessor 为参数的方法。这个接口定义了一个方法,boolean process(byte value),它用来报告输入值是否是一个正在寻求的值。

    ByteBufProcessor 定义了很多方便实现共同目标值。例如,假设您的应用程序需要集成所谓的“Flash sockets”,将使用 NULL 结尾的内容。调用

    forEachByte(ByteBufProcessor.FIND_NUL)
    

    通过减少的,因为少量的 “边界检查”的处理过程中执行了,从而使 消耗 Flash 数据变得 编码工作量更少、效率更高。

    下面例子展示了寻找一个回车符,\ r的一个例子。

    Listing 5.9 Using ByteBufProcessor to find \r

    ByteBuf buffer = ...;
    int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
    

    衍生的缓冲区

    “衍生的缓冲区”是代表一个专门的展示 ByteBuf 内容的“视图”。这种视图是由 duplicate(), slice(), slice(int, int),readOnly(), 和 order(ByteOrder) 方法创建的。所有这些都返回一个新的 ByteBuf 实例包括它自己的 reader, writer 和标记索引。然而,内部数据存储共享就像在一个 NIO 的 ByteBuffer。这使得衍生的缓冲区创建、修改其 内容,以及修改其“源”实例更廉价。

    ByteBuf 拷贝

    如果需要已有的缓冲区的全新副本,使用 copy() 或者 copy(int, int)。不同于派生缓冲区,这个调用返回的 ByteBuf 有数据的独立副本。

    若需要操作某段数据,使用 slice(int, int),下面展示了用法:

    Listing 5.10 Slice a ByteBuf

    Charset utf8 = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1
    
    ByteBuf sliced = buf.slice(0, 14);          //2
    System.out.println(sliced.toString(utf8));  //3
    
    buf.setByte(0, (byte) 'J');                 //4
    assert buf.getByte(0) == sliced.getByte(0);
    

    1.创建一个 ByteBuf 保存特定字节串。

    2.创建从索引 0 开始,并在 14 结束的 ByteBuf 的新 slice。

    3.打印 Netty in Action

    4.更新索引 0 的字节。

    5.断言成功,因为数据是共享的,并以一个地方所做的修改将在其他地方可见。

    下面看下如何将一个 ByteBuf 段的副本不同于 slice。

    Listing 5.11 Copying a ByteBuf

    Charset utf8 = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);     //1
    
    ByteBuf copy = buf.copy(0, 14);               //2
    System.out.println(copy.toString(utf8));      //3
    
    buf.setByte(0, (byte) 'J');                   //4
    assert buf.getByte(0) != copy.getByte(0);
    

    1.创建一个 ByteBuf 保存特定字节串。

    2.创建从索引0开始和 14 结束 的 ByteBuf 的段的拷贝。

    3.打印 Netty in Action

    4.更新索引 0 的字节。

    5.断言成功,因为数据不是共享的,并以一个地方所做的修改将不影响其他。

    代码几乎是相同的,但所 衍生的 ByteBuf 效果是不同的。因此,使用一个 slice 可以尽可能避免复制内存。

    读/写操作

    读/写操作主要由2类:

    • get()/set() 操作从给定的索引开始,保持不变
    • read()/write() 操作从给定的索引开始,与字节访问的数量来适用,递增当前的写索引或读索引

    ByteBuf 的各种读写方法或其他一些检查方法可以看 ByteBuf 的 API,下面是常见的 get() 操作:

    Table 5.1 get() operations

    方法名称描述
    getBoolean(int) 返回当前索引的 Boolean 值
    getByte(int) getUnsignedByte(int) 返回当前索引的(无符号)字节
    getMedium(int) getUnsignedMedium(int) 返回当前索引的 (无符号) 24-bit 中间值
    getInt(int) getUnsignedInt(int) 返回当前索引的(无符号) 整型
    getLong(int) getUnsignedLong(int) 返回当前索引的 (无符号) Long 型
    getShort(int) getUnsignedShort(int) 返回当前索引的 (无符号) Short 型
    getBytes(int, ...) 字节

    常见 set() 操作如下

    Table 5.2 set() operations

    方法名称描述
    setBoolean(int, boolean) 在指定的索引位置设置 Boolean 值
    setByte(int, int) 在指定的索引位置设置 byte 值
    setMedium(int, int) 在指定的索引位置设置 24-bit 中间 值
    setInt(int, int) 在指定的索引位置设置 int 值
    setLong(int, long) 在指定的索引位置设置 long 值
    setShort(int, int) 在指定的索引位置设置 short 值

    下面是用法:

    Listing 5.12 get() and set() usage

    Charset utf8 = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);    //1
    System.out.println((char)buf.getByte(0));                    //2
    
    int readerIndex = buf.readerIndex();                        //3
    int writerIndex = buf.writerIndex();
    
    buf.setByte(0, (byte)'B');                            //4
    
    System.out.println((char)buf.getByte(0));                    //5
    assert readerIndex == buf.readerIndex();                    //6
    assert writerIndex ==  buf.writerIndex();
    

    1.创建一个新的 ByteBuf 给指定 String 保存字节

    2.打印的第一个字符,N

    3.存储当前 readerIndex 和 writerIndex

    4.更新索引 0 的字符B

    5.打印出的第一个字符,现在B

    6.这些断言成功,因为这些操作永远不会改变索引

    现在,让我们来看看 read() 操作,对当前 readerIndex 或 writerIndex 进行操作。这些用于从 ByteBuf 读取就好像它是一个流。 (对应的 write() 操作用于“追加”到 ByteBuf )。下面展示了常见的  read() 方法。

    Table 5.3 read() operations

    方法名称描述
    readBoolean()   Reads the Boolean value at the current readerIndex and increases the readerIndex by 1.
    readByte() readUnsignedByte()  Reads the (unsigned) byte value at the current readerIndex and increases the readerIndex by 1.
    readMedium() readUnsignedMedium()  Reads the (unsigned) 24-bit medium value at the current readerIndex and increases the readerIndex by 3.
    readInt() readUnsignedInt()  Reads the (unsigned) int value at the current readerIndex and increases the readerIndex by 4.
    readLong() readUnsignedLong()   Reads the (unsigned) int value at the current readerIndex and increases the readerIndex by 8.
    readShort() readUnsignedShort()  Reads the (unsigned) int value at the current readerIndex and increases the readerIndex by 2.
    readBytes(int,int, ...) Reads the value on the current readerIndex for the given length into the given object. Also increases the readerIndex by the length.

    每个 read() 方法都对应一个 write()。

    Table 5.4 Write operations

    方法名称描述
    writeBoolean(boolean)  Writes the Boolean value on the current writerIndex and increases the writerIndex by 1.
    writeByte(int)  Writes the byte value on the current writerIndex and increases the writerIndex by 1.
    writeMedium(int)  Writes the medium value on the current writerIndex and increases the writerIndex by 3.
    writeInt(int)  Writes the int value on the current writerIndex and increases the writerIndex by 4.
    writeLong(long)  Writes the long value on the current writerIndex and increases the writerIndex by 8.
    writeShort(int)  Writes the short value on the current writerIndex and increases thewriterIndex by 2.
    writeBytes(int,...)  Transfers the bytes on the current writerIndex from given resources.

    Listing 5.13 read()/write() operations on the ByteBuf

    Charset utf8 = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);    //1
    System.out.println((char)buf.readByte());                    //2
    
    int readerIndex = buf.readerIndex();                        //3
    int writerIndex = buf.writerIndex();                        //4
    
    buf.writeByte((byte)'?');                            //5
    
    assert readerIndex == buf.readerIndex();
    assert writerIndex != buf.writerIndex();
    

    1.创建一个新的 ByteBuf 保存给定 String 的字节。

    2.打印的第一个字符,N

    3.存储当前的 readerIndex

    4.保存当前的 writerIndex

    5.更新索引0的字符 B

    6.此断言成功,因为 writeByte() 在 5 移动了 writerIndex

    更多操作

    Table 5.5 Other useful operations

    方法名称描述
    isReadable() Returns true if at least one byte can be read.
    isWritable() Returns true if at least one byte can be written.
    readableBytes() Returns the number of bytes that can be read.
    writablesBytes() Returns the number of bytes that can be written.
    capacity() Returns the number of bytes that the ByteBuf can hold. After this it will try to expand again until maxCapacity() is reached.
    maxCapacity() Returns the maximum number of bytes the ByteBuf can hold.
    hasArray() Returns true if the ByteBuf is backed by a byte array.
    array() Returns the byte array if the ByteBuf is backed by a byte array, otherwise throws an

    UnsupportedOperationException.

    Netty之ByteBufHolder的使用

    2018-08-08 10:33 更新

    我们时不时的会遇到这样的情况:即需要另外存储除有效的实际数据各种属性值。HTTP响应就是一个很好的例子;与内容一起的字节的还有状态码,cookies等

    Netty 提供的 ByteBufHolder 可以对这种常见情况进行处理。 ByteBufHolder 还提供了对于 Netty 的高级功能,如缓冲池,其中保存实际数据的 ByteBuf 可以从池中借用,如果需要还可以自动释放。

    ByteBufHolder 有那么几个方法。到底层的这些支持接入数据和引用计数。表5.7所示的方法(忽略了那些从继承 ReferenceCounted 的方法)。

    Table 5.7 ByteBufHolder operations

    名称描述
    data() 返回 ByteBuf 保存的数据
    copy() 制作一个 ByteBufHolder 的拷贝,但不共享其数据(所以数据也是拷贝).

    如果你想实现一个“消息对象”有效负载存储在 ByteBuf,使用ByteBufHolder 是一个好主意。

    Netty之ByteBuf 分配

    2018-08-08 10:34 更新

    本节介绍 ByteBuf 实例管理的几种方式:

    ByteBufAllocator

    为了减少分配和释放内存的开销,Netty 通过支持池类 ByteBufAllocator,可用于分配的任何 ByteBuf 我们已经描述过的类型的实例。是否使用池是由应用程序决定的,表5.8列出了 ByteBufAllocator 提供的操作。

    Table 5.8 ByteBufAllocator methods

    名称描述
    buffer() buffer(int) buffer(int, int) Return a ByteBuf with heap-based or direct data storage.
    heapBuffer() heapBuffer(int) heapBuffer(int, int) Return a ByteBuf with heap-based storage.
    directBuffer() directBuffer(int) directBuffer(int, int) Return a ByteBuf with direct storage.
    compositeBuffer() compositeBuffer(int) heapCompositeBuffer() heapCompositeBuffer(int) directCompositeBuffer()directCompositeBuffer(int) Return a CompositeByteBuf that can be expanded by adding heapbased or direct buffers.
    ioBuffer() Return a ByteBuf that will be used for I/O operations on a socket.

    通过一些方法接受整型参数允许用户指定 ByteBuf 的初始和最大容量值。你可能还记得,ByteBuf 存储可以扩大到其最大容量。

    得到一个 ByteBufAllocator 的引用很简单。你可以得到从 Channel (在理论上,每 Channel 可具有不同的 ByteBufAllocator ),或通过绑定到的 ChannelHandler 的 ChannelHandlerContext 得到它,用它实现了你数据处理逻辑。

    下面的列表说明获得 ByteBufAllocator 的两种方式。

    Listing 5.15 Obtain ByteBufAllocator reference

    Channel channel = ...;
    ByteBufAllocator allocator = channel.alloc(); //1
    ....
    ChannelHandlerContext ctx = ...;
    ByteBufAllocator allocator2 = ctx.alloc(); //2
    ...
    

    1.从 channel 获得 ByteBufAllocator

    2.从 ChannelHandlerContext 获得 ByteBufAllocator

    Netty 提供了两种 ByteBufAllocator 的实现,一种是 PooledByteBufAllocator,用ByteBuf 实例池改进性能以及内存使用降到最低,此实现使用一个“jemalloc”内存分配。其他的实现不池化 ByteBuf 情况下,每次返回一个新的实例。

    Netty 默认使用 PooledByteBufAllocator,我们可以通过 ChannelConfig 或通过引导设置一个不同的实现来改变。更多细节在后面讲述 ,见 Chapter 9, "Bootstrapping Netty Applications"

    Unpooled (非池化)缓存

    当未引用 ByteBufAllocator 时,上面的方法无法访问到 ByteBuf。对于这个用例 Netty 提供一个实用工具类称为 Unpooled,,它提供了静态辅助方法来创建非池化的 ByteBuf 实例。表5.9列出了最重要的方法

    Table 5.9 Unpooled helper class

    名称描述
    buffer() buffer(int) buffer(int, int) Returns an unpooled ByteBuf with heap-based storage
    directBuffer() directBuffer(int) directBuffer(int, int) Returns an unpooled ByteBuf with direct storage
    wrappedBuffer() Returns a ByteBuf, which wraps the given data.
    copiedBuffer() Returns a ByteBuf, which copies the given data

    在 非联网项目,该 Unpooled 类也使得它更容易使用的 ByteBuf API,获得一个高性能的可扩展缓冲 API,而不需要 Netty 的其他部分的。

    ByteBufUtil

    ByteBufUtil 静态辅助方法来操作 ByteBuf,因为这个 API 是通用的,与使用池无关,这些方法已经在外面的分配类实现。

    也许最有价值的是 hexDump() 方法,这个方法返回指定 ByteBuf 中可读字节的十六进制字符串,可以用于调试程序时打印 ByteBuf 的内容。一个典型的用途是记录一个 ByteBuf 的内容进行调试。十六进制字符串相比字节而言对用户更友好。 而且十六进制版本可以很容易地转换回实际字节表示。

    另一个有用方法是 使用 boolean equals(ByteBuf, ByteBuf),用来比较 ByteBuf 实例是否相等。在 实现自己 ByteBuf 的子类时经常用到。

    Netty引用计数器

    2018-08-03 11:53 更新

    在Netty 4中为 ByteBuf 和 ByteBufHolder(两者都实现了 ReferenceCounted 接口)引入了引用计数器。

    引用计数器本身并不复杂;它能够在特定的对象上跟踪引用的数目,实现了ReferenceCounted 的类的实例会通常开始于一个活动的引用计数器为 1。而如果对象活动的引用计数器大于0,就会被保证不被释放。当数量引用减少到0,将释放该实例。需要注意的是“释放”的语义是特定于具体的实现。最起码,一个对象,它已被释放应不再可用。

    这种技术就是诸如 PooledByteBufAllocator 这种减少内存分配开销的池化的精髓部分。

    Listing 5.16 Reference counting

    Channel channel = ...;
    ByteBufAllocator allocator = channel.alloc(); //1
    ....
    ByteBuf buffer = allocator.directBuffer(); //2
    assert buffer.refCnt() == 1; //3
    ...
    

    1.从 channel 获取 ByteBufAllocator

    2.从 ByteBufAllocator 分配一个 ByteBuf

    3.检查引用计数器是否是 1

    Listing 5.17 Release reference counted object

    ByteBuf buffer = ...;
    boolean released = buffer.release(); //1
    ...
    

    1.release()将会递减对象引用的数目。当这个引用计数达到0时,对象已被释放,并且该方法返回 true。

    如果尝试访问已经释放的对象,将会抛出 IllegalReferenceCountException 异常。

    需要注意的是一个特定的类可以定义自己独特的方式其释放计数的“规则”。 例如,release() 可以将引用计数器直接计为 0 而不管当前引用的对象数目。

    谁负责 release?

    在一般情况下,最后访问的对象负责释放它。在第6章我们会解释 ChannelHandler 和 ChannelPipeline 的相关概念。

  • 相关阅读:
    博客新域名www.tecchen.tech
    [转载]Redis 持久化之RDB和AOF
    基于zookeeper集群的云平台-配置中心的功能设计
    22部漫威电影大合集和观影顺序
    类的嵌套定义、使用以及用头文件实现多个文件编程(匠心之作C++ p105 22 )
    Python的print格式
    Tensorflow2.0+Anaconda + Windows10+cuda10.0+python(转自豌豆代理)
    C语言中用scanf连续输入两个字符类型的问题(转)
    算法和流程图(及N-S流程图)
    git的安装及针对码云的操作(二)
  • 原文地址:https://www.cnblogs.com/hanease/p/16196823.html
Copyright © 2020-2023  润新知