从BIO到NIO在到Netty线程模型(零拷贝)

从BIO到NIO在到Netty线程模型(零拷贝)
摘要:NIO和netty是面试中被频繁问到的,NIO在网络编程中通过少数几个线程处理大量连接数的核心,是tomcat,netty等框架的底层网络传输基础,本文基于面试题,深入理解非阻塞IO,netty是一个高性能的服务器网络通信框架,是rpc,dubbo的底层实现,本文基于netty核心组件详解netty

1、NIO是什么?NIO特点 接口是异步的,非阻塞的。(1.4引入)
定义:nio是面向缓冲区的、基于通道的io操作,nio将以更加高效的方式进行文件的读写操作和网络通信。
1.1 NIO的特点:

核心组件    作用
1、缓冲区buffer    负责存储 可以保存多个相同类型的数据
2、通道channel    负责传输 表示io源于目标打开的连接 channel不能直接访问数据,只能与buffer进行交互
3、选择器selector    单线程 利用selector可以使一个单独的线程管理多个Channel通道,选择器selector是非阻塞的核心。
1.2传统的socket网络编程的缺点?(jdk1.4之前)

1、传统io流都是阻塞的:当线程调用read()或write()时,该线程被阻塞,直到有数据被读取或写入,线程在此期间不能执行其他任务;
2、serverSocket上的accept()方法将会一直阻塞到一个连接建立,并返回一个新的socket用于通信;
3、readline()方法将会阻塞,直到客户端的数据被读取完毕。造成了大量线程处于休眠状态,只能等待输入/输出数据就绪;
4、每个线程的调用栈都分配内存,默认64k~1M,jvm虽然支持大量线程,但上下文切换带来巨大的开销;
5、伪异步IO开启线程的时机不对,客户端刚连接,就会开启一个新的线程

1.3 nio的非阻塞式网络通信

nio网络通信(面试:如何把BIO的同步阻塞改为非阻塞?)
阻塞和非阻塞的区别? 单线程环境下,能不能同时进行读写操作
1、java nio是非阻塞的,当线程从某通道进行读写操作时,若没有数据可用时,该线程可以进行其他任务。所以单独的线程可以管理多个输入和输出通道,nio可以让服务器端使用一个或有限个线程同时处理连接到服务器端的所有客户端(IO多路复用)

nio多路复用的原理

通过selector,Channel,buffer实现非阻塞io操作,用到了反应器设计模式(react)。使用NIO的服务端内部有一个多路轮询器selector,用于监听端口,当客户端尝试连接时,selector就会收到通知,但并不会直接创建一个线程来建立连接,而是交给内核去处理,当客户端把数据都传送过来,进入nio的缓存中时,服务端才会创建一个线程,通过一个Channel去接受数据。NIO接受数据的过程还是阻塞的,本质上不是异步的,是一种多路复用io,适合于连接较多,且连接较短的架构,如聊天服务器。(底层是linux系统的epoll方法)

NIO三大核心详解

1) selector:用于监督多个channel中的读、写、连接请求 //在linux系统中的实现使用epoll模型,所以才能处理那么多的连接请求

1、创建selector:通过调用selector.open()方法创建一个selector
2、向选择器注册通道:channel.register(selector sel,int ops) //读,写,连接,接受

2) Channel:表示打开到IO设备(文件,套接字)的连接    //Channel负责传输,只能和buffer进行交互
channel的实现类:

实现类    作用
FileChannel    用于读取、写入、映射和操作文件的通道
datagramChannel    通过UDP读写网络中的数据结构
socketChannel    通过TCP读写网络中的数据
serverSocketChannel    可以监听新进来的TCP连接,对新来的连接建立socketChannel
1、获取通道的方式:对支持通道的对象调用getChannel()方法
2、想buffer中数据写入channel //inChannel.write(buf);
3、从Channel读取数据到Buffer //inChannel.read(buf);
4、transferFrom():将数据从源通道传输到其他Channel中:

3) buffer:用于缓存channel的数据(数据是从通道读入缓冲区,从缓冲区写入通道)
//Buffer就像一个数组,可以保存多个相同类型的数据
buffer的常用子类: byteBuffer /charBuffer /ShortBuffer/ intBuffer/ LongBuffer /floatBuffer / DoubleBuffer
buffer的基本属性:(通过allocate方法创建buffer对象)

1、容量capacity:表示buffer最大数据容量
2、限制limit:第一个不应该读取或写入的数据的索引,即limit后的数据不可读写
3、位置position:下一个要读取或写入的数据的索引
4、标记mark与重置reset:标记是一个索引,通过buffer中的mark()方法指定buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。                    
   标记、位置、限制、容量遵守以下不变式  0<=mark<=position<=limit<=capacity
5、通过put()写入数据到缓冲区,通过flip()切换读取数据模式
6、clear方法:清空缓存区并返回对缓冲区的引用
7、get():获取Buffer中的数据    获取单个字节
1
2
3
4
5
6
7
8
直接缓冲区和非直接缓冲区
直接缓冲区:Java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作

1、通过调用此类的allocateDirect()工厂方法来创建。
2、还可以通过FileChannel的map()方法将文件区域直接映射到内存中来创建,该方法返回 MappedByteBuffer

只有当数据准备好所有资源,请求IO时,才会开启一个新的线程

管道的概念(pipe) 用于单向线程通信
java NIO管道是两个线程间单向数据连接。pipe有一个source通道和一个sink通道,数据会被写到sink通道,才能source通道读取。
调用sink通道的write方法向管道写数据,调用source通道的read方法读取数据

2、NIO文件传输?实现了非阻塞的系统调用
与3个核心有关 selector选择器(监听Channel中的读、写、连接请求),在能进入I/O操作时,才会开启一个新的线程,在高负载下可靠和高效地处理和调度IO操作非常繁琐,容易出错,留给高性能的网络编程专家-netty

2.1、传统IO文件传输的弊端?(BIO)

1、在传输文件时,先将文件内容从磁盘中拷贝到操作系统read buffer
2、再从操作系统buffer拷贝到应用buffer
3、从应用buffer拷贝到socket buffer
4、从socket buffer拷贝到网卡缓冲区NIC buffer

2.2、NIO非阻塞Linux sendfile()实现;
操作系统内核中,数据由read buffer->socket buffer

1、非阻塞NIO轮询各个端口,通过内核缓存数据,并发量增加
2、向内核注册监听,app可以做其他事
3、NIO的优势在于服务端的并发量、吞吐量;不在于数据传送的速度,传输速度并不快

具体细节:
1、创建数据通道Channel和监听器(选择器)Selector,Selector轮询就绪通道
   创建数据通道ServerSocketChannel,为其配置非阻塞模式,绑定监听,配置TCP参数
   创建轮询多路复用器Selector
   将创建的ServerSocketChannel注册到啊Selector上,并设置监听,在循环体中执行Selecor.select()方法,轮询就绪的通道
2、数据读取过程:建立连接、注册read监听、读取数据
   客户端进行TCP连接,内核收到事件通知
   Selector注册 建立连接监听,内核连接成功后通知Selector
   Selector注册read监听,内核在收到数据时通知Selector;数据存在内核buffer;
   内核通知可以read,app通过channel读取数据        
3、flip()过程描述   //固定
   相关变量:position,capacity,mark//记录position前面的一个位置
1
2
3
4
5
6
7
8
9
10
11
12
3、NIO模型,select/epoll的区别(操作系统层次)/原生的NIO在jdk1.7版本存在epoll bug
3.1、多路复用I/O模型利用select、poll、epoll可以同时监视多个流的I/O事件,在空闲时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就会从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll只轮询哪些真正发出了事件的流),并且只依次顺序处理就绪的流,这种做法就避免了大量的无用操作。

3.2、select/epoll的区别

1、Select
1.Socket数量限制 : 该模式可操作的Socket数由FD_SETSIZE决定,内核默认32*32=1024.
2.操作限制:通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍
2、Epoll
1.Socket数量几乎无限制:该模式下的Socket对应的fd列表由一个数组来保存,大小不限(默认4k).
2.操作无限制:基于内核提供的反射模式,有活跃Socket时,内核访问该Socket的callback,不需要遍历轮询

3.3、nio2
增强了对文件处理和文件系统特性的支持,java.nio.file.path接口代表了一个平台无关的平台路径,描述了目录结构中文件的位置。
原理:异步套接字通道,对应于操作系统中的事件驱动IO,不需要通过通过多路复用器selector对注册的通道进行轮询操作

3.4、NIO线程模型的优化?
由于JDK的selector在linux等操作系统上时通过epoll实现,它没有连接句柄数的限制,所以selector线程可以同时处理成千上万个客户端连接,且性能不会下降

3.5、bio/伪异步IO/NIO/AIO(NIO2)区别?

性能    BIO    伪异步IO    NIO    AIO(NIO2)
客户端个数:IO线程    1:1    M:N(M可以大于N)    M:1(一个IO线程处理多个连接)    M:0(不需要额外线程,被动回调)
IO类型    阻塞io    阻塞IO    非阻塞IO    非阻塞IO
IO类型    同步    同步    同步(IO多路复用)    异步
API难度/调试难度    简单    简单    复杂    复杂
可靠性    非常差    差    高    高
吞吐量    低    中    高    高
4、什么是netty?为什么选择netty? 20181115
4.1、为什么不选择原生NIO: 太过复杂

因为原生NIO类库和API繁杂,需熟练掌握selector,ServerSocketChannel,socketchannel,byteBuffer等
需要其他的技能,多线程,网络编程,Reactor
处理客户端的重连,网络间断,半包读写,失败缓存,网络拥塞,异常流

JOK NIO的bug,epollbug,这会导致linux上cpu10%,使得nio server/client不可用

空轮训的问题:就是本次select操作,没有发生任何时间,造成了selector假死,cpu100%
netty的解决方案:重建selector,将旧的selecor注册时间全部移植到新的selector中,netty设置了一定次数,如果空轮询N次,就重建selector

4.2、为什么选择netty?(异步非阻塞)
Netty是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端,掌握netty需要网络编程、多线程处理和并发这几方面的知识积累。netty是完全基于NIO实现的,通过Future-Listener机制,用户可以主动获取或者通过通知机制获得IO操作结果。非阻塞网络调用使得我们可以不必等待一个操作的完成,选择器使得我们能够通过较少的线程便可以监控许多连接上的事件
同类产品:Mina
说说业务中,netty的使用场景 dubbo远程服务调用 rpc 专门会在dubbo微服务中讲解

5、netty核心组件
1、Channel    可以看做传输数据的载体,进行基本的I/O操作(bind(),read(),write())
2、回调    一个回调其实就是一个方法,一个指向已经被提供给另一个方法的方法引用,使得接受回调的方法可以在适当的时候调用前者。回调是在操作完成后通知相关方的最常见的方式之一。netty在内部使用了回调来处理事件(类似MFC编程):当一个回调被触发时,相关的事件可以被一个interface Channel Handler 的实现处理。例子:当一个新的连接已经被建立时,channelHandler的channelActive()回调方法将被调用,并打印出一条信息。
3、future(异步)    提供了一种在操作完成时通知应用程序的方式,这个对象可以看做是一个异步操作结果的占位符;他将在未来的某个时刻完成,并提供对其结果的访问。例如:    线程池执行submit()方法时会返回Future对象,这个Future对象可以用来检查Runnable是否已经执行完毕;netty提供了它自己的实现–channelFuture,用于在执行异步操作时使用,ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例(消除了手动检查对应的操作是否完成的必要)
4、channelHandler    //类似springmvc中的controller,在netty中,主要是编写handler代码channelhandler负责请求就绪时的io响应
5、bytebuf    支持零拷贝,通过逻辑buff合并实际buff。eventloop:    线程组负责实现线程池,任务队列里就是io请求任务,类似线程池调度执行;acceptor:    接收线程负责接收tcp请求,并且注册任务到队列里
6、NioEventLoop    (相当于NIO中的selector) //单线程–》可以改为线程池,定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事情。创建Channel —> 将Channel注册到EventLoop—>在整个生命周期内都使用EventLoop处理I/O事件
netty实现异步IO?

1、netty是基于java NIO的网络应用框架,提供了对TCP、UDP和文件传输协议的支持;
2、netty的异步编程模型是建立在future和回调的概念之上的,而将事件派发到channelHandler的方法则发生在更深的层次上。用户可以主动获取或者通过通知机制获得IO操作结果。
3、netty通过触发事件将selector从程序中抽象出来,消除了所有本来将需要手动编写的派发代码。内部:将会为每个channel分配一个EventLoop,用来处理所有事件

1、注册感兴趣的事件;
2、将事件派发给ChannelHandler;
3、安排进一步的动作

6、如何使用netty?
1)、服务端启动类EchoServer

配置服务器功能,如线程、端口
实现服务器处理程序,它包含业务逻辑,决定当有一个请求连接或接收数据时该做什么;
创建serverBootStrap实例来引导绑定和启动服务器;
创建NioEventLoopGroup对象来处理时间,如接收新连接、接收数据、写数据等;
指定通道类型为NioServerSocketChannel,设置inetSocketAddress,让服务器监听某个端口已等待客户端连接
最后绑定服务器等待直到绑定完成,调用sync()方法会阻塞直到服务器完成绑定,然后服务器等待通道关闭,因为使用sync(),所以关闭操作也会被阻塞

2)、服务端回调方法
EchoServerHandler extends ChannelInboundHandlerAdapter
读取byteBuf的数据,并向客户端写回数据
3)、客户端启动类 EchoClient

连接服务器 (创建Bootstrap对象)
写数据到服务器 (创建EventLoopGroup对象并设置到Bootstrap中 EventLoopGroup 可以理解为是一个线程池)
等待接受服务器返回相同的数据
关闭连接

4)、客户端回调方法 EchoClientHandler

7、netty如何发送对象?
Netty中,通讯的双方建立连接后,会把数据按照ByteBuf的方式进行传输,和发送字符串的流程相似,原理是通过Encoder把java对象转换成ByteBuf流进行传输

8、netty线程模型?
8.1、reactor线程模型
reactor模型是时间驱动的,有一个或多个并发输入源,有一个serviceHandler,多个request handler;这个service handler会同步将输入的请求event多路复用的分发给响应的requesthandler,类似生产者消费者模式,而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会立刻的根据不同的Event类型将其分发给对应的Request Handler来处理

8.2、Netty支持单线程、多线程模型、主从多线程模型

1)、单线程模型 Reactor机制中保证每次读写能非阻塞读写

一个线程(单线程)来处理CONNECT事件(Acceptor),一个线程池(多线程)来处理read,一个线程池(多线程)来处理write,那么从Reactor Thread到handler都是异步的,从而IO操作也多线程化

2)、Reactor多线程模型
通过Reactor Thread Pool来提高event的分发能力
3)、Reactor主从模型

9、netty文件传输零拷贝 20181222
9.1、什么是零拷贝?

操作系统层面:避免在用户态和内核态之间来回拷贝数据
netty层面:指避免数据在用户态中冗余拷贝,提高数据的传输速率

9.2、netty是如何做到的?主要是3个层面

1、Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写 //无需多次拷贝
2、Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer
3、通过FileRegion包装的FileChannel.tranferTo方法 实现文件传输
传统:数据从磁盘–》内核的read buffer–》用户缓冲区–》内核的socket buffer–》网卡接口的缓冲区
Netty: 调用transferTo,数据从文件由DMA引擎–》内核read buffer-》网卡接口buffer

10、netty设计模式?使用了责任链模式*****
10.1、责任链模式的定义:
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。

10.2、在netty里面,很明显channelHandler和Pipeline构成了责任链模式

责任链的四个要素:

1、责任处理器接口
ChannelHandler就是责任处理器接口(ChannelInboundHandler、ChannelOutboundHandler 是它的两个增强),负责请求就绪时的io响应
2、创建链,添加删除责任处理器接口
ChannelPipeline,里面有各种add和remove的方法
3、上下文 ChannelHandlerContext
里面有两个最重要的方法,一个返回绑定的channel,一个返回executor来执行任务。
消息是如何一步步向下传递?
不停地指向下一个对象
4、责任链终止机制。
自定义一个InBoundHandlerC
ctx.fileChannelRead方法就是为了把责任传递下去,如果注释掉了,消息就不会传递
另外如果不重写channelRead方法,默认会传递

11、netty中handler的执行顺序?
handler与servlet中的filter很像,通过handler可以完成通讯报文的解析编码、拦截指定的报文、统一对日志错误进行处理、统一对请求进行计数、控制handler执行与否。
netty中,可以注册多个handler,
channelInboundHandler按照注册的先后顺序执行
channelOutboundHandler按照注册的先后顺序逆序执行

12、什么是TCP粘包/拆包?netty中解决TCP粘包/拆包的方法?
什么是TCP粘包/拆包?

在业务上一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去(因为TCP的数据量大小时固定的)
例如: 客户端分别发送两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,Server端一次接收到两个数据包,D1和D2粘合在一起,被称为TCP粘包
第一次读取到D1包和D2包的部分内容D2_1,第二次读取到D2包的剩余内容,被称为TCP拆包。

粘包的解决办法

消息定长:FixedLengthFrameDecoder,空位补空格
分隔符类:DelimiterBasedFrameDecoder(自定义分割符)在包尾增加回车换行符进行分割,例如FTP协议
将消息分为消息头和消息体,消息头中包含消息长度的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度

13、netty内部执行流程/原理?2018122
流程    细节
1、初始化channel    serverbootstrap.bind初始化serverSocketChannel
2、注册Channel到selector(boss)    selector位于NIOserverLoop内部
3、轮询accept事件    将ServerSocketChannel注册到selector,监听selectionKey.OP_ACCEPT 。细节:selector轮询就绪的key 处理被选择的key 运行所有任务
4、处理accept建立连接channel    
5、注册channel到selector    
6、向selector注册监听读写操作selectionKey.OP_READ    
7、处理读写事件    
14、netty重连实现?2018122 参数设置根据服务器性能决定
1、长连接通道不断开
适合服务器性能好,客户端数量小的情况
2、容量限制的批量提交数据
先把数据保存到本地临时缓冲区,达到临界值后一次性批量提交,或定时任务轮询提交;弊端:不能实时
3、时间限制的连接
一定时间内,没有任何通信,则断开连接,有请求时,再次建立连接

15、netty的bootstrap、eventLoop、channel、Pipeline、ByteBuf原理分析 暂不用理会


原文:https://blog.csdn.net/qq_28959087/article/details/86501141 

猜你喜欢

转载自blog.csdn.net/haponchang/article/details/90712786