1.Java IO的读写原理
IO是Input和Output的缩写,即输入和输出。用户程序进行IO的读写基本上会用到read
和write
两大系统调用。
read把数据从内核缓冲区复制到进程缓冲区,write是把数据从进程缓冲区复制到内核缓冲区。 这两大系统的调用
都不负责数据在内核换中去和磁盘之间的交换,底层读写交换是由操作系统kernel内核完成的
。
1.1.内核缓冲区和进程缓冲区
-
目的:
减少频繁的系统IO调用。
因为系统调用需要保存之前的进程数据和状态等信息,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。在linux系统中,系统内核中有个缓冲区叫做内核缓冲区;每个进程有自己独立的缓冲区叫做进程缓冲区。
-
实际: 有了缓冲区,操作系统使用read函数和write函数就是对数据在内核缓冲区和进程缓冲区之间的操作。等待缓冲区达到一定的数量时,再进行IO的调用以提升性能。读取和存储的时机由内核来决定,所以用户程序的IO读写大多数情况下并没有进行实际的IO操作,而是在读写自己的进程缓冲区。
1.2.四种主要的IO模型
1.2.1.同步阻塞IO(BIO)
同步阻塞IO即Blocking IO,在linux的java进程中,默认所有的socket都是BIO。在阻塞式IO模型中,应用程序在从IO系统盗用开始一直到系统调用返回,这段时间是阻塞的。返回成功后应用进程开始处理用户空间的缓存数据。
- 优点: 程序 简单,在阻塞等待数据期间,用户相乘挂起,用户线程基本上不会占用CPU资源。
- 缺点: 一般会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流读写。在并发量较大时,需要大量的线程来维护大量的网络连接、内存,线程切换开销非常巨大。因此BIO在高并发场景下是不可用的。
1.2.2.同步非阻塞IO(NIO非java)
即None Blocking-IO,在linux系统下,可以通过设置socket使其变为non-blocking,应用程序的线程则不断的进行IO系统的调用,轮询数据是否已经准备好,直到数据准备好为止。
- 优点: 每次发起IO,在内核的等待时间中可以立即返回。用户线程不会阻塞,实用性较好。
- 缺点: 需不断的重复发起IO,这种轮询会占用大量的CPU时间,系统资源利用率较低。
Java NIO(New IO)不是IO模型中的NIO,而是IO多路复用模型
1.2.3.IO多路复用模型
即IO multiplexing,核心是为了避免NIO中的轮询等待问题。通过一种新的系统调动,一个进程可以监视多个文件描述符
,一旦某个描述符就绪(一般为内核缓冲区可读/写),内核kernel能够通知程序进行相应的IO系统调用。
- 支持IO多复路的系统调用:
- select:是目前几乎所有操作系统上都支持,具有良好跨平台特性。
- epoll:是linux2.6内核中提出的,是select系统在linux中的增强版。
- 基本原理:
单个线程不断轮询select/epoll
系统来调用所负责的成百上千的socket连接,当某个或某些socket网络连接有数据到达了就返回这些可以读写的连接。 - 优势:
通过一次select/epoll
系统的调用就可查询到可以读写的一个甚至是成百上千的网络连接。
1.2.4.异步IO模型(AIO)
即asynchronous IO。
- 基本流程:
用户线程通过系统调用告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作完成后通知用户程序,用户执行后续的业务操作。 - 特点:
在内核kernel的等待数据和复制的两个阶段,用户线程都不是block(堵塞)的
。用户线程需要接受kernel的IO操作完成的事件或者说注册IO操作完成的回调函数到操作系统的内核。异步IO有时也叫信号驱动IO。 - 缺点:
需要完成时间的注册与传递,这里面需要底层操作系统提供大量的支持。windows系统下通过IOCP实现了真正的异步IO。linux系统在2.6版才引入,目前不完善,所以在linux下高并发网络编程都以IO复用模型为主。
2.Netty概述
Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务端和客户端。对JDK自带的NIO的API进行了封装解决了上述问题。且具有高性能、高吞吐量、低延迟、低资源消耗、最小化不必要的内存复制等优点。
2.1.Reactor线程模式
根据Reactor的数量和处理资源的线程数不通分为:Reactor单线程、Reactor多线程、主从Reactor多线程。Netty线程模式是基于主从Reactor多线程做了改进。
基于传统堵塞IO模型做了一下两种改进:
- 基于IO复用模型
- 基于线程池复用线程资源
Reactor模式的核心组成:
- Reactor:reactor就是多个客户端共用一个阻塞对象,它单独起一个线程运行,负责监听和分发事件,将请求分发给适当的处理程序进行处理。
- Handler:处理程序要完成的实际事件,也就是真正执行业务逻辑的程序,它是非阻塞的。
2.1.1.单线程Reactor
多个客户端请求连接,然后Reactor
通过selector
轮询判断那些通道是有事件发生的,如果是连接事件就到了Acceptor
中建立连接;如果是其他读写事件就由dispatch
分发到对应的handler
中进行处理。
缺点: Reactor和Handler在一个线程中,如果Handler阻塞了,name程序就阻塞了。
2.1.2.多线程Reactor
处理流程:
Reacto
r对象通过Selector
监听客户端请求事件,通过dispatch
进行分发。- 如果是连接事件,则由
Acceptor
通过accept
方法处理连接请求,然后创建一个Handler
对象响应事件。 - 如果不是连接事件,则由
Reactor
对象调用对应Handler
对象进行处理;Handler
只响应事件不做具体的业务处理,它通过read
方法读取数据后,会分发给线程池的某个线程进行业务处理,并将处理结果返回未Handler
。 Handler
收到响应后,通过send
方法将结果返回给Handler
。
优点:
相比单线程Reactor,这里将业务处理的事情交给了不同的线程去做,发挥了多核CPU的性能
。
缺点:
Reactor只有一个,所有事件的监听和响应都由一个Reactor去完成
,并发性并不好。
2.1.3.主从Reactor多线程
与单Reactor多线程的区别就是:专门搞了一个MainReactor
来处理连接事件,如果不是连接事件,就分发给SubReactor
进行处理。SubReactor可有多个。
优点:
父线程与子线程的交互简单、职责明确,父线程负责接受连接,子线程负责完成后续的业务处理。
缺点: 编程复杂度高
2.2.Netty线程模型
Netty模型时局域主从Reactor多线程模型设计的。
- Netty有两组线程池,一个BossGroup,它专门负责客户端连接;另一个WorkGroup专门负责网络读写。
- NIOEventLoopGroup相当于一个事件循环组,这个组包含了多个事件循环,每个循环都是
NIOEventLoop
。 - NIOEventLoop便是一个不断循环执行处理任务的线程,每个
NIOEventLoop
都有一个Selector
用于监听绑定在其上的SocketChannel
的网络通讯。 BoosGroup
下的每个NIOEventLoop
的执行步骤有三步:- 轮询
accept
连接事件 - 处理
accept
事件,与client
建立连接,生成一个NIOSocketChannel
,并将其注册到某个WorkGroup下的NIOEventLoop
的Selecto
r上 - 处理任务队列的任务,即
runAllTask
;
- 轮询
- 每个WorkGroup下的
NIOEventLoop
循环执行以下步骤:
- 轮询
read、write
事件- 处理
read、write
事件,在对应的NIOSocketChannel
处理- 处理任务,及
RunAllTasks
- 每个WorkGroup下的
NIOEventLoop
在处理NIOSocketChannel
业务时,会使用pipeline
(管道),管道中维护了很多handler处理器用来处理channel
中的数据
2.1.1.架构图
2.2.2.架构核心解析
-
Bootstrap、ServerBootstrap:
一个Netty应用通常由一个
Bootstrap
开始,主要作用是配置整个Netty程序,串联各个组件。
Netty中Bootstrap
类是客户端程序的启动引导类,ServerBootstrap
是服务端的启动引导类。
Bootstrap创建启动器的步骤:- 设置
EventLoopGroup
线程组group
(bossGroup和workerGroup) - 设置
channel
通道类型 - 设置
option
参数 - 设置
handler
流水线 - 进行端口绑定
- 启动
- 等待通道关闭
- 优雅关闭
EventLoopGroup
- 设置
-
NIOEventLoopGroup和NIOEventLoop:
NIOEventLoopGroup主要管理
eventLoop
的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NIOEventLoop)负责处理每个Channel
上的事件,而一个Channel
只对应一个线程。每个EventLoopGroup
里包括一个或多个EventLoop
,每个EventLoop
中维护了一个Selector
实例。NIOEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NIOEventLoop中的
run
方法,执行IO任务和非IO任务。- IO任务:
selectionKey
中ready
的事件,如accept、connect、read、write
等,由processSelectedKeys
方法触发。 - 非IO任务: 添加到
taskQueue
中的任务,如register()、bind()
等任务,由runAllTasks
方法触发
理解NIOEventLoopGroup和NIOEventLoop:
NIOEventLoop
实际上就是工作线程。NIOEventLoopGroup
是一个线程池,线程池中的线程就是NIOEventLoop。实际上boosGroup
中有多个NIOEventLoop
线程,每个NIOEventLoop
绑定一个端口,也就是说如果程序只需要监听一个端口的话,bossGroup里面只需要有一个NIOEventLoop线程就行了。- 每个NIOEventLoop都绑定了一个
selector
,所以在Netty的线程模型中是由多个selector在监听IO就绪事件,而channel
注册到selector
。 - 一个
channel
绑定一个NIOEventLoop
,相当于一个连接绑定一个线程,这个连接所有的channelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是channelPipeline
链表必须严格按照顺序执行的,单线程的设计能够保证channelHandler
的顺序执行。 - 一个
NIOEventLoop
的selector
可以被多个channel
注册,也就是说多个channel共享一个EventLoop。EventLoop的selector对这些channel进行检查。
- IO任务:
-
Group:
服务端要使用两个线程组,
bossGroup
用于监听客户端连接专门负责与客户端创建连接,并把连接池注册到workerGroup的selector中。workerGroup
用于处理每个连接发生的读写事件。一般创建线程组如下:// 线程组默认的线程数是CPU核数的两倍 EventLoopGroup bossGroup = new NioEventGroup(); // 参数可自定义线程数 EventLoopGroup workerGroup = new NioEventGroup();
-
Channel:
网络通信的组件,能够用于执行网络IO操作。
channel为用户提供:- 当前网络连接的通道状态
- 网络连接的配置参数
- 提供异步的网络IO操作(如:建立连接、读写、绑定端口),异步调用意味着任何IO调用都将立即返回,并且不保证正在调用结束时所请求的IO操作完成。
- 调用立即返回一个
channelFuture
实例,通过注册监听器到channelFuture
上,可以IO操作成功,失败或取消时回调通知调用方 - 支持关联IO操作与对应的处理程序
常用Channel类型:
NIOSocketChannel
:异步客户端TCP Socke连接;NIOServerSocketChannel
:异步服务端TCP Socke连接;NIODatagramChannel
:异步的UDP Socke连接;NIOSctpChannel
:异步Sctp连接;NIOSctpServerChannel
:异步Sctp服务端连接;
获取Channel的状态:
boolean isOpen(); // 如果通道打开则返回true boolean isRegistered(); // 如果通道注册到EventLoop则返回true boolean isActive(); // 如果通道处于活动状态并且已连接则返回true boolean isWriteAble(); // 当且仅当IO线程将立即执行请求的写入操作时返回true
-
Selector:
Netty基于
Selector
对象实现IO多路复用
,通过Selector一个线程可以监听多个连接的Channel
事件。当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断地查询(select)这些注册的Channel是否有已就绪的IO事件(如可读、可写、网络连接完成等),这样程序就可以很简单地使用一个线程高效的管理多个Channel。 -
Pipeline(ChannelPipeline):
处理器容器
,初始化Channel时,把ChannelHandler按顺序装在pipeline
中,就可按顺序执行ChannelHandler
。ChannelPipeline的list
用于处理或拦截Channel
的入站事件和出站事件。它实现了一种高级形式的拦截过滤器模式,使用户完全控制事件的处理方法以及Channel
中各个ChannelHandler
如何交互。在Netty中每个
Channel
都仅有一个ChannelPipeline
与之对应,一个Channel
包含了一个ChannelPipeline
,而channelPipline
中又维护了一个由channelHandlerContext
组成的双向链表,并且每个channelHandlerContext
中又关联着一个channelhandler
。read
事件(入站事件)和write
事件(出站事件)在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail
往前传递到最后一个出站的handler,两种类型的handler互不干扰。 -
ChannelHandler:
一个接口,处理IO事件或拦截IO操作,并将其转发到其产
ChannelPipeline
中的下一个处理程序。它本身并没有提供很多方法,因为这个接口又许多的方法需要实现,方便使用期间可以继承它的子类:channelInboundHandler
用于处理入站IO事件channelOutboundHandler
用于处理出站IO事件
或使用以下适配器类:
channelInboundHandlerAdapter用于处理入站IO事件:
- 注册事件firechannelRegistered
- 连接建立事件firechannelActive
- 读时间和读完成事件firechannelRead、firechannelReadComplete
- 异常通知事件fireExceptionLaught
- 用户自定义时间fireUserEventTriggered
- channel可写入状态变化事件fireChannelWriteBilityChannel
- 连接关闭事件firechannelInactive
channelOutboundHandlerAdapter用于处理出站IO事件:
- 端口绑定bind
- 连接服务器
- 写事件write
- 刷新时间flush
- 读事件read
- 主动断开连接disconnect
- 关闭channel事件close
channelHandlerContext可以在Handler中拿到channel、pipelined等对象