Netty框架学习(一) - 初次接触Netty框架

Netty简介

Netty框架

Netty是一个NIO(Non-blocking IO或New IO)的客户端服务器框架,它可以快速和简单开发高性能、高可靠性的网络应用,极大地简化和提高网络编程的过程和效率。快速和简单并不会导致网络应用出现性能和维护性问题,Netty吸收了多种协议的实现经验,比如FTP、SMTP、HTTP、各种二进制和文本协议;最终,Netty成功找到一种方式,在保证易于开发的同时还保证了其应用的性能、稳定性和扩展性。

使用Netty的好处

  • 整个Netty都是异步的(Asynchronous),因此客户端不需要为线程被阻塞在等待服务器响应而浪费资源,因此系统的性能会提高
  • Netty中是将Java网络编程API高度抽象化成简单易用的API,以致程序员可以从繁琐的网络处理代码中解耦业务逻辑
  • Netty的功能丰富以及有完整的文档

Netty中所使用的关键技术

异步技术

Netty整个框架的API都是异步的,异步处理能够使得系统更加有效地使用资源,它允许你创建一个任务,当有事件发生时将获取通知并等待事件的完成,不管事件完成与否都会及时返回,系统就可以利用剩下的资源做其他事情,提高了资源的利用率。

1. 回调 Callback

回调是异步处理的其中一种实现技术。因为Java没有C/C++的函数指针或函数对象概念,所以不能够把函数当作对象传到别处进行调用。因此要在Java中实现回调,一般都用接口实现,即回调接口中有回调方法,真正使用的时候便是传递该回调接口的实现类。

下面是一个回调技术演示的简单例子

  • 创建回调接口

    1
    2
    3
    4
    5
    6
    public interface FetcherCallback {
    // 当操作或接收数据时被调用
    void onDataAccess(Data data) throws Exception;
    // 当有异常出现时被调用
    void onError(Throwable cause);
    }
  • 创建一个关于获取数据的业务逻辑接口, 获取数据方法接收回调对象

    1
    2
    3
    public interface Fetcher {
    void fetchData(FetcherCallback callback);
    }
  • 实现获取数据业务逻辑接口, 根据不同的情况, 使用回调对象调用不同的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MyFetcher implements Fetcher {
    final Data data;

    public MyFetcher(Data data) {
    this.data = data;
    }

    @Override
    public void fetchData(FetcherCallback callback) {
    try {
    callback.onDataAccess(data);
    } catch (Exception e) {
    callback.onError(e);
    }
    }
    }
  • 定义Data类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Data {
    private int n;
    private int m;

    public Data(int n, int m) {
    this.n = n;
    this.m = m;
    }

    @Override
    public String toString() {
    return n + " / " + m + " = " + (n / m);
    }
    }
  • 创建业务逻辑类实例进行业务处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    public class Worker {
    public void doWork() {
    Fetcher fetcher = new MyFetcher(new Data(1, 0));
    // 创建并传入回调对象到业务方法中, 并完成回调方法的具体实现
    fetcher.fetchData(new FetcherCallback() {
    @Override
    public void onDataAccess(Data data) throws Exception {
    System.out.println("Data received: " + data);
    }

    @Override
    public void onError(Throwable cause) {
    System.out.println("An error accour: " + cause.getMessage());
    }
    });
    }

    public static void main(String[] args) {
    Worker worker = new Worker();
    worker.doWork();
    }
    }
    ```
    > 从上面的示例我们可以得出使用回调技术的步骤
    1. 创建一个回调接口, 里面有若干个针对业务逻辑不同情况的回调方法
    2. 创建一个类并实现回调接口,该类所创建的对象就称作回调对象或函数对象了
    3. 创建业务类(业务接口不是必须的,还是要根据实际情况而定),通过业务类的构造方法或业务方法接收回调对象,在业务方法中,根据自己设定的不同情况下进而使用回调对象调用对应回调方法


    **2. Future**

    如果你学习过Java,并且使用过Java写过并发程序,或许你就用过java.util.concurrent.Future这一个接口。但是不只局限于Java,其实Future是一种并发设计模式。Future模式是"生产者-消费者"模型的扩展,经典的"生产者-消费者"模型中的消息生产者不关心消费者何时处理完这条消息,也不关心处理结果;但Future模式则可以让消息生产者等待直到消费者对消息处理结束,还能取得处理结果。

    - **Java中Future的具体工作原理**
    Future对象本身可以看作是一个对异步处理结果的引用;由于其异步性质,在创建之初,它所引用的对象可能并不可用(比如在网络传输或等待、一些耗时运算);这时,得到Future对象的程序流程如果并不急于使用Future所引用的对象,那么它可以处理其它任务,当程序流程运行到真正需要Future背后引用的对象时,<font color="red">可能出现两种情况</font>:
    > - 程序流程希望可以使用该Future引用的对象,并以此继续完成后续的流程;若实在不可用,也可以进入其它分支流程
    > - 程序流程一定要使用该Future引用的对象,所以会一直等待,或者会等到超时
    对于第一种情况,是通过调用isDone()判断该对象是否准备就绪,并采取不同的处理;而第二种情况则只需调用get()或get(long timeout, TimeUnit unit)通过同步阻塞方式等待对象就绪。

    以下是代码示例
    ```java
    public class FutureExample {
    public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newCachedThreadPool();

    Runnable task1 = new Runnable() {
    @Override
    public void run() {
    System.out.println("task1 -> run!");
    }
    };

    Callable<Integer> task2 = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
    System.out.println("task2 -> run!");
    return new Integer(100);
    }
    };

    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());

    // 等待任务1的完成
    while (f1.isDone()) {
    System.out.println("task1 completed!");
    break;
    }

    // 等待任务2的完成
    while (f2.isDone()) {
    System.out.println("return value by task2: " + f2.get());
    break;
    }
    }
    }

非阻塞IO技术

Java中,非阻塞IO即NIO(Non-blocking IO或New IO),在jdk1.4时提供的新API,它是基于通道(Channel)和缓冲区(Buffer),支持锁和内存映射文件访问,提供多路非阻塞式的高伸缩性网络IO

Java的Blocking IO和Non-blocking IO的差异

内部概念

Java的阻塞IO使用的是流(Stream)的概念,而Java的非阻塞IO使用的是通道和缓冲区的概念

性能差异

若在一个线程下使用read()或write()等阻塞IO的API,则该线程就会被阻塞,直到那么数据完全被读取或者写入,该线程才能继续往下执行其他操作;而使用非阻塞IO时,线程不需等待IO操作完全地完成,它可以去做别的事情。

最重要的就是这两个区别,其他区别可以查看NIO相关的文章。

Java NIO的问题以及在Netty中是如何解决的

跨平台和兼容性问题

NIO是一个比较底层的API,它依赖操作系统IO的API,因此有可能发现代码在Linux上正常运行,但在Windows上就会出现问题,因此这个问题需要自己亲自验证。
Java7提供了NIO2的API,但是有的程序本身就是基于Java1.6开发的,这就无法使用NIO2了,并且NIO2没有提供DatagramSocket的支持,所以NIO2不支持UDP程序;而Netty提供了统一的API,同一功能下,无论是Java6还是Java7都能正常运行,使得开发人员无需关心API兼容性的问题。

扩展ByteBuffer

Java NIO中的ByteBuffer构造函数是私有的,因此它是不可以被自定义扩展的,如果开发人员希望尽量减少内存拷贝,这是不可能的;但Netty提供了另一种ByteBuffer实现,Netty通过简单的API就能对ByteBuffer进行构造和使用,以此解决NIO的一些限制。

NIO对缓冲区的聚合和分散操作有可能造成内存泄露

很多NIO的通道都实现支持Gather和Scatter操作,但是这可能会导致内存泄露,Java7解决了这一个问题;但Netty统一了API,因此Netty无论在Java6或7环境中使用,都不会有问题。

著名的epoll缺陷

Linux-like OSs的选择器使用的是epoll-IO事件通知工具。这是一个在操作系统以异步方式工作的网络stack.Unfortunately,即使是现在,著名的epoll-bug也可能会导致无效的状态的选择和100%的CPU利用率。要解决epoll-bug的唯一方法是回收旧的选择器,将先前注册的通道实例转移到新创建的选择器上。而Netty解决了这以一问题,开发人员无需再担心。