1. 关于IO模型的理解

Java 共支持三种 IO 模型

  • BIO:同步并阻塞 IO,服务器服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销(阻塞等待)。
  • NIO:(全称 java non-blocking IO )同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理 NIO,是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
  • AIO:异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效 的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

2. NIO 三大核心

Selector Channel Buffer

image.png

  • 每个 channel 都会对应一个 Buffer
  • Selector 对应一个线程,一个线程对应多个 channel (连接)
  • 程序切换到哪个 channel 是有事件决定的,Event就是一个重要的概念。
  • Selector 会根据不同的事件,在各个通道上切换
  • Buffer 就是一个内存块,底层是有一个数组
  • 数据的读取写入是通过 Buffer,这个和BIO 不同。BIO中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是双向的,可以读也可以写, 需要 flip 方法切换。channel 也是双向的, 可以返回底层操作系统的情况, 比如 Linux , 底层的操作系统通道就是双向的。

2.1 Buffer 缓冲区

本质上行就是一个可读可写的内存块,可以理解成一个容器对象(底层是数组),该对象提供了一组方法,可以更轻松地使用内存块。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

Buffer 包含四个属性

  • Capacity:容量,容纳的最大数据量,在缓冲区创建时指定且不能改变
  • Limit:表示缓存区当前的终点,不能对缓冲区超过极限的位置进行读写操作,切极限是可以修改的
  • Position:下一个要被读或写的元素的碎银位置
  • Mark:标记

MappedByteBuffer:直接在堆外内存进行修改,由NIO来同步到文件

2.2 通道 Channel

Channel 可以实现异步的读写数据。Channel 在 NIO 中是一个接口,常见的实现由:

  • FileChannel(文件数据的读写)
    • read:通道中读取放入缓存区中
    • write:将缓存区中的数据写入通道
    • transferFrom:把目标通道的数据复制到当前通道
    • transferTo:把数据从当前通道复制给目标通道
  • DataGramChannel(UDP 数据的读写)
  • ServerSocketChannel
  • SocketChannel

2.3 Select 选择器

Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

只有在连接/通道真正有读写事件发生时,オ会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间上下文切换导致的开销。

相关方法:

  • Selector open():得到一个选择器对象
  • int select(long timeout):监控所有注册的通道,当其中有 IO 操作时,将对应的 selectionKey 加入到内部集合中并返回
  • Set(SelectionKey) selectedKeys():从内部集合中得到所有的 SelectionKey。

3. NIO 非阻塞网络编程原理

image.png

  1. 当网络连接之,会通过 ServerSocketChannel 得到 SocketChannel
  2. Selector 监听 select 方法,返回有事件发生时的通道个数
  3. 将 SocketChannel 注册到 Selector 上,一个 Selector 可以注册多个 SocketChannel
  4. 注册后返回一个 SelectionKey,可以反向获取 SocketChannel
  5. 通过得到的 Channel 完成业务处理

4. NIO 零拷贝

常用的零拷贝技术有两种:

  • mmap(内存映射)
  • sendFile

image.png
传统的拷贝是 4 次拷贝,3 次上下文切换

  1. 硬盘 DMA copy(直接内存拷贝,不经过cpu)到 kernel buffer(内核缓冲区)
  2. 内核 buffer用户 buffer
  3. 用户 buffersocket buffer
  4. socket buffer DMA copy 到 协议栈

mmap(内存映射)
将文件映射到内核缓冲区时,用户空间可以共享内核空间的数据。在进行络传输时,就可以减少内核空间到用户空间的拷贝次数。

  • 硬盘 DMA copy(直接内存拷贝,不经过cpu)到 kernel buffer(内核缓冲区)
  • 内核 buffer cpu copysocket buffer
  • socket buffer DMA copy 到 协议栈

**
sendFile

  1. 数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffter,同时,由于和用户态完全无关,就减少了一次上下文切换(这里少了一次拷贝)。
  2. Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结

image.png

  • 硬盘 DMA copy(直接内存拷贝,不经过cpu)到 kernel buffer(内核缓冲区)
  • 内核 buffer DMA copy协议栈

注:这里其实有一次 cpu 拷贝 kernel buffer -> socket buffer 但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略。

Mmap 和 sendfile 的区别

  1. Mmap 适合小数据量读写,sendfile 适合大文件传输。
  2. Mmap 需要 4 次上下文切换,3 次数据拷贝;sendfile 需要 3 次上下文切换,最少 2 次数据拷贝。
  3. Sendfile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)

Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。

5. Netty 的优点

  • 使用简单:封装了 NIO 的很多细节,使用更简单。
  • 功能强大:预置了多种编解码功能,支持多种主流协议。
  • 定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。
  • 性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。
  • 稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身。
  • 社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。

Netty 高性能表现在哪些方面

  • IO 线程模型:同步非阻塞,用最少的资源做更多的事。
  • 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
  • 内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。
  • 串形化处理读写:避免使用锁带来的性能开销。
  • 高性能序列化协议:支持 protobuf 等高性能序列化协议。

6. Reactor 线程模型

Reactor:反应器模式/分发者模式
目前存在的线程模型有:

  • 传统的阻塞 I/O 线程模型
  • Reactor 线程模型
    • 单 Reactor 单线程(redis)
    • 单 Reactor 多线程
    • 主从 Reactor 多线程(netty)

image.png

  1. Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
  2. 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
  3. SubReactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
  4. 当有新事件发生时,subReactor 就会调用对应的 handler 处理
  5. handler 通过 read 读取数据,分发给后面的 worker 线程处理
  6. worker 线程池分配独立的 worker 线程进行业务处理,并返回结果

7. Netty 模型的工作原理

image.png

  1. Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,Worker Group 专门负责网络的读写
  2. Boss Group 和 Worker Group 类型都是 NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
  4. NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 的网络通讯
  5. NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
  6. 每个 BOSS NioEventLoop 循环执行的步骤有 3 步
    1. 轮询 accept 事件
    2. 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 worker NioeventLoop 上的 selector
    3. 处理任务队列的任务,即 runAllTasks
  7. 每个 Worker NioEventLoop 循环执行的步骤
    1. 轮询 read/write 事件
    2. 处理 I/O 事件,即 read, write 事件,在对应 NioScocketChannel 处理
    3. 处理任务队列的任务,即 runAllTasks
  8. 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器.

8. 异步模型

  • 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。

  • Nety中的I/O操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 Channelfuture 调用者并不能立刻获得结果,而是通过 Future- Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果

  • Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调。重点说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun 返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future 去监控方法 fun 的处理过程(即:Future- Listener 机制)

9 Netty 的核心组件

  • Bootstrap, ServerBootstrap
    • 分别是客户端和服务端 启动程序引导类
    • 作用是配置整个 netty 程序,串联各个组件
  • Future、ChannelFuture
    • 注册监听,当操作成功或失败时触发注册监听的事件
  • Channel
    • 网络通信的组件,执行网络 I/O 操作
  • Selector
    • 实现 I/O 多路复用
  • ChannelHandler
    • Channelhandler 是一个接口,处理 IO 事件或拦截 IO 操作,并将其转发到其 ChannelPipeline(业务处理链中的下一个处理程序
  • Pipline 和 ChannelPieline
    • ChannelPieline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于个贯穿 Netty 的链。(也可以这样理解:ChannelPieline 是保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作)
    • ー个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了ー个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler
    • 入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler 出站事件会从链表 taii 在前传递到最前一个出站的 handler,两种类型的 handler 互不干扰
  • ChannelHandlerContext
    • 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象
  • ChannelOption
  • EventLoopGroup、NioEventLoopGroup
    • Event Loopgroup 是一组 Eventloop 的抽象,Nety 为了更好的利用多核 CPU 資源,一般会有多个 Eventloop 同时工作,每个 Event Loop 维护着一个 Selector 实例
  • Unpooled 类
    • Nety 提供一个专门用来操作缓冲区(即 Nety 的数据容器)的工具类

10. Netty 心跳检测机制

IdleStateHandler 是 netty 提供的处理空闲状态的处理器

  • long readerIdleTime:表示多长时间没有读,就会发送一个心跳检测包检测是否连接
  • long writerIdleTime:表示多长时间没有写,就会发送一个心跳检测包检测是否连接
  • long alIldleTime:表示多长时间没有读写,就会发送一个心跳检测包检测是否连接

11. Google Protobuf

Codec(编解码器)的组成部分有两个:decoder(解码器)和 encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成业务数据

Netty 自身提供了一些 cdec(编解码器)

  • StringEncoder,对字符串数据进行编码
  • StringDecoder,对字符串数据进行解码
  • ObjectEncoder,对 Java 对象进行编码
  • ObjectDecoder,对 Java 对象进行解码

这些编解码器,底层使用的仍是 Java 序列化技术,而 Java 序列化技术本身效率就不高,存在如下问题

  • 无法跨语言
  • 序列化后的体积太大,是二进制编码的 5 倍多
  • 序列化性能太低

Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC【远程过程调用 remote procedure call】数据交换格式。

  • 支持跨平台、跨语言,即【客户端和服务器端可以是不同的语言编写的】(支持目前绝大多数语言,例如 C++ C#、Java、python 等)高性能,高可靠性
  • 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用 proto 文件进行描述。说明,在 idea 中编写 proto 文件时,会自动提示是否下载 ptotot 编写插件。可以让语法高亮。
  • 然后通过 protoc.exe 编译器根据。Proto 自动生成 java 文件 protobuf 使用示意图

其他序列化技术

  • XML
    • 优点:人机可读性好,可指定元素或特性的名称。
    • 缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
    • 适用场景:当做配置文件存储数据,实时数据转换。
  • JSON,是一种轻量级的数据交换格式
    • 优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度比较快。
    • 缺点:数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。
    • 适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
  • Fastjson,采用一种“假定有序快速匹配”的算法。
    • 优点:接口简单易用、目前java语言中最快的json库。
    • 缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。
    • 适用场景:协议交互、Web输出、Android客户端
  • Hessian 采用二进制协议的轻量级remoting onhttp工具

12. 拆包和粘包

出现原因

TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket。因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了
优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界**的。

由于 TCP 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。

解决方案

  1. 使用自定义协议+编解码器来解决
  2. 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的 TCP 粘包、拆包。
  1. <br />
  2. <br />
  3. <br />