《Netty权威指南》笔记 —— 第一章, 第二章

第1章 Java I/O 的演进之路

I/O多路复用技术: 当需要同时处理多个客户端接入请求时, 可以利用多线程或者I/O多路复用技术进行处理.
I/O 多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上, 从而使得系统在单线程的情况下可以同时处理多个服务端请求. 减少系统开销, 系统不需要创建新的额外进程或者线程.

应用场景

  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
  • 服务器需要同时处理多种网络协议的套接字

epoll 克服了select的缺点

  1. 支持一个进程打开的socket描述符 不受限制(仅首先于操作系统最大文件句柄数)
  2. I/O效率不会随着FD数目的增加而线性下降
  3. 使用mmap加速内核与用户空间的消息传递
  4. epoll的API更加简单

Java.NIO包

  • 异步I/O操作的缓冲区ByteBuffer等
  • 异步I/O操作的管道Pipe
  • 进行各种I/O操作的Channel, 包括ServerSocketChannel和SocketChannel

Java.NIO包的主要问题

  • 没有统一的文件属性(例如读写权限)
  • API能力比较弱, 例如目录的级联创建和递归遍历, 往往需要自己实现
  • 底层存储系统的一些高级API无法使用
  • 所有的文件操作都是同步阻塞调用, 不支持异步文件读写操作

第2章 NIO入门

BIO编程

  • Client/Server
    • 三次握手, socket通信
  • 通信模型: 通常由一个独立的Acceptor线程负责监听客户端的连接, 接受请求之后为每个客户端创建一个新的线程进行链路处理, 处理完后通过输出流返回应答给客户端

弊端

  • 缺乏弹性伸缩能力
  • 访问量增加后, 服务端的线程个数和客户端并发访问数呈1:1的关系, 线程是java虚拟机宝贵的系统资源, 程数膨胀会让系统的性能急剧下降
  • 在高性能服务器应用, 需要面向成千上万个客户端的并发连接, 这种模型显然无法满足高性能, 高并发接入.
    

伪异步I/O编程

改进线程链接1:1模型, 演进出了一种通过线程池或者消息队列实现 1个或者多个线程处理N个客户端的模型, 由于它的底层通信机制依然使用同步阻塞I/O, 所以被称为“伪同步”.

  • 连接线程比 M:N, M大于N
  • 当有新的客户端接入时, 将客户端的Socket封装成一个Task (该任务实现Java.lang.runnable接口)投递到后端的线程池中进行处理, JDK的线程池维护一个消息队列和N个活跃线程, 对消息队列中的人物进行处理. 由于线程池可以设置消息队列的大小和最大线程数, 因此它的资源占用是可控的, 无论多少个客户端并发访问,都不会导致资源耗尽和宕机
  • 采用了线程池实现, 避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题. 但是由于底层仍然是同步阻塞模型, 因此无法从根本上解决问题.

弊端

当对Socket的输入流进行读取操作的时候, 它会一直阻塞下去, 知道发生以下三种事件:

  1. 有数据可读
  2. 可用数据已经读取完毕
  3. 发生空指针或者I/O异常

这意味着当对方操作比较缓慢或者网络传输缓慢时, 读取输入流的一方的通信线程将被长时间阻塞, 如果对方要60s才能够将数据发送完成, 读取一方的I/O线程也将会被同步阻塞60s, 其他只能排队.资源利用率低下.

NIO编程

缓冲区(Buffer) 是与原I/O的一个重要区别, 在面向流I/O中, 可以将数据直接写入或则将数据直接读到Stream对象中. NIO库中 都是用缓冲区来操作. 实际上, 缓冲区市个字节数组 bytebuffer

通道(Channel) 像是自来水管, 网络数据通过Channel读取和写入. 通道与流不同之处在于通道是双向的, 流是单向的, 通道可以读写同时 Channel是全双工, 比流更好映射底层操作系统的API.

Channel继承关系类
多路复用Selector

Selector会不断地轮询(poll) 注册在其上的Channel, 如果某个Channel上面发生读或者些事件, 这个channel就处于就绪状态, 会被Selector轮询出来, 然后通过selectionKey可以获得就绪Channel的集合,进行后续的I/O操作

一个多路复用Selector可以同时轮训多个channel, JDK采用了epoll() 代替传统的select实现, 所以它并没有最大连接句柄1024/2048 限制, 这也就意味着只需要一个线程负责Selector的轮询, 就可以接入成千上万的客户端.

NIO服务端通信序列图

NioServerReactor ThreadIoHandler1. 打开ServerSocketChannel2. 绑定监听地址InetSocketAddress3. 创建Selector, 启动线程4. 将ServerSocketChannel 注册到Selector, 监听5. Selector 轮询就绪的Key7. 设置新建客户端连接的Socket参数8. 向Selector注册监听读操作SelectionKey.OP_READ9.handleRead() 异步读请求消息到ByteBuffer10. decode 请求消息异步写ByteBuffer到SocketChannelNioServerReactor ThreadIoHandler

NIO客户端创建序列图

NioClientServerReactor ThreadIoHandler1. 打开SocketChannel2. 设置SocketChannel为非阻塞模式, 同事设置TCP参数3. 异步连接服务端4. 判断连接结果, 如果连接成功, 调到步骤10, 否则执行步骤55. 向Reactor线程的多路复用器注册 OP_CONNECT 事件6. 创建Selector, 启动线程7. Selector轮询就绪的Key8. handerConnect()9 判断连接是否完成, 完成则执行步骤1010. 向多路复用器注册读事件 OP_READ11. handleRead() 异步读请求消息到ByteBuffer12. decode 请求消息13. 异步写 ByteBuffer 到 SocketChannelNioClientServerReactor ThreadIoHandler

NIO编程的难度确实比同步阻塞BIO大很多. 尽管难度增加, 但是不影响程序员对其使用, 因为以下优点:

优点

  1. 客户端发起的连接操作是异步的, 可以通过在多路复用器注册OP_CONNECT等后续结果, 不需要像之前的客户端那样被同步阻塞.
  2. SockChannel的读写操作都是异步的, 如果没有可读写的数据它不会同步等待, 直接返回, 这样I/O通信线程就可以处理别的链路, 不需要同步等待这个链路可用.
  3. 线程模型的优化 由于JDK的Selector在Linux等主流操作系统上通过epoll实现, 它没有连接句柄数的限制, 意味着一个selector线程可以同时处理成千上万客户端连接, 而且性能不会有所折损

AIO编程

引入了异步通道的概念, 并提供异步文件通道和异步套接字通道的实现. 异步通道提供两种方式获取获取操作结果

  • 通过java.util.concurrent.Future类
  • 在执行异步操作的时候传入一个java.nio.channels

CompletionHandler 接口 的实现类作为操作完成的回调. 异步Socket Channel是被动执行对象, 我们不需要像NIO编程那样创建一个独立的I/O线程来处理读写操作. 对于AsynchronousServerSocketChannelAsynchronousSocketChannel, 它们都由JDK底层的线程池负责回调并驱动读写操作. 正因为如此, 基于NIO2.0新的异步非阻塞Channel会比NIO简单.

模型对比

在这里插入图片描述

为何选择Netty

不选择原生IO

  1. NIO的类库 API繁杂 需要熟练掌握Selector, ServerSocketChannel, SocketChannel, ByteBuffer
  2. 需要其他前置的技能, java多线程.
  3. 可靠性能力补齐, 工作量和难度都非常大
  4. JDK NIO的bug, 比如 epoll bug 会导致Selector空轮询

选择NIO

业界中最流行NIO

  • API简单
  • 功能强大
  • 定制能力强
  • 性能高

版权声明:本文为weixin_41225956原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。