Netty4实战第一章:Netty和Java NIO APIs

来源:互联网 发布:c语言函数大全 chm 编辑:程序博客网 时间:2024/05/18 00:25

一、此章内容

  • Netty架构
  • 为什么我们需要非阻塞IO(NIO)
  • 阻塞IO和非阻塞IO对比
  • 了解JDK NIO的问题和Netty的解决方案

  这一章内容是要介绍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和异步处理的知识,这些知识可以帮助你学习本书其他章节的内容。


二、为什么使用Netty?

  David Wheeler说过一句话,“计算机科学中的任何问题都可以通过加上一层逻辑层来解决”。作为一个NIO client-server框架,Netty也提供了这样一层逻辑。Netty简化了TCP或UDP服务端编程,不过你依然可以访问和使用底层API因为Netty提供了高层次的抽象。


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

  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社区非常活跃并且项目依然充满活力。

2.2、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/writeSecurity(安全性)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的两个最常用的方法,并探讨他们之间的差异。

3.1、回调

  回调是常用的异步处理技术。回调是作为参数传递给方法并且在方法执行完后调用它。你可能认为这种模式来自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等技术。

3.2、Futures

  另一种异步的技术是使用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<Integer> task2 = () -> {            Thread.sleep(2000L);            return 1314;        };        //提交任务        Future<?> f1 = executor.submit(task1);        Future<Integer> 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,你可以快速翻阅即可。

四、JVM阻塞IO和非阻塞IO对比

  Web服务的持续增长就需要网络应用能适应这种增长需求。性能已经成为满足这些需求的重要因素。幸运的是,Java及其相关工具可以创建高效的、可扩展的网络应用。虽然Java最早版本就已经支持开发网络应用,不过到了JDK1.4才有了NIO API,这些给了开发者开发更高效的网络应用的基础。

  到了JDK1.7,有了NIO.2的API,它允许开发者使用异步代码开发应用并且试图提供更高级的API。

  利用Java开发网络应用,可以采用下面两种方式:

  • IO,也就是阻塞IO
  • NIO,也就是非阻塞IO
  下图展示了阻塞IO如何使用一个专有线程处理每一个连接,也就是说连接和线程的比例是1:1,因此,应用的最大连接量会受到限制,这个限制就是JVM能够创建的线程数的限制。


  与阻塞IO对比,下图展示了非阻塞IO是怎么使用一个选择器处理多个Socket连接的。


  仅仅通过上面两幅图是不够的,现在让我们更深入的了解阻塞IO和非阻塞IO。我将通过一个最常用的服务器例子EchoServer来展示阻塞IO和非阻塞IO之间的区别。EchoServer的逻辑很简单,就是接收客户端的请求,并显示和回传客户端发来的数据。

4.1、基于阻塞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的用途主要包括下面几项:

  • 向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常用的操作。

        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还是NIO.2的API,都是使用选择器来处理客户端请求事件和数据。
  Channel表示能进行IO操作的实体例如文件或Socket。
  选择器就是确定一个或多个通道进行读写操作的NIO组件,因此一个选择器可以处理多个客户端连接,这样就解决了阻塞IO里面一个连接对应一个线程的模型。
  使用选择器,一般都要完成下面几步操作。
  1.创建一个或多个已经打开的Channel(Socket)可以注册的选择器
  2.当一个Channel注册了,你需要指定监听的事件类型。共有四种可以监听的事件类型,如下:

  • OP_ACCEPT- Scoket接收事件
  • OP_CONNECT- Socket连接事件
  • OP_READ- 读操作
  • OP_WRITE- 写操作
  3.当Channel已经注册了,你可以调用Selector.select()方法阻塞应用只到上面四种事件之一发生
  4.当上面的方法不阻塞应用了,你就会获得所有的SelectionKey实例(这个实例可以获得注册的Channel和触发的事件)并且可以做一些操作。至于能做什么操作取决于哪个事件准备好了。一个SelectionKey实例可以在任何给定时间包含多个操作。
  
  了解了上面的流程后,现在我们来实现一个基于非阻塞IO的EchoServer。这个示例可以帮助你了解更多NIO和NIO.2的实现细节。而且你会发现ByteBuffer是它们必不可少的一部分。

4.3、基于NIO的EchoServer

  下面这个版本的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<SelectionKey> readyKeys = selector.selectedKeys();            Iterator<SelectionKey> 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。

4.4、基于NIO.2的EchoServer

  不同于NIO的实现,NIO.2允许你发出IO操作并提供一个完成操作Handler(CompletionHandlerclass)。这个Handler会在操作完成之后执行。然后,执行此Handler是底层系统驱动的,向开发者隐藏了实现细节。它也保证了Channel同一时间只有一个CompletionHandler在执行。这有利于简化代码,因为它不会有多线程并发执行的问题。

  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<AsynchronousSocketChannel, Object>() {            @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<Integer, ByteBuffer> {        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<Integer, ByteBuffer>() {                @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的实现里面隐藏的问题。

五、NIO的问题和Netty是如何解决这些问题的

  本节中将介绍Java NIO的问题,以及Netty是如何解决这些问题的。Java的NIO相对老的IO APIs有着非常大的进步,但是使用NIO是受限制的。这些问题一部分是过去的设计造成的结果,并且现在也不容易去改正,也有一部分是已经的缺陷。

5.1、跨平台兼容性问题

  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就可以轻松实现相关功能。

5.2、无法扩展ByteBuffer

  通过前面的介绍,可以看出ByteBuffer是作为数据容器来使用。不幸的是,JDK的ByteBuffer没有实现通过包装多个ByteBuffer来初始化。如果你希望尽量减少内存拷贝,那么这种方式是非常有用的。若果你想将ByteBuffer重新实现,那么不要浪费你的时间了,ByteBuffer的构造函数是私有的,所以它不能被扩展。

  Netty提供了自己的ByteBuffer实现,Netty通过一些简单的APIs对ByteBuffer进行构造、使用和操作,以此来解决NIO中的一些限制。

5.3、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<SelectedKeys> 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写出一些有用的代码。