Netty 学习之路

前言

好几年没写博客了,全是自己本地做笔记。最近项目中遇到在线即时通讯、事件通知等一系列需求,遂开始学习十几年前但现在依然很火的经典开源框架 – Netty,并记录下来分享分享。

I/O 模型

学习 Netty 之前,必须要理解就是这个 I/O 模型了;废话不多说,开整

BIO(Blocking IO)

以流的方式处理数据,基于字节流和字符流
Java BIO 是传统的的 Java IO 编程,类和接口在 java.io 中

  • 服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,但如果这个连接不做任何事,会造成不必要的线程开销(可以使用线程池机制改善,实现多客户连接,但不能减少线程的个数)
  • 适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择
  • 连接过后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
    编程流程
  1. 服务端启动一个 ServerSocket
  2. 客户端启动 Socket 连接对服务器通信,默认情况下服务器端对每个客户建立一个线程与之通讯
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待或者被拒绝
  4. 如果有响应,客户端线程会等待请求结束后,才继续执行

NIO (Non Blocking IO)

以块的方式处理数据,基于 Channel 和 Buffer
JDK1.4 之后输入/输出的新特性,是同步非阻塞的,类在 java.nio 下

  • NIO 是面向缓冲区、或者面向块编程,可以做到用一个线程处理多个操作(一个线程监听多个客户端通道)。而不需要类似100个请求就开辟100个线程
  • 一个线程从某个通道请求或者读取数据,但它仅能得到目前可用的数据;如果没有数据可用,就什么都不会做,而不是保持线程阻塞
  • HTTP2.0 使用了多路复用,做到同一个连接并发处理多个请求,是 HTTP1.0 的好几个数量级
  • NIO 包括三大核心组件
名称描述
Selector选择器一个 Selector(类比注册中心) 对应一个线程,管理着多个 Channel;Selector 切换到哪个 Channel 是由事件决定的,Selector 会根据不同的事件在 Channel 上切换
Channel通道一个 Channel 都对应一个 Buffer;Channel 是双向的,可以反应底层操作系统的情况(例如 Linux 底层的操作系统是双向的)
Buffer缓冲区Buffer 是一个内存块,底层是一个数组;数据的读写都是通过 Buffer,和 BIO(输入流/输出流) 本质上不同(BIO 不是双向)

NIO组件关系

Selector 选择器

能够检测多个注册的通道上是否有事件发生(多个 channel 以事件的方式可以注册到同一个 Selector)

当客户端连接时

  1. 通过 ServerSocketChannel 得到 Socketchannel
  2. 将 SocketChannel 注册到 Selector 上,使用 register(Selector sel, int ops) 方法;一个 Selector 上可以注册多个 SocketChannel
  3. 每一次注册后返回 selectionKey, 被 Selector 作成集合的形式管理起来
  4. Selector 此时可以进行监听, 使用方法 Select 方法,并返回有事件发生的通道个数,进而得到各个 SelectionKey
事件名称事件行为
OP_CONNECT客户端连接事件
OP_ACCEPT接受客户端注册事件
OP_READ客户端读事件
OP_WRITE客户端写事件
  1. 再通过 SelectionKey 可以反向获取 SocketChannel,通过得到的 channel 完成业务读写处理

Channel 连接通道

NIO 中的接口
通道可以同时进行读写数据、实现异步读写数据、从缓冲区读写数据

常用类名作用
FileChannel用于文件数据读写
DatagramChannel用于 UDP 数据读写
ServerSocketChannel用于 TCP 数据读写
SocketChannel用于 TCP 数据读写

Buffer 容器对象

名称类型描述
markint标记
positionint位置,下一个要被读写的元素的索引,每次读写缓冲区数据时都会改变值,为下一次读写做准备
limitint表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的
capacityint容量,即可以容纳的最大数据量,在缓冲区创建时被设定并且不能改变
hb[]数据承载结构

注意事项


  1. put 时的类型,get 时需要按照对应的类型获取
  2. MappedByteBuffer 让文件直接在内存中修改(堆外内存)
  3. Scattering,将数据写入到 Buffer 时,可以采用 Buffer 数组,依次写入(分散)
  4. Gathering,从 Buffer 读取数据时,可以采用 Buffer 数组,依次读(聚合)

零拷贝

注意:不是不拷贝,而是没有 CPU 的拷贝
在 Java 中常用的两种拷贝方式:mmap(内存映射)、sendFile

  • 传统 I/O 的文件拷贝方式(4次拷贝3次切换)

  1. 首先从硬件 (Hard Drive)DMA Copy(direct memory access 直接内存拷贝),即不使用 CPU
  2. 拷贝至内核(kernal buffer)
  3. 再将内核中的数据使用 CPU Copy 至 user buffer,此时数据可以修改
  4. 再使用 CPU Copy 至 Socket buffer
  5. 最后使用 DMA Copy 至 protocol engine(协议引擎/栈)

传统IO文件拷贝方式

  • mmap 优化(3次拷贝3次切换)

适合数据、文件较小
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据;这样在网络传输时,就可以减少内核空间到用户空间的拷贝次数

  1. 首先从硬件 (Hard Drive)DMA Copy(direct memory access 直接内存拷贝),即不使用 CPU
  2. 由于共享了内核空间,因此此时可以在 kernal buffer 中进行数据修改
  3. 再通过 CPU Copy 至 Socket Buffer
  4. 最后使用 DMA Copy 至 protocol engine(协议引擎/栈)
  • sendFile(3次拷贝2次切换)

Linux 2.1 版本提供的 sendFile 函数,适合大文件
数据根本不经过用户态,直接从内核缓冲区进去 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
其优势在于

  • 更少的数据 复制
  • 更少的上下文切换
  • 更少的 CPU 缓存伪共享
  • 无 CPU 校验和计算
  1. 首先从硬件 (Hard Drive)DMA Copy(direct memory access 直接内存拷贝),即不使用 CPU
  2. 此时,减少了user context 的切换
  3. 直接从 kernal buffer 通过 CPU Copy 至 Socket Buffer
  4. 再使用 DMA Copy 至 protocol engine(协议引擎/栈)
  5. 但是,在 Linux 2.4 时做了修改,实现了零拷贝;减少了从 kernal buffer 经过 CPU 至 Socket Buffer 的过程,直接从 kernal buffer 通过 DMA Copy 至 protocol engine,从而减少了一次数据拷贝(kernal buffer 的 length、offset 还是会 copy 至 socket buffer,但是信息量很少,消耗很低,可以忽略)

AIO(Asynchronous I/O)

JDK7 引入了 Asynchronous I/O。涉及到两种模式 Reactor 和 Proactor;而 Java 的 NIO 就是 Reactor
AIO 也被称呼为 NIO2.0,异步非阻塞 IO。引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,特点是先由操作系统完成后才通知服务端开启线程去处理,适用于连接数较多、连接时间较长的应用场景

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值