I/O网络
阻塞与非阻塞:
阻塞:拜访IO的线程是否会阻塞(期待)。
同步和异步:
数据的申请形式。
- 同步会期待资源返回的后果。
- 异步通过回调的形式获取返回的后果
BIO
同步阻塞。传统的socket编程,实现模式为一个连贯一个线程,客户端有连贯申请时服务器就启动一个线程解决,如果这个连贯不做任何事件就会造成不必要的线程开销,能够通过线程池改善(实现多个客户连贯服务器)。
存在的问题:
- 针对每个申请都须要创立一个线程。
- 并发较大时须要创立大量线程解决,占用资源大
- 连贯建设后,如果以后线程临时没有数据可读,则线程阻塞在read,造成线程资源节约
NIO
同步非阻塞。实现模式为一个线程解决多个申请(连贯),客户端发送的申请都会注册到多路复用器上多路复用器轮询到连贯有I/O申请就进行解决。
AIO
异步非阻塞。引入了异步通道的概念,应用Proactor
模式,简化了程序编写,无效的申请才启动线程,特点是现有操作系统实现后再告诉服务端程序启动线程去解决,用于连接数较多且连接时间较长的利用。
Proactor
: 音讯异步告诉的设计模式,Proactor
告诉的不是就绪事件,而是实现事件。
场景剖析
- BIO实用于连接数小且固定的架构,对服务器资源要求高,并发局限于利用。JDK1.4以前。
- NIO实用于连贯数目多且连贯比拟短的架构,比方聊天服务器,弹幕零碎,服务器间通信。应用较多
- AIO实用于连贯数目多且和长连贯的架构,比方相册服务器,充沛调用OS参加并发操作。
NIO编程
介绍
- 外围局部:Channel通道,buffer缓冲区,selector选择器
- 面向缓冲区编程。数据读取到缓冲区,须要时可在缓冲区中前后挪动,减少了处理过程中的灵活性,提供非阻塞式的高伸缩性网络。
- 当一个申请从通道发送申请或者读取数据时:如果有数据就读取,没有数据就去做其余的事件,不会阻塞线程。写操作也是
NIO与BIO比拟
- BIO以流的形式解决数据,NIO以缓冲区的形式解决数据。NIO效率更高。
- BIO是阻塞的,NIO是非阻塞的。
-
BIO是基于字节和字符流操作,NIO基于channel和buffer缓冲区进行操作。
数据总是从通道读取到缓冲区,或者从缓冲区写入到通道,selector用于监听多个通道的事件,因而单线程就能够监听多个客户端通道
流程:
客户端与服务器建设连贯,先获取一个通道,通道注册到selector,selector 轮询查看通道的事件(状态),如果客户端向channel的buffer写入了数据,selector监听到了对应事件(例如写事件),则由server端的线程进行操作。如果没有监听到事件则不会让服务端的线程解决。
即:IO多路复用
缓冲区Buffer
Buffer是内存块。Buffer对象就是用来操作内存块的。
介绍:缓冲区实质上是一个能够读写数据的内存块,能够了解为一个数组,Buffer对象提供了能够读写内存块的API,并且能够跟踪记录缓冲区的状态变动。Channel读写数据必须通过buffer。
常见API
蕴含7个子类(byte,short,int,long,float,double,char)罕用子类 ByteBuffer.
ByteBuffer.alloate(长度)
创立byte类型的指定长度的缓冲区。没数据的
ByteBuffer.wrap(byte[] array)
创立一个有内容的byte类型缓冲区。有数据的。
写模式的时候position相当于是以后在那个地位,而后limit了解为length+1
flip()
切换读模式: 将position设置成0就是从头开始读,而后limit设置成原来position的地位 相当于是记录有多少个数据当position=limit就示意读完了。
clear()
切换写模式:将position 设置成0 就是从头开始笼罩写,而后limit设置成最大容量。
Channel
通道能够读也能够写,流一半是单向的,只能读或写,所以须要别离创立一个输出流和输入流。通道能够异步读写,都是基于缓冲区Buffer来读写
常见的实现类有:FileChannel,ServerSocketChannel,SocketChannel
。罕用的ServerSocket 和Socket就能够实现客户端服务端的通信编写。
应用
server
- 创立
ServerSocketChannel
- 绑定端口
- 配置成非阻塞模式
configureBlocking(false)
- while true外面accept。如果有
accpet
会返回一个channel
- 如果channel不为空阐明有传过来的数据
-
创立
ByteBuffer
用channel读取 read()返回值: 负数 无效字节数 0 没有读到数据 -1 读到开端
- 给客户端回写数据write()
- 开释资源
client
- 关上通道
SocketChannel.open()
- 设置ip端口号
- 写出数据 write()
- 读取server写回的数据 read()
- 开释资源
Selector
检测多个注册到服务端的通道上是否有事件产生,而后对每个事件进行相应的解决。用一个线程,解决多个客户端连贯和申请。
所以次要作用:监听通道事件,依据不同事件做不同解决。这样只有在通道监听到读写事件才会进行读写操作,不必节俭资源。
API
Selector.open
失去一个选择器
Selector.select()
阻塞监听所有注册的通道,当有事件产生,放入到了selectionkey
的汇合中。
Selector.slectedKeys
返回事件汇合。
isAcceptable
连贯持续事件:就是发动连贯 ==》ACCEPTisConnectable
连贯就绪事件:就是连贯胜利==》CONNECTisReadable
读就绪事件==》READisWriteable
写就绪事件==》WRITE
事件用完后删除,避免二次解决。
流程
- serverSocketChannel.open关上一个通道
- selector.open创立一个selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
服务端注册连贯事件- while true判断外面有没有事件
- 如果是isAcceptable,获取到的客户端通道设置成非阻塞而后注册到selector并设置读事件。
- 如果是isReadable,取得客户端通道key.channel 读取数据到缓冲区。
- 回写数据。
- 敞开资源。
Netty
原生NIO存在的bug
- NIO类库和API应用简单。
- 须要把握多线程以及reactor模式
- 开发工作了难度大:例如客户端重连断连,半包读写,失败缓存。等
- JDK-NIO有Epoll Bug 导致selector空轮询CPU100%。
Netty是Jboos提供的异步的基于NIO事件驱动的网络应用程序框架,疾速开发高性能高可靠性的网络IO程序。简化了NIO的开发过程。
劣势:
- 提供阻塞和非阻塞的Socket,可灵便扩大事件,可定制的线程模型
- 具备更高的性能吞吐量,应用零拷贝,节俭资源。
- SSL
- 反对多种协定,预置多种编解码性能,反对开发公有协定。
线程模型
- 传统阻塞IO 详情见BIO
-
Reactor模型
是一种散发的模式(Dispatcher模式)一个或多个输出(也就是申请)同时传递给服务端的模式。服务端程序处理多个申请,同步散发到相应的解决线程。高并发的解决的要害是应用IO复用来监听事件,收到事件后分发给某个线程。
Reactor中 蕴含一个Reactor由selector和dispatcher组成,selector用于监听申请,dispatch用于散发申请,如果申请是连贯申请 会分发给Acceptor 由Acceptor建设连贯,IO的读写申请则分发给handler由handler进行读取-解决-响应
-
单Reactor单线程
长处:模型简略,没有多线程、过程通信、竞争问题。
毛病:
- 性能问题:单线程,Handler在解决连贯的业务时,整个过程无奈解决其余连贯的事件,造成性能瓶颈。
- 可靠性问题:线程意外终止或者死循环,整个零碎通信模块不可用,造成节点故障
-
单Reactor多线程
在单Reactor单线程的根底上有多个handler,此时handler只负责读取和响应数据,并且减少了worker线程池,由worker线程来解决业务。所以多线程实际上是减少了work线程。
长处:充分利用多核CPU的解决能力。
毛病:多线程数据共享和拜访比较复杂,reactor要解决所有的事件的监听和响应,而且是单线程运行,在高并发场景容易性能瓶颈
-
主从Reactor多线程
在单reactor多线程的根底上,reactor降级为主从,主Reactor(主线程)只用于监听连贯申请,在由Acceptor建设连贯后交给Reactor子线程,子线程会将连贯退出到本人的连贯队列进行事件监听,而后再分发给handler,再到worker,在理论开发中 子线程是能够扩大的。所以主从多线程,集体感觉扩大的是reactor子线程。
长处:
- 主从reactor,职责明确,主线程只需承受新连贯,子线程实现后续业务解决
-
主从之间数据交互简略,主线程只须要把新连贯交给子线程,子线程不须要返回数据。
之前的模式单reactor还要解决数据的响应
- 多个子reactor可能应答更高的并发申请。
毛病:复杂度难度较高,相似的有Nginx Netty 这种模式也叫1+M+N线程模式即应用1个(代指绝对较少)连贯建设线程+M个IO线程+N个业务解决线程
-
Netty线程模型
基于Reactor主从做了改良,其实就是在主线程将建设连贯后ServerSocketChannel返回的SocketChannel封装成了NioSocketChannel 注册进子线程的selector。
由主从两组线程池组成BossGroup和WorkerGroup,线程池由NioEventLoop线程组成,所以也就是NioEventLoopGroup。NioEventLoop蕴含了selector和taskqueue
其中Boss次要用轮询监听建设连贯,并且将建设连贯后的连贯注册到worker,而后在执行taskqueue其余tasks
worker也是监听读写事件,解决读写,解决其余task
对于Boss来说
- select:轮询注册ssc的accpet事件
- processSelectedKeys:解决accept事件,与客户端建设连贯生成NioSocketChannel并注册到work的selector
- runAllTask:再去以此循环解决队列中的其余工作
对于worker来说:
- select:轮询读写事件
- processSelectedKeys:解决读写事件
- runAllTask:顺次循环解决其余工作
在processSelectedKeys
中会应用pipeline管道,管道中援用了channel。也就是说通过pipeline能够获取到对于的channel,并且管道中保护了很多的处理器(过滤、拦挡、自定义等)
Server端demo
{
// 1. 创立bossGroup线程组: 解决网络事件--连贯事件
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 2. 创立workerGroup线程组: 解决网络事件--读写事件2*处理器线程数
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 3. 创立服务端启动助手
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 4. 设置bossGroup线程组和workerGroup线程组
serverBootstrap.group(bossGroup, workerGroup)
// 5. 设置服务端通道实现为NIO
.channel(NioServerSocketChannel.class)
// 6. Boss参数设置.初始化服务端可连贯队列
.option(ChannelOption.SO_BACKLOG, 128)
// 6.1 child参数设置。两个服务之间应用心跳来检测对方是否还活着
//https://ihui.ink/post/netty/channel-options/
.childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
// 7. 创立一个通道初始化对象
.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) throws Exception {
// 8. 向pipeline中增加自定义业务解决handler
ch.pipeline().addLast(new NettyServerHandler());
}
});
// 9. 启动服务端并绑定端口,同时将异步改为同步
// ChannelFuture future = serverBootstrap.bind(9999).sync();//同步
ChannelFuture bind = serverBootstrap.bind(9999);//异步
bind.addListener(future -> {
if (future.isSuccess()) {
System.out.println("端口绑定胜利");
} else {
System.out.println("端口绑定失败");
}
});
System.out.println("服务器启动胜利....");
// 10. 敞开通道(并不是真正意义上的敞开,而是监听通道敞开状态)
// 敞开连接池
bind.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
TCP粘包拆包
场景
如果发送两个独立的数据包,但服务端一次性接管到了两个数据包则称为粘包;
如果第二个数据包比拟大,服务端两次读取到了两个数据包第一次读到了实现的第一个包和第二个包的局部内容,第二次读取到第二个包的残余局部则称为拆包。
如果两个数据包都跟大,服务端可能会分屡次能力将两个数据包接管齐全,期间会产生屡次拆包。
起因
因为数据的发送和接管方都须要通过操作系统的缓冲区,缓冲区数据沉积,导致多个申请数据粘在一起,拆包则可了解为发送的数据大于缓冲区,进行拆分解决。
解决方案
-
业内罕用
- 音讯长度固定,累计读取长度为定长的报文。
- 换行符作为音讯结束符
- 非凡分隔符作为音讯完结标记,例如回车
- 音讯头定义长度字段标识音讯总长度
-
Netty中的解决方案
Netty提供的解码器
- 定长拆包器FixedLengthFrameDecoder。拆分定长的数据包。
- 行拆包器LineBasedFrameDecoder,以换行符为分隔符拆分
- 分隔符拆包
- 数据包长度,此办法要求协定中要蕴含数据包的长度
发表回复