这一章内容是要介绍Netty,不过大部分内容是介绍Java NIO接口。如果你是JVM网络编程的新手,那么本章将是你学习网络编程优秀的开端,对于经验丰富的Java开发者,也可以令你复习到很多知识。对于有经验的开发者来说,学习本章内容也是很好的复习。如果你已经非常属性Java NIO和NIO2的API,也可以从第二章开始,直接进入Netty框架的学习。
Netty是一个NIO client-server的框架,它可以令开发者简单快速的开发网络应用,例如各种网络协议的服务端和客户端。Netty提供了一种全新的方式开发网络应用,并且有很强的易用性和扩展性。它通过抽象复杂性并且提供简单易用的API来帮助我们从网络处理代码中解耦出业务逻辑。因为使用的是JVM NIO,所以Netty的API也都是异步的。
一般来说,网络应用的可扩展性都不太好,无论是否用Netty还是其他NIO API。Netty一个关键组成部分就是它的异步性,本章会讨论同步(阻塞)和异步(非阻塞)IO来说明异步代码是怎么解决扩展性问题的。
对于网络编程新手,本章会让你对网络应用有一个基本了解和Netty是怎么实现的。还会解释如何使用基本的java网络API,探讨他们的优点和缺点,并展示Netty是怎么处理Java的一些问题,例如注明的epoll错误或内存泄漏等。
学习完本章,你就会知道Netty是什么,它提供了什么功能,并且你将会获得Java NIO和异步处理的知识,这些知识可以帮助你学习本书其他章节的内容。
David Wheeler说过一句话,“计算机科学中的任何问题都可以通过加上一层逻辑层来解决”。作为一个NIO client-server框架,Netty也提供了这样一层逻辑。Netty简化了TCP或UDP服务端编程,不过你依然可以访问和使用底层API因为Netty提供了高层次的抽象。
Netty的“快速和简单”并不意味着开发的应用将要承受可维护性和性能的问题。从各种协议如FTP, SMTP, HTTP, WebSocket, SPDY和其他基于二进制和基于文本的协议实现获取的经验令Netty创始人非常精心它的设计。所以,Netty成功拥有了易于开发,高性能,稳定且灵活等优点,并且没有副作用。
使用并且对Netty有贡献的著名公司和开源项目包括RedHat, Twitter, Infinispan, HornetQ, Vert.x, Finagle, Akka, Apache Cassandra, Elasticsearch等等。这些项目也促进了Netty一些新功能的开发。这些年来,Netty已发展为著名并且是JVM中最常用的网络框架之一,多个非常受欢迎的开源和闭源项目都在用它。另外,Netty也获得了2011年的Duke's Choice Award。
同样在2011年,Netty创始人Trustin Lee离开RedHat加入了Twitter。从这一点来说,Netty独立于任何公司,努力帮助大家简化对它贡献代码。RedHat和Twitter都在使用Netty,所以这两家公司是Netty贡献代码最多的。Netty项目个人贡献者也在不断增长。Netty社区非常活跃并且项目依然充满活力。
当你学习完本书后,你将会学到和使用很多Netty的功能。下图展示了Netty支持的功能以及传输协议,可以让你大致了解Netty架构。
Netty除了支持这些传输协议,还有很多优点,看下面表格:
Design(设计) | 1、各种传输类型、阻塞和非阻塞套接字统一的API 2、使用灵活 3、简单但功能强大的线程模型 4、无连接的DatagramSocket支持 5、链逻辑,易于重用 |
Ease of Use(易于使用) | 1、提供大量的文档和例子 2、除了依赖jdk1.6+,没有额外的依赖关系。某些功能依赖jdk1.7+,其他特性可能有相关依赖,但都是可选的 |
Performance(性能) | 1、比Java APIS更好的吞吐量和更低的延迟 2、因为线程池和重用所有消耗较少的资源 3、尽量减少不必要的内存拷贝 |
Robustness(鲁 棒性) | 1、链接快或慢或超载不会导致更多的OutOfMemoryError 2、在高速的网络程序中不会有不公平的read/write |
Security(安全性) | 1、完整的SSL/TLS和StartTLS支持 2、可以在如Applet或OSGI这些受限制的环境中运行 |
Community(社区) | 1、版本发布频繁 2、社区活跃 |
除了列出的功能和优点外,Netty为Java NIO中的bug和限制也提供了解决方案,所以你就不用烦恼那些问题了。
大致了解了Netty的架构后,现在应该深入了解它的异步逻辑以及背后的思想。我们应该我们需要深刻理解Netty的功能以及它的异步处理机制和它的架构。NIO和Netty都大量使用了异步代码,如果不了解它背后的思想,就很难真正理解Netty。下一小节,我们就学习下为什么网络编程需要异步API。
整个Netty的API都是异步的,异步并不是一个新的机制,这个机制出来已经有一些时间了。对网络应用来说,IO一般是性能的瓶颈,使用异步IO可以较大程度上提高程序性能,因为异步变的越来越重要。但它是如何工作的呢,以及有哪些不同的模式呢?
异步处理提倡更有效的使用资源,你可以创建一个任务,任务完成后通知你而不用你去等待它完成。这样任务在执行的过程中就可以做些其他事情。
本节将简要说明实现异步API的两个最常用的方法,并探讨他们之间的差异。
回调是常用的异步处理技术。回调是作为参数传递给方法并且在方法执行完后调用它。你可能认为这种模式来自Javascript,在Javascript中,回调是它的核心。下面的代码简要展示了Java中如何实现回调技术。
由于Java目前还不支持函数当参数,所以先定义一个回调接口。
package com.nan.netty.callback;
/**
* 读数据回调
*/
public interface ReadCallback {
/**
* 读完数据回调方法
*/
void onData(Object data);
/**
* 出现异常回调
*/
void onError(Throwable cause);
}
再定义一个读取数据类,读取方法接收参数为上面的回调接口。
package com.nan.netty.callback;
public class ReadFile {
private String data;
public ReadFile(String data) {
this.data = data;
}
/**
* 读取数据模拟
*/
public void read(ReadCallback readCallback) {
try {
//读到数据回调
readCallback.onData(data);
} catch (Exception e) {
//出现异常回调
readCallback.onError(e);
}
}
}
最后在工作类中实例读取类并调用读取数据方法。
package com.nan.netty.callback;
public class Worker {
public static void main(String[] args) {
//初始化读取数据对象
ReadFile readFile = new ReadFile("Hello world");
//调用读取方法
readFile.read(new ReadCallback() {
@Override
public void onData(Object data) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("on data : " + data);
}
@Override
public void onError(Throwable cause) {
cause.printStackTrace();
}
});
}
}
回调会产生一个问题,当你的业务复杂时,需要链式调用回调,会产生很多回调嵌套代码。很多人都觉得这种代码是难以阅读的,但我任务这取决于开发者的品味和风格。举个例子,Node.js,基于JavaScript,已经越来越流行。用它编写的应用大量使用了回调,但是大部分人觉得还是比较易于阅读和编写应用的,不过也有一部分人觉得这样不好,所以慢慢就出现了promise、yield、async/await等技术。
另一种异步的技术是使用Futures,Futures是一个抽象的概念,它表示一个值,该值可能在某一点变得可用。一个Future要么获得计算完的结果,要么获得计算失败后的异常。Java在java.util.concurrent包中附带了Future接口,它使用Executor异步执行。例如下面的代码,每传递一个Runnable或Callable对象到ExecutorService.submit()方法就会得到一个回调的Future,你能使用它检测是否执行完成。
package com.nan.netty.future;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureExample {
public static void main(String[] args) throws Exception {
//初始化线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
//异步任务1
Runnable task1 = () -> System.out.println("This is task1");
//异步任务2
Callable task2 = () -> {
Thread.sleep(2000L);
return 1314;
};
//提交任务
Future> f1 = executor.submit(task1);
Future f2 = executor.submit(task2);
System.out.println("task1 is completed? " + f1.isDone());
System.out.println("task2 is completed? " + f2.isDone());
//等待task1完成
while (f1.isDone()) {
System.out.println("task1 completed.");
break;
}
//等待task2完成
System.out.println("task2返回值: " + f2.get());
executor.shutdown();
}
}
有时候你会角色使用Future的代码很难看,因为你需要间隔检查Future是否已完成,而使用回调会直接收到返回通知。
看完这两个常用的异步执行技术后,你可能想知道使用哪个最好?这里没有明确的答案,因为他们都是有各自的使用场景的。事实上,Netty两者都使用,提供两全其美的方案。
下一节将在JVM上首先使用阻塞,然后再使用NIO和NIO2写一个网络程序。这些是本书后续章节必不可少的基础知识,如果你熟悉Java网络AIPs,你可以快速翻阅即可。
Web服务的持续增长就需要网络应用能适应这种增长需求。性能已经成为满足这些需求的重要因素。幸运的是,Java及其相关工具可以创建高效的、可扩展的网络应用。虽然Java最早版本就已经支持开发网络应用,不过到了JDK1.4才有了NIO API,这些给了开发者开发更高效的网络应用的基础。
到了JDK1.7,有了NIO.2的API,它允许开发者使用异步代码开发应用并且试图提供更高级的API。
利用Java开发网络应用,可以采用下面两种方式:
与阻塞IO对比,下图展示了非阻塞IO是怎么使用一个选择器处理多个Socket连接的。
仅仅通过上面两幅图是不够的,现在让我们更深入的了解阻塞IO和非阻塞IO。我将通过一个最常用的服务器例子EchoServer来展示阻塞IO和非阻塞IO之间的区别。EchoServer的逻辑很简单,就是接收客户端的请求,并显示和回传客户端发来的数据。
第一个版本的EchoServer基于阻塞IO,这可能是编写Java网络应用最常用的方式,主要由两方面因素造成:首先最早版本的Java就有阻塞IO的API了,其次阻塞IO API非常简单易用。
除非遇到可伸缩性或高访问量的情况,所以一般情况下用阻塞IO也不会有问题。下面我们就开始实现阻塞IO的EchoServer。
package com.nan.netty.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class PlainEchoServer {
public void serve(int port) throws IOException {
//服务端绑定端口号
final ServerSocket socket = new ServerSocket(port);
try {
while (true) {
//这里会一直阻塞只到有客户端连接进来
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection from " + clientSocket);
//创建一个线程处理连接
new Thread(() -> {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
//这里是服务端处理逻辑,读取客户端传来的数据并写回到客户端
String line = reader.readLine();
System.out.println("Server received:" + line);
writer.println(line);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 主执行方法
*/
public static void main(String[] args) throws IOException {
new PlainEchoServer().serve(9999);
}
}
接下来,再写一个客户端,客户端做的事情很简单,就是去连接服务端并向服务端发送一行字符串,然后读取服务端返回的内容,请看下面的代码。
package com.nan.netty.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class PlainEchoClient {
public void client(int port) throws IOException {
//创建连接到服务端
final Socket socket = new Socket("localhost", port);
//向服务端写入数据
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
writer.write("Hello Server " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + "\n");
writer.flush();
//读取服务端返回的数据并展示
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
writer.close();
reader.close();
socket.close();
System.out.println("Received from server: " + line);
}
/**
* 主执行方法
*/
public static void main(String[] args) throws IOException {
new PlainEchoClient().client(9999);
}
}
先启动服务端,再执行客户端,可以看到服务端正确收到了客户端发来的数据,并且客户端也收到了服务端返回来的数据。
下面列一下Java BIO服务端的核心代码,如果你用Java BIO写过网络应用,应该会很熟悉这套代码。现在我们思考一些:采用这种设计会出现什么问题?
final Socket clientSocket = socket.accept();
new Thread(() -> {
...
}).start();
很明显,每个客户端连接都需要一个线程为它服务。你可能会说可以利用线程池技术来消除创建线程的系统开销,这样做会有一点作用。但是本质问题还在:你的应用能够服务的客户端数量受限于存活的线程数量。当你的应用有成千上万个并发客户端时,这个问题就是个大问题了。
下个版本我们使用NIO实现EchoServer,就不会受到线程数的限制。不过,我们要先来了解NIO中几个重要的概念。
4.2、NIO基础概念
Java7有了一个新的NIO API,名字叫NIO.2,不过你都可以使用。虽然NIO.2也是异步的,不过它的API和实现都不同于早期版本的NIO了。不过,它们的API也不是完全不同,还有很多的共同特征的。例如,它们的数据容器都是使用ByteBuffer。
ByteBuffer
ByteBuffer是NIO和NIO.2的基础,也是Netty的基础。一个ByteBuffer可以分配在堆内存上,也可以分配在直接内存,直接内存也被成为堆外内存,意思就是它不是存储在堆内存上。通常,当传递数据到通道时使用直接内存速度会快一些,但是分配或回收直接内存的成本要高一些。ByteBuffer提供了统一的方式访问和操作数据。ByteBuffer允许同样的数据很容易分享给其他ByteBuffer实例而且不用进行内存复制的操作。未来它还会提供切片和其他操作来限制数据可见性。切片一个ByteBuffer就是创建一个新ByteBuffer并共享原ByteBuffer的数据,只不过新的ByteBuffer只展示部分数据,当访问部分数据时这个操作就可以尽量减少使用内存复制。
ByteBuffer的用途主要包括下面几项:
Channel inChannel = ...;
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = -1;
do {
//从Channel读数据并写入到ByteBuffer
bytesRead = inChannel.read(buf);
if (bytesRead != -1) {
//ByteBuffer切换到读模式
buf.flip();
while(buf.hasRemaining()){
//从ByteBuffer读数据,get方法会将其指向位置加1
System.out.print((char) buf.get());
}
//清空ByteBuffer数据,就可以重新向其写入
buf.clear();
}
} while (bytesRead != -1);
inChannel.close();
ByteBuffer基本的操作就是这些了,现在我们把重心转到选择器上。
无论是NIO还是NIO.2的API,都是使用选择器来处理客户端请求事件和数据。
Channel表示能进行IO操作的实体例如文件或Socket。
选择器就是确定一个或多个通道进行读写操作的NIO组件,因此一个选择器可以处理多个客户端连接,这样就解决了阻塞IO里面一个连接对应一个线程的模型。
使用选择器,一般都要完成下面几步操作。
1.创建一个或多个已经打开的Channel(Socket)可以注册的选择器
2.当一个Channel注册了,你需要指定监听的事件类型。共有四种可以监听的事件类型,如下:
下面这个版本的EchoServer采用了异步的NIO API,它允许你使用一个线程服务成千上万个并发客户端,这相比BIO已经算得上天壤之别了。
package com.nan.netty.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
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);
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
//Channel注册选择器并指定触发事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
//阻塞代码只到触发接受客户端连接事件
selector.select();
} catch (IOException ex) {
ex.printStackTrace();
break;
}
//获取所有的SelectionKey实例
Set readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//移除已经遍历的SelectionKey
iterator.remove();
try {
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//接收客户端连接
SocketChannel client = server.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
//客户端注册选择器并指定ByteBuffer
client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, ByteBuffer.allocate(100));
}
//检查SelectionKey是否可读
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
//读取数据
client.read(output);
String line = Charset.forName("UTF-8").decode(output).toString();
System.out.println("Read from client: " + line);
}
//检查SelectionKey是否可写
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
//直接将ByteBuffer内容回写给客户端
client.write(output);
output.compact();
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
}
}
}
}
}
/**
* 主执行方法
*/
public static void main(String[] args) throws IOException {
new PlainNioEchoServer().serve(9999);
}
}
客户端继续使用上一小节使用的BIO客户端即可,可以看到NIO服务端返回了客户端传来的内容,并且没有启动新线程去处理。
不过NIO版本的EchoServer比BIO的复杂一些。复杂的代码换来的性能提升;通常异步代码就是比同步代码复杂一些。
从语法来说,NIO和NIO.2的API是很相似的,但是它们的实现不同的。接下来我们花点时间了解它们的不同,并实现一个基于NIO.2的EchoServer。
NIO.2和NIO的主要区别就是你不用去检查Channel上发生了哪个事件然后去触发操作。在NIO.2你只需要触发IO操作然后注册一个
CompletionHandler来处理它,这个Handler在操作完成之后就会获得通知。这样你就不用自己写代码去检查操作是否完成这种没有必要的代码。
废话不多说了,下面我们就来用NIO.2的API实现异步的EchoServer。
package com.nan.netty.nio2;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.Charset;
import java.util.concurrent.CountDownLatch;
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);
final CountDownLatch latch = new CountDownLatch(1);
//开始接收客户端连接,一旦连接进来,参数CompletionHandler将会回调
serverChannel.accept(null, new CompletionHandler() {
@Override
public void completed(final AsynchronousSocketChannel channel,
Object attachment) {
//再次接收客户端连接
serverChannel.accept(null, this);
ByteBuffer buffer = ByteBuffer.allocate(100);
//触发读操作,一旦读到内容就会触发EchoCompletionHandler
channel.read(buffer, buffer, new EchoCompletionHandler(channel));
}
@Override
public void failed(Throwable throwable, Object attachment) {
try {
//出现错误时关闭Socket
serverChannel.close();
} catch (IOException e) {
} finally {
latch.countDown();
}
}
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private final class EchoCompletionHandler implements CompletionHandler {
private final AsynchronousSocketChannel channel;
EchoCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
String line = Charset.forName("UTF-8").decode(buffer).toString();
System.out.println("Read from client: " + line);
buffer.flip();
//触发写操作,一旦内容写出就会回调CompletionHandler
channel.write(buffer, buffer, new CompletionHandler() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (buffer.hasRemaining()) {
//如果ByteBuffer还有内容继续触发写操作
channel.write(buffer, buffer, this);
} else {
buffer.compact();
//ByteBuffer内容读完了,重新触发读操作
channel.read(buffer, buffer, EchoCompletionHandler.this);
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
}
}
}
/**
* 主执行方法
*/
public static void main(String[] args) throws IOException {
new PlainNio2EchoServer().serve(9999);
}
}
这里面很多API和使用NIO实现的EchoServer很相似。但是注意一下NIO.2的IO操作事件通过回调通知Handler,而不需要你自己去检查事件是否准备好。这种机制有助于简化编写多线程非阻塞IO应用,尽管上面的例子很简单以至于不容易看出这点。随着应用的负责度增加,你将获得更多的这种机制带来的好处,因为你构建了一个代码干净的应用。
接下来这一小节,我们将讨论下JDK NIO的实现里面隐藏的问题。
本节中将介绍Java NIO的问题,以及Netty是如何解决这些问题的。Java的NIO相对老的IO APIs有着非常大的进步,但是使用NIO是受限制的。这些问题一部分是过去的设计造成的结果,并且现在也不容易去改正,也有一部分是已经的缺陷。
NIO是一个比较底层的APIs,它依赖于操作系统的IO APIs。Java实现了统一的接口来操作IO,其在所有操作系统中的工作行为是一样的,这是很伟大的。
使用NIO会经常发现代码在Linux上正常运行,但在Windows上就会出现问题。我建议如果使用NIO编写程序,就应该在所有的操作系统上进行测试来支持,使程序可以在任何操作系统上正常运行;即使在所有的Linux系统上都测试通过了,也要在其他的操作系统上进行测试。你如果不验证,就需要准备收到一些意外的惊喜~嘿嘿嘿。
NIO2看起来很理想,但是NIO2只支持Jdk1.7+,若你的程序在Java1.6上运行,则无法使用NIO2。另外,Java7的NIO2中没有提供DatagramSocket的支持,所以NIO2只支持TCP程序,不支持UDP程序。
而Netty提供一个统一的接口,同一语义无论在Java6还是Java7的环境下都是可以运行的,开发者无需关心底层APIs就可以轻松实现相关功能。
通过前面的介绍,可以看出ByteBuffer是作为数据容器来使用。不幸的是,JDK的ByteBuffer没有实现通过包装多个ByteBuffer来初始化。如果你希望尽量减少内存拷贝,那么这种方式是非常有用的。若果你想将ByteBuffer重新实现,那么不要浪费你的时间了,ByteBuffer的构造函数是私有的,所以它不能被扩展。
Netty提供了自己的ByteBuffer实现,Netty通过一些简单的APIs对ByteBuffer进行构造、使用和操作,以此来解决NIO中的一些限制。
NIO对缓冲区的聚合和分散操作可能会引起内存泄露,很多Channel的实现支持Gather和Scatter。这个功能允许从从多个ByteBuffer中读入或写入到多个ByteBuffer,这样做可以提高性能。操作系统底层知道如何处理这些被写入/读出,并且能以最有效的方式处理。如果要分割的数据再多个不同的ByteBuffer中,使用Gather/Scatter是比较好的方式。
例如,你可能希望header在一个ByteBuffer中,而body在另外的ByteBuffer中;下图显示的是Scatter(分散),将ScatteringByteBuffer中的数据分散读取到多个ByteBuffer中。
下图显示的是Gather(聚合),将多个ByteBuffer的数据写入到GatheringByteChannel。
可惜Gather/Scatter功能会导致内存泄露,只到Java7才解决内存泄露问题,使用这个功能必须小心编码和Java版本。
还有一个非常著名的bug-epoll bug,Linux-like OSs的选择器使用的是epoll-IO事件通知工具,这是Linux下开发高性能网络程序的一个热门技术,不幸的是,即使是现在,著名的epoll-bug也可能会导致无效的状态的选择和100%的CPU利用率。要解决epoll-bug的唯一方法是回收旧的选择器,将先前注册的通道实例转移到新创建的选择器上。Bug发生的时候,不管有没有已选择的SelectionKey,Selector.select()方法总是不会阻塞并且会立刻返回,这违反了Javadoc中对Selector.select()方法的描述,Selector.select()方法若未选中任何事件将会阻塞。NIO中对epoll问题的解决方案是有限制的,Netty提供了更好的解决方案。下面是epoll-bug的一个例子。
while (true) {
int selected = selector.select();
Set readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
...
...
}
}
...
这段代码的作用是while循环消耗CPU
...
while (true) {
}
下图就展示一下Java进程吃掉了大量CPU资源的情况。
这些仅仅是在使用NIO时可能会出现的一些问题。不幸的是,虽然在这个领域发展了多年,问题依然存在;幸运的是,Netty给了你解决方案。
这一章简要介绍了下Netty的功能、设计以及优点。这一章也讨论了下阻塞IO和非阻塞IO的区别,这是理解为什么使用非阻塞IO框架的基础。
现在,你应该已经学到了如何使用JDK的阻塞IO API和非阻塞IO API编写网络应用。包括JDK7的NIO.2 API。在实际使用中,了解NIO可能出现的问题也是很重要的。事实上,这也是这么多人用Netty的原因:避免陷入JDK的坑中。
在下一章,你将会学到基本的Netty API和编程模型,最后,你就可以用Netty写出一些有用的代码。