代构建高可用分布式系统的利器——Netty

特性:

  • 高性能,事件驱动,异步非阻塞 Java 开源框架
  • 基于 NIO 的客户端,服务端编程框架
  • 稳定性和伸缩性

常用于建立 TCP/IP 底层的连接,能够建立高性能的 Http 服务器。

正因为高性能、异步非阻塞等特性,很多高性能项目将其作为底层的通信基础,比如阿里的 Dubbo。

活跃的主要领域:

  • 高性能领域
  • 多线程并发领域
  • 异步通信领域

IO通信

关于这块,以前看到有篇写的非常好的文章解释:

关于同步、异步、并行、并发等

下面为简略的介绍。

BIO

一个线程负责连接。

就是一个线程来负责监听,它接受到客户端的请求后,为每一个请求创建一个新的线程来处理,处理完成后通过输出流返回给客户端,典型的一请求一应答请求模型

这种模型缺乏弹性伸缩能力,服务端的处理线程数和客户端的并发访问数是 1:1 的关系,线程数膨胀后导致效率大大降低,甚至线程的堆栈溢出,无法创建新的线程。

伪异步IO通信

使用线程池负责连接。

将用户的请求封装成一个 task 然后扔到线程池中进行处理(其中还会使用队列),对应的是 M 请求 N 应答关系

因为线程池是可控的,在高并发下一般也不会资源枯竭而宕机,但是会发生线程池的阻塞

PS:都需要一个线程来做监听,它会将请求封装、投递到线程池。

NIO通信

关键三个词:缓冲区(Buffer)、通道(Channel)、多路复用器(Selector)。

它会有一个缓冲区 Buffer 的对象,包含一些要写入或者要读出的数据,NIO 库中所有的数据都是用缓冲区处理的,在编程层的流操作中不管是读还是写都是在操作缓冲区。

通道 Channel 和流不同,它是双向的,而流只能是单向的,所以通道可以进行读、写操作,或者两者同时进行。

多路复用器 Selector,简单说,它会不断的轮询在其上的 Channel,如果某个 Channel 上出现读或者写就说明这个 Channel 处于就绪状态,然后通过 Selector 可以获取就绪状态的 Channel 集合,然后进行后续的操作。

它并没有最大连接数的限制,在 IO 领域是个巨大的进步。

AIO通信

连接注册读写事件和回调函数,读写方法是真异步的,会主动通知程序。

AIO 中使用了系统底层的模型(异步套接字通道对应 Unix 中的事件驱动 IO,是真正的异步非阻塞 IO),主动通知进程,所以它不需要通过多路复用器就可以实现异步读写,简化了 NIO 的编程模型

比较

客户端个数(客户端:服务端):

BIO : 1:1

伪异步IO : M:N

NIO : M:1

AIO : M:0

类型:

BIO : 阻塞同步

伪异步IO : 阻塞同步

NIO : 非阻塞同步

AIO : 非阻塞异步

综合使用

使用难度:BIO < 伪异步IO < AIO < NIO

调试难度:BIO < 伪异步IO < NIO 约= AIO

可靠性:BIO < 伪异步IO < NIO 约= AIO

吞吐量:BIO < 伪异步IO < NIO 约= AIO

Netty入门

原生 NIO 的类库和 API 繁杂,需要熟练掌握 buffer、selector、channel;入门门槛高(还需要熟练 Java 多线程、网络编程)

可靠性能力补齐的工作量和难度大,JDK NIO 可能存在一些 bug。

而 Netty,API 简单,入门门槛低,性能高,成熟稳定。

这里是一个使用 Netty + WebSocket 的小程序:Github地址

WebSocket

HTML5 协议规范,通过握手机制,客户端和服务器建立一个类似 TCP 的连接,方便客户端和服务器的通信。

它的出现是为了解决客户端与服务端实时通信问题,通过 http 或者 https 发送一个特殊的请求,握手后建立起一个用于交换数据的 TCP 连接,此后客户端和服务端通过这个连接进行实时通信

避免了之前通过轮询达到“实时”的效果,节省通信开销;因为服务器可以主动传送数据给客户端,他们可以在任意时刻互相推送信息。

关闭的时候,一般是由服务器关闭底层 TCP,客户端也可以发起 TCP Close 。

关于NIO

引用知乎上的一个比较好的回答:https://www.zhihu.com/question/24322387/answer/282001188

NIO 并不是 Java 独有的概念,NIO 代表的一个词汇叫着 IO 多路复用。

它是由操作系统提供的系统调用,早期这个操作系统调用的名字是 select,但是性能低下,后来渐渐演化成了 Linux 下的 epoll 和 Mac 里的 kqueue。

我们一般就说是 epoll,因为没有人拿苹果电脑作为服务器使用对外提供服务。而 Netty 就是基于 Java NIO 技术封装的一套框架。

为什么要封装?因为原生的 Java NIO 使用起来没那么方便,而且还有臭名昭著的 bug,Netty 把它封装之后,提供了一个易于操作的使用模式和接口,用户使用起来也就便捷多了。


那 NIO 究竟是什么东西呢?

NIO 的全称是 NoneBlocking IO,非阻塞 IO;区别与 BIO,BIO 的全称是 Blocking IO,阻塞 IO。

那这个阻塞是什么意思呢?

  1. Accept 是阻塞的,只有新连接来了,Accept 才会返回,主线程才能继
  2. Read 是阻塞的,只有请求消息来了,Read 才能返回,子线程才能继续处理
  3. Write 是阻塞的,只有客户端把消息收了,Write 才能返回,子线程才能继续读取下一个请求

所以传统的多线程服务器是 BlockingIO 模式的,从头到尾所有的线程都是阻塞的。这些线程就干等在哪里,占用了操作系统的调度资源,什么事也不干,是浪费。

那么 NIO 是怎么做到非阻塞的呢?它用的是事件机制

它可以用一个线程把 Accept,读写操作,请求处理的逻辑全干了。如果什么事都没得做,它也不会死循环,它会将线程休眠起来,直到下一个事件来了再继续干活,这样的一个线程称之为 NIO 线程。

while (true) {
  events = takeEvents(fds);  // 获取事件,如果没有事件,线程就休眠
  for (event : events) {
    if (event.isAcceptable()) {
      doAccept(); // 新链接来了
    } else if (event.isReadable()) {
      request = doRead(); // 读消息
      if (request.isComplete()) {
        doProcess();
      }
    } else if (event.isWriteable()) {
      doWrite();  // 写消息
    }
  }
}

上面这一段是这个流程的伪代码,跟真实的代码还是有很多差异的。

关于Netty

Netty 是建立在 NIO 基础之上,Netty 在 NIO 之上又提供了更高层次的抽象。

在 Netty 里面,Accept 连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。

Accept 连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是 NIO 线程。用户可以根据实际情况进行组装,构造出满足系统需求的并发模型。

Netty 提供了内置的常用编解码器,包括 行编解码器[一行一个请求],前缀长度编解码器[前 N 个字节定义请求的字节长度],可重放解码器[记录半包消息的状态],HTTP 编解码器,WebSocket 消息编解码器等等

Netty 提供了一些列生命周期回调接口,当一个完整的请求到达时,当一个连接关闭时,当一个连接建立时,用户都会收到回调事件,然后进行逻辑处理。

Netty 可以同时管理多个端口,可以使用 NIO 客户端模型,这些对于 RPC 服务是很有必要的。

Netty 除了可以处理 TCP Socket 之外,还可以处理 UDP Socket。

在消息读写过程中,需要大量使用 ByteBuffer,Netty对ByteBuffer 在性能和使用的便捷性上都进行了优化和抽象。

总之,Netty 是 Java 程序员进阶的必备神奇。如果你知其然,还想知其所以然,一定要好好研究下 Netty。如果你觉得 Java 枯燥无谓,Netty 则是重新开启你对 Java 兴趣大门的钥匙。

posted @ 2018-06-17 15:41 Kerronex 阅读(...) 评论(...) 编辑 收藏