目录
- NIO 网络编程
- Buffer(缓冲区)
- Channel(通道)
- Selector(选择器)
- SelectionKey
- 零拷贝
- 原生NIO存在的问题
- 线程模型
- 传统阻塞 I/O 服务模型
- Reactor 模式
- 单 Reactor 单线程
- 单 Reactor 多线程
- 主从 Reactor 多线程
- Netty 实现网络编程
- Netty 的线程模型(架构)
- BossGroup
- WorkerGroup
- BossGroup 与 WorkerGroup
- NioEventLoop
- 异步模型(ChannelFuture)
- Bootstrap、ServerBootstrap
- ChannelOption
- Channel
- Selector
- ChannelHandler 及其实现类
- 出站/入站
- ChannelHandler 实现类中的各种方法
- ChannelPipeline
- 常用方法
- ChannelHandlerContext
- ChannelHandler 、ChannelPipeline、NioEventLoop、Channel 关系
- Unpooled 类
- Netty心跳检测机制
- 编码与解码
- TCP 粘包和拆包
NIO 网络编程
- NIO 网络编程 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
Buffer(缓冲区)
- 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,Channel 读取或写入的数据都必须经由 Buffer
- ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException 异常
- 可以将一个普通Buffer 转成只读Buffer
- NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成
- NIO 还支持通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering
Channel(通道)
- 通道类似于流,但有些区别如下
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
- Channel 常用的方法
Selector(选择器)
- Java 的 NIO,用非阻塞的 IO 方式,可以用一个线程使用 Selector 处理多个的客户端连接
- 每个 Channel 以事件的方式可以注册到同一个Selector,Selector 能够检测多个注册的 Channel 上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求
- 常用方法
//得到一个选择器对象
public static Selector open();
//返回有事件发生的 Channel 个数,如果没有发生事件的 Channel,则阻塞指定ms,然后返回 0
public int select(long timeout);
//返回有事件发生的 Channel 对应的 SelectionKey
public Set<SelectionKey> selectedKeys();
//阻塞,直到有 Channel 发生事件
selector.select();
//唤醒阻塞的 selector
selector.wakeup();
//不阻塞,立马返回有事件发生的 Channel 个数 or 0
selector.selectNow();
SelectionKey
- Selector 不会直接返回有事件发生的 Channel,而是返回有事件发生的 Channel 对应的 SelectionKey
- 具体的事件种类,有如下图所示的四种
零拷贝
- 零拷贝是网络编程的关键,常用的零拷贝有 mmap(内存映射) 和 sendFile
- 零拷贝,是从操作系统的角度来说的,因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)
- mmap,用户空间可以共享内核空间的数据,就不用将文件内容拷贝到用户空间了,适合小数据量读写
- sendFile,内核空间中,修改后的数据直接复制到协议引擎,适合大文件传输
- NIO 使用零拷贝通过如图所示方法
原生NIO存在的问题
线程模型
传统阻塞 I/O 服务模型
Reactor 模式
单 Reactor 单线程
单 Reactor 多线程
- 其实就是将具体的业务逻辑交给线程池的线程执行
主从 Reactor 多线程
- 其实就是将所有工作分成三个部分,接受连接请求并管理的Reactor 主线程 + 负责分发任务的 Reactor 子线程 + 负责执行业务逻辑的 Worker线程池
Netty 实现网络编程
Netty 的线程模型(架构)
- Netty 主要基于主从 Reactor 多线程模型做了一定的改进,其中主从Reactor 多线程模型有多个 Reactor
- Netty 抽象出两组线程池,类型都是 NioEventLoopGroup
- BossGroup 专门负责接收客户端的连接,等同于 MainReactor
- WorkerGroup 专门负责网络的读写,等同于 SubReactor
- NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
- 简单理解就是,NioEventLoopGroup 是一个线程池,里面包含很多 NioEventLoop,也就是一个个线程
- NioEventLoop 表示一个
不断循环
的执行处理任务的线程, 每个 NioEventLoop 都有一个 selector , 用于监听绑定在其上的 socket 的网络通讯(Channel)- 这个不断循环,是指每次都执行相同的逻辑,但不是自己循环,而是事件触发每一次的执行
BossGroup
- 每个BossGroup 中的 NioEventLoop 循环执行的步骤有3步
- 轮询 accept 事件
- 处理 accept 事件 , 与 client 建立连接 , 生成Channel , 并将其注册到 WorkerGroup 的某个 NIOEventLoop 上的 selector
- 处理任务队列 TaskQueue 的任务 , 即 runAllTasks
WorkerGroup
- 每个 WorkerGroup 中的 NIOEventLoop 循环执行的步骤
- selector 轮询 read, write 事件
- 针对每个触发 read, write 事件的 Channel,分别执行相应的 I/O 操作 和 逻辑处理
- 处理任务队列 TaskQueue 的任务 , 即 runAllTasks
BossGroup 与 WorkerGroup
- BossGroup 、WorkerGroup 实际上是 NioEventLoopGroup 类的实例
- NioEventLoopGroup 是 EventLoopGroup 接口的实现类
- 常用方法
//构造方法
public NioEventLoopGroup()
//断开连接,关闭线程
public Future<?> shutdownGracefully()
NioEventLoop
- NioEventLoop 是
- 针对 WorkerGroup 中的 NioEventLoop有如下特点
- 每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道(Channel)
- NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责
- 每个 Channel 都绑定有一个自己的 ChannelPipeline
- 每个 NioEventLoop 都有一个 TaskQueue 任务队列,可以在 handler(ChannelHandler 实现类中的方法) 中添加异步任务 or 定时任务,但是因为 NioEventLoop 内部采用串行化设计,所以任务之间是串行化执行的
- 对于耗时较长的操作,可以自定义线程池执行,不要添加到 NioEventLoop 中,特别是高并发,或追求高吞吐量的场景下
异步模型(ChannelFuture)
- Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture
- 调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果
- Netty 的异步模型是建立在 future 和 callback 的之上的
- 简单来说,使用 Netty 的API,一般都是异步操作,会返回一个 ChannelFuture 实例,通过这个实例,可以添加 Listener ,其实就是一个 callback 方法
- 常用的 Listener 为 ChannelFutureListener
// Netty server 端绑定接受请求的端口,这个绑定是一个异步操作,通过下面步骤可以在完成绑定后输出内容
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System.out.println(newDate() + ": 端口["+ port + "]绑定成功!");
} else{
System.err.println("端口["+ port + "]绑定失败!");
}
});
Bootstrap、ServerBootstrap
- Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件
- Bootstrap 类是客户端程序的启动引导类
- ServerBootstrap 类是服务端程序的启动引导类
- 常见的方法如下
//该方法用于服务器端,用来设置两个 EventLoop,BossGroup 、WorkerGroup
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup)
//该方法用于客户端,用来设置一个 EventLoop
public B group(EventLoopGroup group)
//该方法用来设置一个服务器端的通道 Channel 实现
public B channel(Class<? extends C> channelClass)
//用来给 Channel 添加配置,针对 BossGroup
public <T> B option(ChannelOption<T> option, T value)
//用来给接收到的 Channel 添加配置,针对 WorkerGroup
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value)
//该方法用来设置业务处理类即 自定义的 ChannelHandler 实现类,针对 WorkerGroup ,还有一个 handler(ChannelHandler handler) 针对 BossGroup
public ServerBootstrap childHandler(ChannelHandler childHandler)
//该方法用于服务器端,用来设置占用的端口号
public ChannelFuture bind(int inetPort)
//该方法用于客户端,用来连接服务器
public ChannelFuture connect(String inetHost, int inetPort)
ChannelOption
- Netty 在创建 Channel 实例后,一般都通过 ChannelOption 参数设置 Channel
- ChannelOption 可以视为一个转为配置参数,参数配置类,作为
option(ChannelOption<T> option, T value)
、childOption(ChannelOption<T> childOption, T value)
的入参 - 常用的配置参数
Channel
- 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型如下
NioSocketChannel,异步的客户端 TCP Socket 连接
NioServerSocketChannel,异步的服务器端 TCP Socket 连接
NioDatagramChannel,异步的 UDP 连接
NioSctpChannel,异步的客户端 Sctp 连接
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO以及文件 IO
Selector
ChannelHandler 及其实现类
- ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序
- 对所有请求(Channel)的逻辑处理都是通过 ChannelHandler 的实现类完成的,ChannelHandler 的实现类是区分出站/入站的
出站/入站
- 具体看请求的方向,如果方向是 Channel—>ChannelPipeline ,那就是入站
- 如果是 ChannelPipelinel—>Channe,那就是出站
ChannelHandler 实现类中的各种方法
- ChannelHandler 实现类中的各种方法都是不同的事件触发并执行的
channelActive
方法,通道就绪事件就会触发channelRead
方法,通道读取数据事件就会触发channelInactive
方法,通道断开事件就会触发exceptionCaught
方法,异常事件触发,可以用于发生异常后程序的善后处理,尽量不要影响到其它 ChanneluserEventTriggered
方法,当前的 ChannelHandler 的该方法 由 前面的 Netty 心跳检测机制 ChannelHandler 的触发- 还有很多,结合业务场景自定义重写
ChannelPipeline
- ChannelPipeline 是一个 ChannelHandler 实现类的集合,它负责处理和拦截 inbound 或者outbound 的事件和操作,相当于一个贯穿 Netty 的链
- ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互
常用方法
ChannelHandlerContext
- 一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler
- 入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 ChannelHandler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 ChannelHandler(inbound/outbound) 互不干扰
- 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象
- ChannelHandlerContext 中包含一个具体的事件处理器 ChannelHandler ,同时 ChannelHandlerContext 中也绑定了对应的 ChannelPipeline 和 Channel 的信息,方便对 ChannelHandler进行调用
- 常用方法
//关闭通道 Channel
ChannelFuture close()
//刷新
ChannelOutboundInvoker flush()
//将数据写到 ChannelPipeline 中当前 ChannelHandler 的下一个 ChannelHandler 然后开始处理(出站)
ChannelFuture writeAndFlush(Object msg)
ChannelHandler 、ChannelPipeline、NioEventLoop、Channel 关系
- ChannelHandler 充当了处理入站和出站数据的应用程序逻辑的容器
- 实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),可以接收入站事件和数据,这些数据会被业务逻辑处理
- 同理,实现ChannelOutboundHandler接口(或ChannelOutboundHandlerAdapter),可以接收出站事件和数据,这些数据会被业务逻辑处理
- 出站/入站要关注 ChannelHandler 在 ChannelPipeline 中的顺序,一般来讲, ChannelHandler 在 ChannelPipeline 中的顺序即为代码中往 ChannelPipeline 中添加 ChannelHandler 的顺序
- 第一个添加的就是 ChannelPipeline 双向链表的头节点(head),最后一个添加的就是 ChannelPipeline 双向链表的尾节点(tail)
- 出/入站的 ChannelHandler 都在同一个 ChannelPipeline 的双向链表中,编解码器的 ChannelHandler 一般作为 head,和第二节点
- 因为入站的 ChannelHandler 执行顺序是 heap —>tail,出站的 ChannelHandler 执行顺序是 tail —> heap
- 出/入站 ChannelHandler 之间的顺序并不互相影响,它们只是在同一个 ChannelPipeline 双向链表中连续相连,但是执行还是各按各的,即双向链表中的顺序同时包含了 出站/入站的 ChannelHandler 执行顺序,看出站顺序的时候只需要忽略掉链表中的入站 ChannelHandler ,反之看入站顺序亦然
- 因为每个 NioEventLoop 都有自己的 select ,是一对一的关系,Channel 只能注册到多个 select 中的一个,所以一个 Channel 只能对应一个 NioEventLoop
- 每个 NioEventLoop 都有一个 ChannelPipeline,所以 一个 Channel 对应一个 ChannelPipeline
- 总结:每一个 Channel 都会有属于自己的 NioEventLoop 和 ChannelPipeline
- NioEventLoop 的数量是有限的,Channel 的数量远大于 NioEventLoop 数量,所以多个 NioEventLoop 是按照固定的次序,被用来处理不断新加进来的 Channel ,NioEventLoop 每次处理新加进来的 Channel 都会为其初始化一遍,然后创建新的 ChannelPipeline
Unpooled 类
- Netty 提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类
- 常用方法
//通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 但有区别)
public static ByteBuf copiedBuffer(CharSequence string, Charset charset)
- ByteBuf 不需要 flip,可以同时写入数据和读取数据,主要有三个成员属性
- readerindex ,指明当前 ByteBuf 已经读取到的位置, 0—readerindex 已经读取的区域
- writerIndex,指明当前 ByteBuf 已经写入的位置,readerindex—writerIndex 为可读的区域
- capacity,整个 ByteBuf 的容量,单位是字节
Netty心跳检测机制
编码与解码
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
- codec(编解码器) 的组成部分有两个:decoder(解码器)和 encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成业务数据
- Netty提供一系列实用的编解码器,他们都实现了 ChannelInboundHadnler 或者 ChannelOutboundHandler 接口
- 在这些类中,channelRead 方法已经被重写了
- 以入站为例,对于从 Channel 读取的数据,channelRead 方法会被调用。随后,它将调用由解码器所提供的 decode 方法进行解码,并将已经解码的数据转发给 ChannelPipeline 中的下一个 ChannelInboundHandler
- 常用解码器 ByteToMessageDecoder
- 其它解码器
- 常用编码器 MessageToByteEncoder
- MessageToByteEncoder、ByteToMessageDecoder 之所以能成为常用的编/解码器,是因为它支持泛型,可以很简单直接重写
encode/decode
方法实现自定义的编/解码器,这对解决 粘包/拆包 问题非常重要
TCP 粘包和拆包
- TCP是面向连接的,面向流的,提供高可靠性服务
- 收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包
- 这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 解决粘包和拆包,关键就是要解决服务器端每次应该读取的数据长度的问题, 这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP 粘包、拆包
- 解决方式:使用自定义协议 + 编解码器
- 由上面引申出在确定协议和编解码器后就不会粘包、拆包,出现粘包、拆包就是因为没有使用已存在的协议和对应的编解码器,然后又没有自定义,导致使用 Netty 接收请求时,不知道一次正确的请求数据,应该读取多少字节