【RPC高性能框架总结】2.NIO的原理以及与传统IO的对比

上一篇我们学习了一个自定义rpc框架的设计思路,在最后我们提到了,rpc技术需要使用Socket进行网络传输,为了其能承受多线程、高并发的网络传输请求,需要使用非阻塞型的NIO框架来处理。本篇就来详细介绍一下什么是NIO,以及NIO的大致原理。

一、简介

NIO是New IO的简称,也叫做“non-blocking IO”,即非阻塞IO,为jdk1.4中提供的新API。当时Sun官方标榜的特性如下:为所有的原始类型提供(Buffer)缓存支持、字符集编码解决方案。其中有一个Channel的概念,即一个新的原始I/O抽象。支持锁和内存映射文件的文件访问接口。
提供多路(non-blocking)非阻塞式的高伸缩性网络I/O。
上面的描述可能初学者会不理解,可以继续向下看,我们将从“信息流读取”以及“网络传输模式”这两大方面,来搞清楚NIO和传统IO的区别,对我们理解NIO有很大的帮助。

二、传统的I/O(BIO)

(1)信息流读取
使用传统的I/O程序读写文件内容,并写入到另一个文件(或socket),如下程序:
File.read(fileDesc,buf,len);
Socket.send(socket,buf,len);
会有较大的性能开销,主要表现在以下两个方面:
1.上下文切换(context switch),此处有4次用户态和内核态的切换(磁盘-操作系统-应用程序-协议引擎)。
2.Buffer内存开销,一个是应用程序buffer,另一个是系统读取buffer以及socket buffer。
其运行示意图如下:

1)先将文件内容从磁盘中拷贝到操作系统buffer。
2)再从操作系统buffer拷贝到应用程序(如JVM)的buffer。
3)然后从应用程序(如JVM)的buffer拷贝到socket buffer。
4)最后从socket buffer拷贝到协议引擎(TCP底层)。
可以发现数据来来回回传输了很多遍,做了很多次数据的复制。所以传统的I/O传输效率慢,前面的原因占了很大一部分。

(2)网络传输模式

传统的I/O名为BIO(blocking IO),即“同步并阻塞IO”。我们在下图的网络传输模型中,来了解一下传统IO的网络传输模式:

备注:教学视频上的图有些缺失,做了适当的补充^_^
上图中,服务端与客户端建立了一个socket网络连接,在连接存在时,服务端会一直处于等待,当请求来临时,客户端需要对每个请求建立一堆线程等待请求。
下面这段代码就是传统的实现“同步并阻塞IO”的JAVA代码:

//创建Socket服务,监听10010端口
ServerSocket server = new ServerSocket(10010);
System.out.println("服务端启动!");
while(true){
    //获取一个套接字(阻塞)
    final Socket socket = server.accept();
    System.out.println("出现一个新客户端!");
    //业务处理
    handler(socket);
}

上面的代码中的for循环,实际上就是一个线程阻塞的、一直等待的死循环。它存在两个阻塞点:“等待客户端连接”,“等待客户端发送信息”。而在不使用线程池的情况下,只能有一个客户端连接。
使用线程池的情况下,可以连接多个客户端:

while(true){
    //获取一个套接字(阻塞)
    final Socket socket = server.accept();
    System.out.println("出现一个新客户端!");
    //在线程池为新客户端开一个线程
    newCachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            //业务处理
            handler(socket);
        }
    });
}

但即使可以连接多个客户端,这里所谓的异步,实际上也是伪异步,因为每一个新的客户端发送请求进来,服务端就要分配一个线程给客户端,当客户端数量十分庞大的时候,长连接的情况下,资源消耗会十分巨大。

NIO在传统IO上做了很大一部分的改造,使得其传输效率会得到很大提高,资源消耗也会变小,下面来瞻仰一下。

三、NIO

(1)信息流读取
NIO技术省去了将操作系统的read buffer拷贝到程序的buffer,以及从程序的buffer拷贝到socket buffer的步骤,直接将read buffer拷贝到socket buffer。java的FileChannel.transferTo()方法就是一个这样的实现,这个实现是依赖于操作系统底层的sendFile()实现的。
public void transferTo(long position,long count,WritableByteChannel terget);
它的底层调用的是系统调用的sendFile()方法:
sendFile(int out_fd,int in_fd,off_t *offset,size_t count);
如下图:

根据上图我们可以看出,NIO比较传统IO的优点在于,当信息从磁盘拷贝到操作系统的read buffer中后,不再读取到应用程序中,而是应用程序发送一个指令,控制操作系统直接将read buffer中的信息读取到socket buffer中,最后socket buffer拷贝到协议引擎(TCP底层)。
相当于之前的4步,变成了3步:
1)先将文件内容从磁盘中拷贝到操作系统buffer。
2)再从操作系统buffer拷贝到socket buffer。
3)最后从socket buffer拷贝到协议引擎(TCP底层)。
延伸:在JDK1.7中,出现了AIO(Asynchronous Input/Output异步输入/输出),AIO比较NIO的话,省去了read buffer到socket buffer的步骤,使用一个映射MAP来控制信息流的流向,相当于读进来之后就可以直接写出去。

(2)网络传输模式
NIO叫做“non-blocking IO”,即“同步非阻塞IO”。Linux进行Socket网络连接通信时,模式其实就是NIO的模式,该模式是由Linux操作系统的内核“Kernel”来完成的。下面我们就举Kernel的例子来给大家讲解NIO。
下图就是Linux内核Kernel提供Socket网络连接服务的一个过程:

上图描述了这样一个过程:
1)内核与监听器的交互
在Linux的服务端,为了接受客户端的请求,会向Kernel内核注册一个监听器“selector”,该监听器会等待内核的回调。当内核发现监听的端口上有客户端的请求事件,就会执行回调事件,通知监听器“selector”,此时监听器“selector”会向Kernel内核注册一个“连接建立”,由Kernel内核去创建一个与客户端的连接。
2)监听器与客户端的交互
当Kernel内核创建好了与客户端的连接,此时会与客户端进行TCP协议的交互,会经历“三次握手”的操作连接成功。此时Kernel内核会通知监听器“selector”连接成功,此时监听器“selector”会注册“READ监听”,该监听的意思是,如果客户端有请求信息过来,你就通知我,没有的话,我做其他的事情(即接受其他的客户端请求)。其实这里就体现了NIO的高性能,即一个监听器“selector”就可以同时处理多个客户端的请求,也不用阻塞等待。
3)监听器读取客户端请求信息
上面的“READ监听”,会对应一个Channel通道,当客户端发送数据时,Kernel内核会有一个tcp缓存,来存储客户端的请求数据。一旦内核发现tcp缓存中存在数据,此时就会将数据存储在应用注册的缓存,即bytebuffer,来交换数据,即内核和应用进行数据交换。然后通知监听器“selector”可以进行读取。此时监听器“selector”会通过Channel通道读取数据,无需一直等待客户端,即是哪个客户端什么时候发了,监听器就什么时候读,如果不发,监听器就去处理别的客户端的请求数据,即一个监听器就可以异步处理多个客户端的请求。
4)总结
上面的所有操作,就是通过事件监听和回调来处理的,每一个客户端拥有自己的Channel,和一个bytebuffer,当bytebuffer有数据时,内核通知监听器,监听器就去读取,没有的话,就去处理其它Channel的bytebuffer。

对比:
传统IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了:
NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 

我有另一篇文章针对IO和NIO做了详细阐述,参见我的博文:
【Netty入门和实践】1.传统的socket分析》:
https://blog.csdn.net/acmman/article/details/80039159
【Netty入门和实践】2.NIO的样例代码分析》:
https://blog.csdn.net/acmman/article/details/80039201

而Java针对高性能的服务端的开发,处理逻辑就类似于c/c++开发的Linux的Kernel内核。其中NIO就是Java针对服务端的高性能处理的核心技术。
篇幅有限,下一篇将为大家总结并简析一套NIO的示例代码。

参考:
传智播客《2017零基础大数据》教学视频

转载请注明出处:https://blog.csdn.net/acmman/article/details/86382614

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光仔December

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值