IO模型
一个输入操作通常包括两个阶段:
- 等待数据准备好
- 从内核向进程复制数据
阻塞式 I/O
应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。
非阻塞IO
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。
I/O 复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
信号驱动 I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
异步 I/O
应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
对比
Reactor多线程模型
Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式,(基于事件驱动)服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程, 因此Reactor模式也叫 Dispatcher模式
Reactor 模式使用IO复用监听事件, 收到事件后,分发给某个线程(进程), 这点就是网络服务器高并发处理关键
单 Reactor 单线程
单Reactor多线程
主从 Reactor 多线程
①Reactor主线程 MainReactor 对象通过select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件
②当 Acceptor 处理连接事件后,MainReactor 将连接分配给SubReactor
③subReactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理
④当有新事件发生时, subreactor 就会调用对应的handler处理
⑤handler 通过read 读取数据,分发给后面的worker 线程处理
⑥worker 线程池分配独立的worker 线程进行业务处理,并返回结果
⑦handler 收到响应的结果后,再通过send 将结果返回给client
⑧Reactor 主线程可以对应多个Reactor 子线程, 即MainRecator 可以关联多个SubReactor
什么是Netty
netty是jboss提供的一个java开源框架,netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可用性的网络服务器和客户端程序。也就是说netty是一个基于nio的编程框架,使用netty可以快速的开发出一个网络应用。
应用场景
理论上来说,NIO 可以做的事情,Netty 都可以做并且更好。Netty 主要用来做网络通信:
- RPC 框架的网络通信工具;
- 实现一个 HTTP 服务器;
- 实现一个即时通讯系统;
- 实现消息推送系统。
Netty核心组件
Core 核心层
Core 核心层是 Netty 最精华的内容,它提供了底层网络通信的通用抽象和实现,包括事件模型、通用API、支持零拷贝的 ByteBuf 等。
Protocol Support 协议支持层
协议支持层基本上覆盖了主流协议的编解码实现,如 HTTP、Protobuf、WebSocket、二进制等主流协议,此外 Netty 还支持自定义应用层协议。Netty 丰富的协议支持降低了用户的开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。
Transport Service 传输服务层
传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。
Netty逻辑架构
Netty抽象出两组线程池,类型都是 NioEventLoopGroup。 BossGroup 专门负责接收客户端的连接创建Channel, WorkerGroup 专门负责处理
NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在该通道上的socket的网络通讯
Boss NioEventLoop
轮询accept 事件
处理accept 事件 , 与client端建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector 上。 EventLoop 负责处理一种类型 Channel,Channel 在生命周期内可以对和多个 EventLoop 进行多次绑定和解绑
Worker NIOEventLoop
轮询read, write 事件
处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
Channel
当服务端和客户端建立一个新的连接时, 一个新的 Channel 将被创建,同时它会被自动地分配到它专属的 ChannelPipeline。ChannelPipeline 内部通过双向链表将不同的 ChannelHandler 链接在一起。当 I/O 读写事件触发时,Pipeline 会依次调用 Handler 列表对 Channel 的数据进行拦截和处理。
Netty启动过程
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
// (非必备)打印日志
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定义客户端消息的业务处理逻辑
p.addLast(new HelloServerHandler());
}
});
// 6.绑定端口,调用 sync 方法阻塞知道绑定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
拆包和粘包
什么是拆包和粘包
TCP 是一个面向流的传输协议,所谓流,就是没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
Netty解决
相比粘包,拆包问题比较简单,用户可以自己定义自己的编码器进行处理,Netty 并没有提供相应的组件。对于粘包的问题,代码比较繁琐,Netty 提供了 4 种解码器来解决,分别如下:
- 固定长度的拆包器(FixedLengthFrameDecoder),每个应用层数据包的都拆分成都是固定长度的大小;
- 行拆包器(LineBasedFrameDecoder),每个应用层数据包都以换行符作为分隔符,进行分割拆分;
- 分隔符拆包器(DelimiterBasedFrameDecoder),每个应用层数据包,都通过自定义的分隔符,进行分割拆分;
- 基于数据包长度的拆包器(LengthFieldBasedFrameDecoder),将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度。
Netty时间轮(海量定时任务)
常规的JDK 的Timer 和 DelayedQueue 等工具类,可实现简单的定时任务,单底层用的是堆数据结构,存取复杂度都是 O(NlogN),无法支撑海量定时任务。Netty经典的时间轮方案,正是通过将任务存取及取消操作时间复杂度降为 O(1),而广泛应用在定时任务量大、性能要求高的场景中。
基于Netty的Websocket底层,服务器端维护一个高效批量管理定时任务的调度模型。时间轮一般会实现成一个环形数组结构,类似一个时钟,分为很多槽,一个槽代表一个时间间隔,每个槽使用双向链表存储定时任务。指针周期性地跳动,跳动到一个槽位,就执行该槽位的定时任务。
单层时间轮的容量和精度都是有限的,对于精度要求特别高、时间跨度特别大或是海量定时任务需要调度的场景,可以考虑使用多级时间轮以及持久化存储与时间轮结合的方案。时间轮的定时任务处理逻辑如下:
将缓存在 timeouts 队列中的定时任务转移到时间轮中对应的槽中
根据当前指针定位对应槽,处理该槽位的双向链表中的定时任务,从链表头部开始迭代:
属于当前时钟周期则取出运行
不属于则将其剩余的时钟周期数减一
检测时间轮的状态。如果时间轮处于运行状态,则循环执行上述步骤,不断执行定时任务。
Kafka中的时间轮
通过多层次的时间轮。多层次时间轮还会有降级的操作,假设一个任务延迟 500 秒执行,那么刚开始加进来肯定是放在第三层的,当时间过了 436 秒后,此时还需要 64 秒就会触发任务的执行,而此时相对而言它就是个延迟 64 秒后的任务,因此它会被降低放在第二层中,第一层还放不下它。
再过个 56 秒,相对而言它就是个延迟 8 秒后执行的任务,因此它会再被降级放在第一层中,等待执行。
降级是为了保证时间精度一致性。Kafka内部用的就是多层次的时间轮算法。
为什么用Netty
速度快
https://www.iamshuaidi.com/17603.html
1)IO 线程模型:同步非阻塞,多路复用,用最少的资源做更多的事;
2)内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输;
3)内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况;
http://m.studyofnet.com/451789556.html
4)串形化处理读写:避免使用锁带来的性能开销。多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。SingleThreadEventExecutor中定义:private volatile Thread thread;通过thread来判断当前线程是否是创建NioEventLoop时绑定的线程,如果是就直接执行读写操作,如果不是就说明是其他线程,把读写操作封装成任务放在任务队列中。
5)高性能序列化协议:支持 protobuf 等高性能序列化协议。
封装好
- 解决原生NIO的问题。API 繁杂、网络异常处理难度大、Epoll Bug(空select没有阻塞,Netty通过空轮询检测替换selector解决)
- 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池.
- 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。