CH01-Netty 概览

什么是 Netty

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。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 = 											//3
  new BufferedReader(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 中创建的线程数。

NAME

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

SELECTOR

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

NAME

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

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

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

整体构成

正如我们前面解释的,非阻塞 I/O 不迫使我们等待完成的操作。在这种能力的基础上,真正的异步 I/O 起到了更进一步的作用:一个异步方法完成时立即返回并直接或稍后通知用户。

正如我们将看到的,在一个网络环境的异步模型可以更有效地利用资源,可以快速连续执行多个调用。

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显示了一个事件可以由一连串的事件处理器来处理

NAME

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的功能,设计和效益的概述。这些包括了Netty 的异步模型,包括回调,future 及其组合使用。我们还谈到了Netty 的线程模型,事件是如何被使用的,以及它们如何被拦截和处理。展望未来,我们将更加深入探索如何使用这些丰富的工具集用来满足特殊需求的应用。

一路上,我们将介绍公司的工程师自己的案例研究解释为什么他们选择的Netty 以及他们如何使用它。

因此,让我们开始吧。在下一章中,我们将深入研究了 Netty 的 API 的基础知识,编程模型,开始写 echo(回声)服务器和客户端。