Netty and Java NIO APIs

Netty和Java NIO APIs

本章包括

  • Netty架构
  • 为什么需要非阻塞IO(NIO)
  • 阻塞和非阻塞对比
  • JDK的NIO实现存在的问题,Netty的解决方式

本章介绍Netty,并且专注于Java NIO API的介绍。如果你是刚刚接触JVM中的网络编程,本章是一个很好的开始,同时,对于经验丰富的Java开发者也是很好的复习。如果你对于NIO和NIO2很熟悉,你可以直接跳到第2章,第2章会使得在你的机器上运行Netty并且进行探索Netty。

Netty是一个NIO客户端-服务器端框架,使得可以快速便捷的开发网络应用,例如协议服务器和客户端。Netty提供了一种新的方式来开发便捷和可扩展的网络应用。它抽象了复杂的操作,提供了易用的API,使得业务逻辑和网络操作相分离。因为它是为NIO构建,所以整个Netty API都是异步的。

通常来说,网络操作存在可扩展问题,无论它是基于Netty还是其它的NIO API。Netty的一个关键特点是它的异步特性,本章讨论同步和异步IO来说明异步代码如何解决扩展问题。

对于刚接触网络编程的,本章将概括的介绍网络应用,以及Netty如何帮助实现。将阐述如何使用基础的Java网络API,讨论它的优点和缺点,说明Netty如何解决Java存在的问题,例如Epoll Bug和内存泄露。

在本章的结尾,你将会了解Netty到底是什么,它能提供什么,你将会充分了解Java NIO和异步处理,使得你更好的了解其它章节。

为什么选择Netty?

使用David Wheele的话说:“计算机科学里面的所有问题都可以通过另外一层来简介解决”。作为一个客户端-服务器端NIO框架,Netty提供了这一层次的解决方案。Netty简化了TCP和UD服务器P网络编程,但是你仍然可以使用低层次的API,因为Netty提供了高层次的抽象。

不是所有的网络框架都一样

不要因为Netty简单易用,就意味着由它构建的应用就会存在维护和性能问题。从许多协议的实现,例如FTP、SMTP、HTTP、WebSocket、SPDY和各种各样的基于二进制和文本的历史协议,Netty对于自身的设计十分细微。最终,Netty以易于开发,并且性能、稳定性和可扩展性具佳的形式发布。

一些知名度高的公司包括RedHat,Twitter,Infinispan和开源项目包括HornetQ,Vert.x,Finagle,Akka,Apache Cassandra,Elasticsearch等等使用并且贡献Netty。更确切的说Netty的一些特性是这些项目的需求的结果。在过去的这些年,Netty成为广为人知的、在JVM中被做多使用的网络框架之一,从它被用在一些受欢迎的开源和闭源项目就可以看出。实际上,在2011年,Netty被颁布了Duke选择奖。

同时在2011年,Netty的创建者离开了RedHat,加入了Twitter。从这种情况上来说,Netty独立于任何一个公司,从而简化了它的贡献提交。RedHat和Twitter都在使用Netty,所以在写到这里的时候,这两家是对Netty贡献最大的公司。采用Netty的人不断增加,贡献者也不断增加。Netty使用者社区活跃,项目充满活力。

Netty拥有丰富的特征集

当你粗略过目本书的一些章节之后,你会了解和使用到Netty的许多特征。图1.1强调了Netty支持的传输和协议,并且给出Netty框架的视图。
Netty框架的视图
图1.1模型、传输、协议概览

除了提供了这么多传输和协议,Netty在不同的开发领域还提供了许多好处。
表格1.1Netty改开发者丰富的工具集

开发阶段Netty特点
设计传输层统一的API-阻塞和非阻塞套接字 灵活使用 简单并且功能强大的线程模型 真正的无状态报文套接字支持 逻辑链使得重用更加容易
易用性书写详尽的文档和大量的例子 除了JDK1.6(或者以上)。有些特征只在JDK1.7或者以上中支持。其它一些特征也许需要其它依赖,但这些是可选的
性能更好的吞吐量;比Java API更低的延迟。 由于线程池和重用,消耗更小的资源 最小化没有必要的内存拷贝
鲁棒性不会因为快速、缓慢或者过载单位连接OutOfMemoryError错误 在NIO应用中由于高速的网络导致的读写比率失衡问题不会出现
安全完整的SSL/TLS和StartTLS支持 可以在一些严格的环境中,例如 Applet或者OSGI中运行
社区发布早,经常发布 活跃

除了以上列出的一些特点,Netty同时具有一些特征来解决Java NIO中一些Bug和限制问题,所以你不用担心处理 Java NIO中的这些问题。

现在已经大体上了解Netty的特点,是时候看看异步处理和它背后的思想。NIO和Netty充分使用了异步代码,如果不能理解采取异步代码的影响,就很难的掌握它。在下一章节,我们将学习我们为什么需要异步API。

异步设计

整个Netty API都是异步的。异步处理并不新鲜;这个思想存在有一段时间了。然而,当前普通IO存在一个瓶颈,异步IO处理变得越来越重要。它是如何工作的,不同的模式可以达到什么样的效果?

异步处理鼓励你在处理任务的时候,可以处理其它事情知道被告知任务处理完成,而不是等待任务完成,这样可以更高效的使用资源。在任务执行的人过程中,你可以自由的处理任何事情。

这一部分将传输实现异步API的最常用的两种方式,以及这两种方式之间的不同点。

回调

回调是异步处理中常用的技术。一个回调对象被传递给方法,在方法完成之后执行。在JavaScript中你可能看到过这种模式,回调是该语言的核心。以下代码片段如何使用这一技术来获取数据。
列表1.1回调例子

public interface Fetcher {
void fetchData(FetchCallback callback);
}
public interface FetchCallback {
void onData(Data data);
void onError(Throwable cause);
}
public class Worker {
public void doWork() {

    Fetcher fetcher = ...
fetcher.fetchData(new FetchCallback() {
@Override
public void onData(Data data) {                 (1)
System.out.println("Data received: " + data);
}
@Override
public void onError(Throwable cause) {              (2)
System.err.println("An error accour: " + cause.getMessage());
}
});
...
}
}

(1)在数据被获取到并且没有错误的时候调用
(2)会在在获取数据过程中出现错误的时候被调用

Fetcher.fetchData()方法的参数为FetchCallback ,它将会在数据被获得到或者出现错误的时候被调用。
对于每一种场景,它提供了一个方法:

  • FetchCallback .onData()-获取到数据并且没有错误的时候调用(1)
  • FetchCallback .onError()-在获取数据过程中出现错误的时候调用(2)

你可以将这些方法的调用从调用者线程移动到其它的一些线程。并不能保证FetchCallback 其中的一个方法必定会被调用。

当你将许多异步方法串联起来,并且使用不同的回调,这样存在问题形成套管式代码。许多人认为这种方法对应的代码很难阅读,我认为这只是个人喜好的问题。例如,Node.js,基于JavaScript,越来越流行。它充分利用回调,许多人认为使用它来读、写应用很容易。

Futrues

第二种技术是使用Futrues。Future是一个抽象,代表在未来某个时间点可能获得到的值。Future对象可以持有运算的结果,在计算失败的情况下,也可以存储异常。

Java在java.util.concurrent包中存在Future接口,Executor使用它进行异步处理。

例如,在下一个列表中,当你将Runnable对象传递给ExecutorService.submit()方法时候,将返回Future,你可以使用它来判断执行是否完成。
列表1.2Future例子(ExecutorService中使用)

ExecutorService executor = Executors.newCachedThreadPool();
Runnable task1 = new Runnable() {
    @Override
public void run() {
doSomeHeavyWork();
}
...
}
Callable<Interger> task2 = new Callable() {
@Override
public Integer call() {
return doSomeHeavyWorkWithResul();
}
...
}
Future<?> future1 = executor.submit(task1);   (A)
Future<Integer> future2 = executor.submit(task2); (B)
while (!future1.isDone() || !future2.isDone()) {
...
// do something else
...
}

(A)
(B)

同样,你可以在你自己的API中使用该项技术。你可以使用Future实现Fetcher(像列表1.1中的那样)。
列表1.3使用Future的Fetcher

public interface Fetcher {
Future<Data> fetchData();
}
public class Worker {
public void doWork() {
Fetcher fetcher = ...
Future<Data> future = fetcher.fetchData();
try {
while(!fetcher.isDone()) {            (A)
...
// do something else
}
System.out.println("Data received: " + future.get());
} catch (Throwable cause) {           (B)
System.err.println("An error accour: " +
cause.getMessage());
}
}
}

(A)
(B)

同样,在这里你可以检测fetcher是否完成,做一些其它的工作。有些时候,使用future感觉很丑陋,因为你不得不定时检查Future的状态来看任务是否完成,然而使用回调当任务完成后直接会通知你。
在看完异步操作的两个方法之后,你想知道哪一个是最好的。在这里没有明确的答复。事实上,Netty使用了两者的混合。
下一部分将介绍在JVM中使用阻塞IO,NIO和NIO2来书写网络应用。这些基础知识是学习后续章节的基础,如果你对Java 网络编程很熟悉,快速的浏览下一部分就可以了。

JVM中阻塞和非阻塞IO

web的不断成长使得对于网络应用能够处理扩展性的需求不断增加。在满足这些需求的同时效率很重要。幸运的是Java具有创建高效、可扩展网络应用的工具。尽管Java的早起版本就包括网络的支持,但是直到Java 1.4版本才加入NIO API,通过它可以写出更高效的网络应用。
NIO2在Java 7中才引入,被设计用来书写异步网络代码,同时提供了更高层次的抽象。
在Java中完成网络相关的任务,你可以采取以下两种方式:

  • 使用IO,也叫做阻塞IO
  • 使用NIO,也叫做新的/非阻塞 IO

新的还是非阻塞?
NIO中的N指的是非阻塞而不是新的。NIO存在很长时间了,没有人再叫它新IO。大多人叫它非阻塞IO。


图1.2中展示了阻塞IO如何使用一个专属线程来处理一个连接,这意味着连接和线程之间1:1的关系,同时会受到JVM可以创建线程数目的限制。
Blocking IO
图1.2 Blocking IO
相对的,图1.3展示了如何通过选择器来处理多个连接。
Non-blocking IO
图1.3Non-blocking IO
将这些图牢记,我们来探索阻塞IO和非阻塞IO。我将会使用一个简单的应答服务器来展示IO和NIO之间的区别。一个应答服务器接受客户端的请求并将接收到的内容应答给客户端。

基于IO的EchoServer

这是EchoServer的第一个版本,依赖于阻塞IO使用它来书写网络相关的应用可能是最为普遍的方式,有两个原因:阻塞IO API存在很长一段时间,使用它也十分容易。

除非扩展性问题,一般情况下阻塞IO不会存在问题。以下展示了EchoServer的实现。
列表1.4 EchoServer v1:IO

public class PlainEchoServer {
public void serve(int port) throws IOException {
final ServerSocket socket = new ServerSocket(port); #1
try {
while (true) {
final Socket clientSocket = socket.accept(); #2
System.out.println("Accepted connection from " +
clientSocket);
new Thread(new Runnable() { #3
@Override
public void run() {
try {
BufferedReader reader = new BufferedReader(
new
InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket
.getOutputStream(), true);
while(true) { #4
writer.println(reader.readLine());
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start(); #5
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
#1 绑定端口
#2 阻塞直到新的连接被接受
#3 创建新的线程来处理客户端连接
#4 从客户端读取数据,并且返回给客户端
#5 启动线程

如果你以前使用Java写过网络应用,以上代码应该很熟悉。我们停下来思考一下:这类设计存在什么问题?
我们重新看看这些行:

final Socket clientSocket = socket.accept();
new Thread(new Runnable() {
@Override
public void run() {
...
}
}).start`
);

对于连接上来的每一个客户端都需要一个线程来处理。你也许说可以通过线程池来避免过度的创建线程,但那只是一时起作用。问题的根本依然存在:你可以服务的并发客户端的数目受到同时存活的线程数目的限制。当你的应用需要同时处理成千上万的客户端时,问题就来了。
这个问题在使用NIO的时候就不会存在,EchoServer的NIO版本我将在后面章节介绍。首先,我们来介绍一下NIO的两个核心概念。

NIO基础

Java 7引进的NIO又被称作NIO.2,但你可以使用NIO或者NIO.2。新的API是异步的,它和原先的NIO实现在API和实现上都不同。但是,这些API并不是完全不一样,拥有一些相同的特征。例如,两者都使用ByteBuffer作为数据容器。
ByteBuffer
ByteBuffer对于NIO和Netty都是基础。ByteBuffer可以分配在堆上或者直接分配,直接分配意味着存储在堆空间之外。通常,当传递给Channel的时候,直接缓存更快,但是分配和回收成本较高。对于分配的两种方式,ByteBuffer API是一样的,这样就提供了统一的访问和操作数据的方式。ByteBuffer允许相同的数据在ByteBuffer实例之间共享,不需要任何的内存拷贝。它还支持分割和其它的一些操作来限制数据的可见性。


分割
分割一个ByteBuffer允许创建一个新的ByteBuffer,和原来的ByteBuffer共享数据,但是只暴露部分数据。这样,最小化内存拷贝,但只允许访问部分数据。


ByteBuffer的典型应用包括:

  • 向ByteBuffer中写入数据
  • 调用ByteBuffer.flip()从写模式切换到读模式
  • 从ByteBuffer中读取数据
  • 调用ByteBuffer.clear()或者ByteBuffer.compact()

    当你向ByteBuffer中写入数据的时候,它通过更新缓存中写索引的位置来跟踪写入数据的数量。这也可以手动来完成。
    当你准备读数据的时候,你可以调用ByteBuffer.flip()从写模式切换到读模式。调用ByteBuffer.flip()设置ByteBuffer的限制外置到当前位置,紧接着将当前位置更新为0。通过这种方式,你可以读取ByteBuffer中的所有数据。
    为了再一次写入到ByteBuffer,切换到写模式,然后调用以下两个方法之一:

  • ByteBuffer.clear()-清除整个ByteBuffer

  • ByteBuffer.compact()-只清除那些通过内存拷贝读取的数据

ByteBuffer.compact()将没有读取的数据到ByteBuffer的头部并且调整位置。以下列出了ByteBuffer的典型用法。
列表1.5ByteBuffer的典型用法

Channel inChannel = ....;
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = -1;
do {
bytesRead = inChannel.read(buf); #1
if (bytesRead != -1) {
buf.flip(); #2
while(buf.hasRemaining()){
System.out.print((char) buf.get()); #3
}
buf.clear(); #4
}
} while (bytesRead != -1);
inChannel.close();
#1从Channel中读取数据到ByteBuffer
#2 缓存做好写准备
#3 读取ByteBuffer中的字节,每一个get()操作使得位置加1
#4 使得ByteBuffer在再次准备写操作

已经大体上了解了ByteBuffer如何使用,我们继续了解选择器的概念。

使用NIO选择器
NIO API依然是最为广泛使用的NIO API,使用基于选择器的方式来处理网络事件和数据。
通道代表与实体的连接,这里的实体指的是能够执行IO操作的实体,例如文件或者套接字。
选择器这个NIO组件决定一个或者多个通道是否准备好了读或者写,因此一个被选中的选择器,可以被用来处理多个连接,避免了前面你所看到的阻塞IO EchoServer例子中的每个连接一个线程模型。
为了使用选择器,通常你需要完成以下步骤。

  1. 创建一个或者多个选择器,打开的通道可以在上面注册
  2. 当通道注册之后,你需要指定你感兴趣要监听的事件
  3. 当通道注册之后,你可以调用Selector.select()方法阻塞直到其中的一个时间触发
  • OP_ACCEPT-套接字接受操作位
  • OP_CONNECT-套接字连接操作位
  • OP_READ-读取操作位
  • OP_WRITE-写操作操作位
  1. 当以上方法不在阻塞的时候,你可以获得所有的SelectionKey实例(它持有注册的通道和选择选项的引用)做一些事情。具体要做什么取决于哪些操作已经就绪。在任何时候,一个SelectionKey可以包含多个操作。

我们来一个非阻塞版本的EchoServer来看看它是如何工作的。你将详尽的看到两个NIO的实现。同时你将看到ByteBuffer是两者的基础。

基于NIO的EchoServer

列表1.6 EchoServer v2:NIO

public class PlainNioEchoServer {
public void serve(int port) throws IOException {
System.out.println("Listening for connections on port " + port);
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address); #1
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT); #2
while (true) {
try {
selector.select(); #3
} catch (IOException ex) {
ex.printStackTrace();
// handle in a proper way
break;
}
Set readyKeys = selector.selectedKeys(); #4
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey) iterator.next();
iterator.remove(); #5
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel)
key.channel();
SocketChannel client = server.accept(); #6
System.out.println("Accepted connection from " +
client);
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_WRITE |
SelectionKey.OP_READ, ByteBuffer.allocate(100)); #7
}
if (key.isReadable()) { #8
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
client.read(output); #9
}
if (key.isWritable()) { #10
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
client.write(output); #11
output.compact();
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
}
}
}
}
}
}
#1 绑定服务器端口
#2 这侧通道到选择器来监听客户端连接接收事件
#3 阻塞直到一些事件被选定
#4 获取所有的SelectedKey实例
#5 从迭代器中移除当前key
#6 接收客户端连接
#7 注册通道到选择器,并且添加附件
#8 检核SelectedKey读取
#9 读取数据到ByteBuffer
#10 检核SelectedKey写入
#11 将ByteBuffer写入到通道中

这个例子要比前一个版本的Echoserver要复杂。这是要付出的代价,异步代码通常要比同步部分要复杂。
从语义上看原始的NIO和新的NIO.2 API相似,它它们的实现不同,我们将在EchoServer的第三个版本的实现中看看它们的不同。

基于NIO.2的EchoServer

public class PlainNio2EchoServer {
public void serve(int port) throws IOException {
System.out.println("Listening for connections on port " + port);
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(port);
serverChannel.bind(address); #1
final CountDownLatch latch = new CountDownLatch(1);
serverChannel.accept(null, new
CompletionHandler<AsynchronousSocketChannel, Object>() { #2
@Override
public void completed(final AsynchronousSocketChannel channel,
Object attachment) {
    serverChannel.accept(null, this); #3
ByteBuffer buffer = ByteBuffer.allocate(100);
channel.read(buffer, buffer,
new EchoCompletionHandler(channel)); #4
}
@Override
public void failed(Throwable throwable, Object attachment) {
try {
serverChannel.close(); #5
} catch (IOException e) {
// ingnore on close
} finally {
latch.countDown();
}
}
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private final class EchoCompletionHandler implements
CompletionHandler<Integer, ByteBuffer> {
private final AsynchronousSocketChannel channel;
EchoCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
channel.write(buffer, buffer, new CompletionHandler<Integer,
ByteBuffer>() { #6
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (buffer.hasRemaining()) {
channel.write(buffer, buffer, this); #7
} else {
buffer.compact();
channel.read(buffer, buffer,
EchoCompletionHandler.this); #8
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
}
}
#1 绑定服务器端口
#2 开始接受新的连接,一旦接受CompletionHandler会被调用
#3 再次接收客户端连接
#4 触发通道上的读操作,一旦有数据读入,CompletionHandler 会被调用
#5 在出现错误的情况下关闭套接字
#6 触发写操作,一旦有数据写入,相应的CompletionHandler会被调用
#7 再次触发写操作
#8 触发通道上的读操作

乍一看,这一实现要比前一版本的NIO、实现代码要多得多。但注意到NIO.2处理了线程和所谓的事件轮训的创建。这一方式简化了多线程NIO应用的代码,尽管在这个例子中看上去不像。当应用程序的复杂度增加的时候,这种方式获益更加明显,因为你将创建干净的代码。
在下一部分,我们将看一看JDK NIO实现中存在的一些问题。

NIO存在的问题和Netty如何处理这些问题

在这一部分,我们将看看Java NIO API中的一些问题和限制,Netty如何处理这些问题。拥有JDK的NIO包是在正确方向上很重要的一步,但是用户如何使用它们是受限制的。这些问题是过去早期设计的结果现在不容易更改,其它一些就是缺陷。

跨平台和兼容性问题

NIO是低层次的,依赖于操作系统来处理IO。所以满足Java中统一API,在所有的系统上行为一致并不容易。
当你使用NIO的时候,你经常发现你的代码在Linux上运行正常,但是在Windows系统上存在问题。我的建议是:即使你不使用NIO,如果使用的话这条建议更重要,在所有你想支持和使用的操作系统上测试。即使所有的测试在Linux工作站上通过测试,一定要在其它操作系统上检核。如果你不进行检核,那你准备以后处理吧。
NIO.2看起来很完美,它仅仅在Java 7中支持,如果你的应用运行在Java 6上,你将不能使用它。到目前为止,数目报文通道在NIO.2 API中不存在,所以目前它只局限于TCP应用使用。
Netty通过提供统一的API来解决这些问题,这样就允许相同的代码在Java 6或者Java 7中无缝运行。你不用担心底层的版本,你将受益于简单、一致的API。

继承ByteBuffer?

像你看到那样,ByteBuffer被用作数据容器。不幸的是没有允许包装一组ByteBuffer实例的ByteBuffer实现。这一功能当你向见效内存拷贝的时候很有用。如果你想自己重新实现它,别白费力气,ByteBuffer提供的构造函数是私有的,所以继承它是不可能的。
Netty提供了自己的ByteBuffer实现,这样就绕过了这一限制,并且提供了其它一些构造、使用、操作方式,API更加简单。

分散、聚集可能存在内存泄漏

许多通道的实现支持分散和聚集。这一特性允许同时写到多个ByteBuffer或者同时从多个ByteBuffer中读取,并且拥有更好的性能。在这里,OS的核心决定如何写和读,通常由OS的核心决定如何达到最好性能,因为OS的核心更接近于硬件,它知道如何通过最高效的方式来处理。
如果你想讲数据分散到不同的ByteBuffer实例中分别处理,分散、聚集会被经常用到。例如你想将头部和主体部分放在不同的ByteBuffer实例中。

图1.4展示了分散读取是如何被执行的。你将ByteBuffer实例数组传递给ScatteringByteChannel,这样数据就会从通道中分散到这些缓存中。
从通道中分散读取
图1.4从通道中分散读取
聚合写入方式类似,只是数据是写入到通道。你传递了一组ByteBuffer实例数组到GatheringByteChannel.write()方法,数据会从所有的缓存中汇聚到通道中。
图1.5展示了汇聚写入的过程。

汇聚写入通道
图1.5汇聚写入通道
不幸的是,这一特性一直存在内存泄漏问题,会导致一个OutOfMemoryError,直到Java 6更新版本和Java 7中才被解决。当你使用分散/聚集功能的时候,请确定一下你在生产系统中使用的Java的版本。
你也许会问:为什么不升级Java?我同意这样会解决这一问题,但是在现实中升级经常并不可行,因为你的公司可能会限制所有系统所部署的版本。升级实际上不可能。

“著名的”epoll bug

在类似于Linux的操作系统上,选择器利用epoll-IO事件通知设施。这是一项高性能的技术,操作系统可以与网络堆栈异步的工作。不幸的是,即使今天,这个“著名的”epoll bug能够引起选择器的状态无效,导致100%的CPU开销和运转。恢复的唯一方式是回收旧的选择器,将注册的通道转移给新创建的选择器。
这里到底发生了什么?Selector.select() 不再阻塞,并且立即返回,即使没有选中的SelectionKeys。这和JavaDoc中说明的Selector.select() 必须阻塞如果没有险种的SelectionKey。


注意详细信息请参考 https://github.com/netty/netty/issues/327


这一epoll问题的解决方案的范围受到限制,但是Netty试图自动探测并且阻止这一问题。下面列出了epoll bug例子。
列表1.8 Epoll- bug

...
while (true) {
int selected = selector.select(); #1
Set<SelectedKeys> readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator(); #2
while (iterator.hasNext()) { #3
...
... #4
}
}
...
#1 立马返回,并且返回值是0表示任何值没有选中
#2 获取所有的SelectedKeys,Iterator为空因为没有选中任何东西
#3 通过迭代器遍历SelectedKeys,但是没有进入该区域,因为没有选中任何的东西
#4 真正的工作在这里

这段代码的效果是While循环吃内存:

...
while (true) {
}
...

这个值不可能为false,这段代码使得CPU不断运转,吃掉资源。这样可以产生不可思议的副作用,因为它可以吃掉你的CPU,从而阻止其它任何CPU工作。
图1.4展示了Java进程占有所有的CPU。
java吃掉100%的CPU
图1.4java吃掉100%的CPU

这些是使用NIO的时候可能存在的几个问题。不幸的是,在这一领域开发了一些年之后,问题依然有待解决。万幸的是Netty为你处理了这些。

总结

这一章节介绍了Netty的特点,设计和优点。我们还讨论了阻塞IO和非阻塞IO处理的不同,这样能更好理解选择使用非阻塞框架的理由。
你学到了如何使用JDK API来写阻塞和非阻塞模式的网络代码。这里包括新的非阻塞API,到JDK 7中才有。在看了NIO的API实战之后,了解一些可能遇到的问题也很重要。事实上,这是许多人使用Netty的原因:细致的处理工作区和JVM中的问题。
在下一章节,你将会学习Netty API的基础和编程模型,最后,使用Netty来写一些有用的代码。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值