来聊聊NIO吧

了解NIO

分布式系统的基础是网络,但是网络往往是不可靠的,所以一方面就需要更稳健的底层通信技术作为支持,笔者上一次细看这一部分内容时,还是一年多以前,今天重新简单梳理了下这些基础

IO模型可以理解为:如何使用一个高效的数据发收通道,来决定程序通信的性能

Java中主要支持了三种网络IO模型:BIO(阻塞型),NIO(同步非阻塞),AIO(异步非阻塞)

  • JavaBIO的实现模式是一个socket挂载一个线程,即客户端向服务端发起请求就得启动一个线程进行处理,如果这个线程不做任何处理,或者是多个线程之间各自负载极不均衡,这会造成额外的线程开销(在时间和空间上,线程的创建开销,执行开销,销毁开销),但是可以通过线程池来改善

图片

再来了解下线程,线程在Java中也是一个对象,其与进程不同在于,进程是操作系统进行资源调度和分配的基本单位,进程中包括了至少一个线程的存在,而多个线程共享进程的堆内存和方法区,所以这么一来可以确定线程是CPU进行独立调度的基本单位,个人认为这种关系可以这样脑补,进程是部门主管,线程是部门员工,主管为员工分配工作所需与资源(内存,CPU时间片等),而其实所有指令是由操作系统(公司)调度的,同一部门员工与员工(线程之间)沟通与交接工作效率很高,但是跨部门主管之间(进程之间)就需要额外的IPC进行辅助,但是每一个线程都有自己的程序计数器,虚拟机栈与本地方法栈,这些都是线程私有的,他们伴随着线程的一生,朝生夕灭.

线程的种类很多,例如前端中的事件响应线程,HTTP请求线程,这些默默无闻的工作者帮助用户承担了同时发送网络请求与响应其他事件操作的工,包括在虚拟机中的守护线程,他们承担着在主线程结束之后回收所有“生命终结”对象的GC工作,没有GC,就没有如今的Java

再次回到IO的理解,BIO之后,为了能够提升整体对服务资源的可控,NIO与AIO出现了

  • JavaNIO是一种同步非阻塞模型,实现策略为一个线程处理多个连接,即所有连接请求都会注册到多路复用器上,多路复用器对所有连接进行扫描,有IO请求的就进行处理

  • JavaAIO(NIO2.0)是一种异步非阻塞模型,其引入了异步通道的概念,接收到有效的请求才会启动线程,特点是先由操作系统完成后才通知服务端程序启动线程去处理,适用于连接数量多,连接时间长的数据密集型应用

图片

NIO有三大核心部分,Channel,Buffer,Selector,它是直接面向缓冲区的,数据读取到一个到其稍后处理的缓冲区中,需要时可以通过指针扫描缓冲区的位置,灵活性较好,非阻塞模型的好处是如果某一个线程往通道里写数据或者读取数据,如果数据不可用,它不会阻塞在这里,而是直到数据能读/写之前,它可以去处理其他的请求,甚至是不需要等待数据完全写入,在http2.0中,增加了多路复用机制,一个连接能够并发处理其他请求,数量级也比之前大了很多,在这里总结下http的发展

http协议的发展

http目前已经有1.0,1.1,2三个版本了

首先得了解,http是基于TCP/IP协议的,TCP和IP协议是操作系统之间进行通信的一种规则,它的特点就是分层进行了接口规划,分为了应用层,传输层,网路层以及数据链路层

图片

http就属于TCP协议的第四层,属于一种上层协议,同时该层还包括了FTP(文件传输协议)和DNS(域名解析系统),我们熟悉的TCP与UDP协议则位于第三层传输层

所以http1.0诞生了,它是一种无连接无状态的应用层协议,其只能保持短暂的连接,也就是一旦发起连接,且服务器处理完成后,立刻就断开,服务器不会记录用户的状态,同时,频繁的创建与销毁连接也是及耗资源的,要知道TCP本身是基于三次握手,四次挥手的,总结来说,其弊端有两点:1.无法复用连接,2.队头阻塞(如果当前请求一直得不到响应,下一个请求将会阻塞)

http1.0的诞生则是解决了前者性能上的问题,重点来说,1.0增加了Keep-Alive机制,有效保持了客户端与服务端能够进行长连接的需求,以及增加了两个效果并不突出的断点续传管道化传输策略

我们重点来看2.0,2.0的效率获得了很大的提升,也就是加入了多路复用机制,其在应用层和传输层之间加入了一个二进制分层帧,同时进行头部压缩来降低传输时延,其中每一个帧都挂载了一个操作流,流是独立且双向传输的,这样提升了数据交换的效率,同时还增加了优先级控制,这么一整,直接起飞

图片

而http3.0,则是基于UDP传输,其抛弃TCP协议,以全新的视角重新设计HTTP。其底层支撑是QUIC协议,该协议基于UDP,有UDP特有的优势,同时它又取了TCP中的精华,实现了即快又可靠的协议

所以再次回到NIO场景下,其一个很大的优势在于引入了多路复用机制,同时NIO使用了块传输来代替BIO的流传输,效率获得了更大的提升

NIO与AIO模型

图片

这就是一个基本的NIO模型,其中每个channel对应多个buffer,每个线程对应多个channel,有三个channel注册到了selector程序上,程序切换到哪个channel是由event决定的,selector会根据不同的事件,在各个不同通道上进行切换,注意,在这里buffer是一个内存块,在Bio中由于是采用流传输的形式,所以输入流和输出流是分开的,但是NIO中的buffer是既可以读也可以写的,所以说是双向的,但是需要加入一个flip函数进行状态的切换

我们结合NIO里的非阻塞与TCP协议的联系来进一步分析,由于在TCP通信过程中,每一个Socket在内核中都有一个发送缓冲区和接受缓冲区,如果双方有一个没有正常调用,数据都会阻塞在这里,所以TCP就引入了滑动窗口来控制数据包的丢弃,而对于UDP来说,由于其只有一个接受缓冲区,并不存在流量控制,所以快的发送者会很快淹没慢的接受者,所以UDP虽然简单,但也只是秉承着尽最大可能交付的策略,二者各有各的应用场景

所以传统阻塞模式问题可以抽象为,对于读取socket数据而言,如果接受缓冲区为空,则调用socket的read方法将会阻塞,直到数据进入接受缓冲区,与之对应,对于写数据到socket中的线程而言,如果等待发送的数据长度大雨发送缓冲区的空余长度,则会被阻塞在写方法上,直到等待发送缓冲区的数据被发送到网络上,然后空出位置进行下一段数据的传输,如果每一个线程单独监控一个发送-接受通道,效率是很低的,而在Java的NIO框架中,实现了reactor模型,隐藏了NIO的底层细节

图片

Acceptor负责接收客户端Socket发起的新建连接请求,并把该Socket绑定到一个Reactor线程上,于是这个Socket随后的读写事件都交给此Reactor线程来处理。Reactor线程在读取数据后,交给用户程序中的具体Handler实现类来完成特定的业务逻辑处理。为了不影响Reactor线程,我们通常使用一个单独的线程池来异步执行Handler的接口方法,如果仅仅到此为止,则NIO里的Reactor模型还不算很复杂,但实际上,我们的服务器是多核心的,而且需要高速并发处理大量的客户端连接,单线程的Reactor模型就满足不了需求了,因此我们需要多线程的Reactor。一般原则是Reactor(线程)的数量与CPU核心数(逻辑CPU)保持一致,即每个CPU都执行一个Reactor线程,客户端的Socket连接则被随机均分到这些Reactor线程上去处理,如果有8000个连接,而CPU核心数为8,则每个CPU核心平均承担1000个连接

相较于NIO的设计,AIO模型则是更高的一个层次,它重要之处在于,其解决了socket请求与文件的异步处理问题,其核心概念为应用发起非阻塞方式的IO操作,在IO操作完成后通知应用,但是这种设计思想在Linux系统中由于可靠性与安全性难以保证,在内核优化上并没有采用此策略,反而其思想在一些中间件产品,Nginx,redis,Mysql上实现,而这些仅仅也是基于用户线程的用户态实现

更多精彩,欢迎关注我的公众号 James的黑板报

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值