JAVA后端知识点碎片化整理 基础篇(八) NIO(netty)

因为马上开始2019秋招、平时的学习比较琐碎、JAVA后端博大精深,想在暑假这段时间从头开始整理JAVA知识点查缺补漏,迎战2019秋招。主要参考(微信公众号)JAVA团长与(博客园)五月的仓颉的知识点复习线,对其列出的每一个的知识点再一次的咀嚼并谈谈自己的理解。(平时从这两位学到很多,也非常感谢身边同行的人)补充一下:其中知识点的讲解参考了之前看过的博客讲解或者书籍,之所以称之为基础篇,主要是加深对Java技术栈的宏观认识。(netty可以自己搭建一下并不难,容易理解)

IO就是JAVA中的原始输入输出流,根据类型的不同又分为字符流和字节流,但这都不是重点,重点说一下NIO与IO的区别,JAVA中IO是面向流的,NIO则是面向缓冲区的。JAVA中IO的处理方式就是从流中取到一个或者多个字节,直到读取所有的字节,他们没有被缓存在任何地方。此外,他不能前后移动流的数据,如果需要移动就需要将其移动到一个缓冲区。JAVA的NIO缓冲导向方法略有不同,数据读取到一个它稍后处理的缓冲区,这时候就增加了处理过程中的灵活性,需要确保更多数据读取时候不会覆盖缓冲区中为处理的数据。
1、首先了解几个概念,IO模型分别同步阻塞IO、同步非阻塞IO、异步IO、异步非阻塞IO

同步阻塞IO:在这种方式下,用户进程发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成IO操作以后,用户进程才能运行。(传统IO)
同步非阻塞IO:用户发一起IO操作后可以返回做其他的事情,只有当真正完成了IO操作以后,用户进程才能运行。(目前java中的NIO就是这种)
异步阻塞IO:此种方式下是指应用发起IO操作以后,不等待或轮询内核IO操作的完成,等内核操作完成IO操作以后会通知应用程序,这其实就是同步与异步最关键的区别,同步必须等待或主导去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select的系统调用完成的,select函数的实现方式本身就是阻塞的,能够同时监听的多个文件句柄,从而提高系统的并发性。

异步非阻塞IO:这种方式下应用发起一个IO操作以后立刻返回,等IO操作真正完成之后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作由内核完成。目前java中没有支持此模型,因为异步非阻塞的实现需要系统的兼容,需要操作系统的参与。

这样取解释同步、异步、阻塞、非阻塞:同步意味着自己亲自出马去做某件事(java自己处理IO读写);异步意味着将某件事交给小弟去处理,处理完了再交给你。(java将io委托给OS处理,需要将缓冲区地址和大小传给OS);阻塞:ATM排队取款,你只能等待(使用阻塞IO会一直阻塞到读写完成才返回);非阻塞:柜台区号,大堂经理叫人。(使用非阻塞,如果不能读写Java会马上返回,当IO事件分发器会通知可读写时再继续读写)

2、再说BIO、NIO、AIO 

BIO:同步阻塞IO,服务器实现就是将一个连接转换为一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接做不了任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送连接请求都会注册到多路复用选择器上,多路复用轮询到IO请求时候才会启动一个线程进行处理。

AIO:异步非阻塞,服务实现模式实际是一个有效请求一个线程,客户端的IO请求都是由OS完成了再通知服务器应用去启动线程进行处理。

3、NIO中的核心 Channel Buffer Selector概念

Buffer(缓冲区):使用数据的方式先然后不够灵活而且性能差,JAVAnio缓冲区功能更加强大,容量(Capacity)表示缓冲区的额外建立需要(一个allocate静态方法分配缓冲区空间;读写限制(limit)表示缓冲区进行读写操作时的最大允许位置,读写位置(position)表示前进读写操作时的位置,缓冲区(还有clear,flip(将缓冲区由写模式转变为读模式),rewin)都是去操作limit和position的值来实现的。(这一块可以好好看,因为这种buffer的实现好像很常用)

Channel(通道):channel表示一个已经建立好的支持IO操作的实体(文件或者网络)的连接,在此连接上进行数据的读写操作,使用的缓冲区来实现读写的。

Selector(多路复用器):套接字通道的多路复用的思想比较简单,通过专门的选择器selector来同时对多个套接字通道进行监听,当其中的某些套接字通道上有它感兴趣的事件发生时,这些通道就会变为可用状态,可以再选择器的选择操作中被选中,可用通道的选择一般是通过操作系统提供的底层操作系统调用来实现的,性能也比较高。

4、Netty 作为业界最流行的NIO框架,跳出NIO中的几个点说,它的健壮性、功能、性能、可定制性和可拓展性在同类框架中首屈一指的,已经得到很多项目的验证。优点:API文档简单,真的简单。。。2、功能强大,预置多种协议3、定制功能强大3、ChannelHandler对通信框架扩展灵活。4、性能高,主流NIO框架中,综合性能优秀5、社区活跃,及时修复bug、更多新功能引入。

4.1Netty解决粘包/拆包:一般所谓tcp粘包是在一次数据接收不能完整的数据消息,TCP通信为何存在粘包,主要原因是因为TCP以流的方式来处理数据,再加上网络不能及时的处理消息数据,就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通讯协议来规范现有接收数据是否满足消息的需要。

解决办法,实际上都是应用层的协议来确保1:消息定长,按照报文定长,不够补全。2:包尾增加分隔符,或者以特殊字符作为分隔符3:将其分为消息头消息体,就是指在消息头中包含了消息体的长度。 netty提供如下多个解码器:

* LineBasedFrameDecoder 将会回车作为分隔符
* DelimiterBasedFrameDecoder(添加特殊分隔符报文来分包) 
* FixedLengthFrameDecoder(使用定长的报文来分包) 

* LengthFieldBasedFrameDecoder大多数协议都会携带长度字段,用于标识消息体或者整包消息的长度,Netty提供LenthFeilBasedFrameDecoder自动屏蔽tcp底层的拆包和粘包问题。

4.2Reactor三种线程模型(Reactor模式基于事件驱动,Proactor也是基于事件驱动是两种不同设计模式,前者同步后者异步)

1.1单线程模型


Reactor单线程模型,所有的IO操作都不会阻塞,理论上一个线程可以独立处理所有的IO操作,从架构层面看,一个NIO线程确实可以完成其承担的职责,例如过Acceptor类接受客户端TCP请求消息,链路建立成功之后通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行进行消息解码。(一个NIO线程处理成百上千条链路性能上无法支撑,即便CPU负荷达到100,也无法满足海量消息的编码,解码,读取和发送。最后大量客户端连接超时,超时之后往往会重发,更加加重负担,而且一旦出现问题,NIO线程跑肥了那就整个系统出现故障。)

1.2多线程模型()


Reactor多线程模型和单线程模型最大区别就是一组NIO线程处理IO操作,(1)有专门的NIO线程-Acceptor线程监听服务端,接收客户端TCP连接请求;(2)网络IO操作读,写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,他保护一个任务队列和N个可用线程,这些NIO线程负责消息的读取、解码、编码与发送。(3)一个Nio线程可以处理多个链路,但是一个链路智能对应一个NIO线程。防止发生并发操作。(客户端的请求会被直接丢入线程池中就不会阻塞

1.3主从多线程模型


主从Reactor线程模型的特点:服务端用于接受客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程。Acceptor接收到客户端TCP请求处理完成后(可能保护接入认证等),将新建的SocketChannel注册到IO线程池,一旦链路建立成功就将链路注册到后端的subReactor线程池的io线程上,有IO线程负责后序的操作。主Reactor用于响应连接请求,从Reactor用于处理IO操作请求。 对于netty而言 EventLoopGroup即时一个Reactor,bossgroup对应主Reactor,workerGroup对应Reactor。

4.3Netty“零拷贝”主要体现在以下几个方面:

1、Netty接受和发送ByteBuffer采用的Direct Buffer,使用堆外内存对socket进行读写,不需要对字节缓冲区的二次拷贝,如果使用传统的堆外内存进行socket读写,(内存分为用户内存和系统内存,一般用户内存不允许对系统底层进行直接操作)所以JVM将会对内存Buffer拷贝一份直接内存,然后才写入Socket中,相比堆外内存,消息在发送过程中多一次缓冲区的内存拷贝。

2、Netty提供了CompositeByteBuf,作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer进行操作。真正的实现就是用数据保存他们的引用即地址,让他看起来像一个完整的buffer。整个buffer都有一个读指针和写指针,在读取数据的时候读指针后移,写数据的时候写指针后移。(虚拟的)  wrap操作,可以将byte[]数组和byteBuf包装成一个Netty ByteBuf对象,进而避免了拷贝操作。

4.4Netty的通信建立

1、创建一个ServerBootstrap对象,配置一系列参数,例如接受传出数据缓存的大小等配置信息。。。比如创建serversockechannel将其设置为非阻塞式的,设置tcp参数,backlog的大小。

2、创建两个NIO线程组,一个专门用于处理网络事件的处理(接受客户端的连接bossgroup)一个用于网络通信的读写,(这个worker通信)

3、创建一个世纪处理数据的类ChannelInitializer,进行初始化准备工作,比如设置接受传出数据的字符值,编码解码等

4、绑定端口

4.5、Boss和worker(看一这段很有感触,知道为什么netty用于IM等情况下非常多)

dubbo中对此有一个解释,有一个boss开了一家公司对外提供服务,他手下有一群员工提供服务,并且接受有需要的科幻,当客户找到boss说他需要公司提供服务,那么boss就会为这个客户端安排一个worker去服务(读写)       。其主要的执行流程是这样的,当Severbootstrap监听一个端口对应一个boss线程,他们一一对应,比如你需要netty监听80和443端口,那么就会两个boss线程分别处理两个来自socket请求,当boss线程接受了socket连接请求后会产生channel(一个socket打开一个channel)并且把这个channel交给ServerBootstrap初始化指定的ServerChannelFactory去处理,boss线程完成了他的使命,他继续处理其他的socket请求。ServerSocketChannelFactory会从worker线程池中找到一个worker线程继续处理这个请求。这个woreker在socket没有关闭的情况下只能为这个socket处理消息,(这就是原因)。  处理长连接的时候,就变使用NIoServerSocketChannelFactory,这样每个worker用于不同的socket和channel不再一一对应。对于线程这种资源 ,当处理长连接的时候最好使用NioServerSocketChannelFactory。使用nioServerSocketChannelFactory这种模式,worker线程在执行完messageReceived后就会完成这次任务。   我们写piepline中的handler的时候,@Shareble注解表名是这个hander在不同pipeline之间共享,他的作用就是自动从ExecutionHandler自己管理线程池拿出一个线程来处理他后面的业务逻辑handler,而worker线程在执行了ExectionHandler之后,就会被ChannelFactory的worker线程池所回收。(可以避免handler的重复加载)

4.6Channel 

每个socket都有连接都有一个channel,Netty中的NioServerChannel和NIOSocketChannel分别封装了javaNIO中包括的SocketChannel的功能,该Channel可以获取开关状态信息,支持不同的操作write bind connect等。最终的一点就是channelPipeline可调用请求的相关IO操作,处理事事件流。upstream和downStream,类似一个事件流的过滤器,是典型的拦截器模式。

4.7、selector在netty中的变换情况

selector是NIO中重要的一环,而netty对Selector有了较好的封装。Selector存在与NIO中类似一个观察者,只要把我们需要探知的socketChannel告诉selector,我们接着做别的事情,当有时间发送就会通知我们,传给我们selectionKey,我们读取这些key,就会获取我们刚刚注册的socketchannel,然后我们再从这个channal中读取数据。这里就截图举例:

4.7断线重连

在channelHandler监测连接是否断掉,断掉的话也就要重连。(看网络上的显示)

public class MyInboundHandler extends SimpleChannelInboundHandler{
    private Client client;
    public MyInboundHandler (Client client){
        this.client = client;
    }
    public void channelInactive(ChannelHandler ctx)throws Exception{
        final EventLoop eventLoop = ctx.channel().eventLoop();
        eventLoop.schedule(new Runnable(){
            public void run(){
                client.createBootstrap(new Bootstrap(),eventLoop);
            }
        },1L,TimeUnit.SECONDS);
        super.channelInactive(ctx);
    }
}
顺便解释一下NioEventLoop设计:NioEventLoop的设计并不是单纯的io读写(可以理解它的操作IO线程),还兼顾处理了一下两类方法:一、系统任务,通过调用NioEventLoop的execute来实现某个任务,创建他们的原因是IO线程会用户线程同时操作网络资源,为了防止并发操作导致锁竞争,将用户线程的操作封装成Task放入消息队列,由IO线程负责执行,实现局部无锁化。二、进行轮询也可能为空,没有wakeup操作就是本次轮序的(epollbug就在这好像会导致io线程处于100%)空轮训,当轮询到邮局徐的channel时候就进行对应的网络读写操作( 其实就是不同的EventLoopGroup对应了不同Reactor线程模型)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值