IO通道 ,DMA

 

 

通道处理机 : 通道处理机虽然不是一台具有完整指令系统的处理机,但可以把它看作是一台能够执行有限输入输出指令、能够被多台外围设备共享的小型DMA专用处理器机。

通道完成一次数据传输的主要过程 :

1591516324146

通道的三种类型#

字节多路通道#

  • 适用于字符类低速外围设备, 通道的数据宽度为单字节, 以字节交叉方式轮流的为多台外部设备服务. 如 : 光电机
  • 选中一个设备后传一个字节, 然后换下一个设备

选择通道#

  • 选择通道为优先级高的高速外围设备服务, 如磁盘. 数据传送以成块的方式进行.
  • 每个选择通道只有一个以成组方式工作的子通道, 逐个为多台高速外围设备服务
  • 选中一个设备后必须把该设备所有数据全传送完

数组多路通道#

  • 把字节多路通道和选择通道的特性结合起来
  • 连接多台高速外设, 每次为一台高速设备传送一个数据块, 并轮流为多台外围设备服务

1591517151446

计算通道流量#

计算通道实际最大流量#

  1. 字节多路通道-----各设备字节传送速率之和
  2. 选择通道和数组多路通道-----各设备字节传送速率的最大值

例题#

1591517535780

极限流量 = 各通道最大值之和

DMA

DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

DMA控制器 : DMAC ( Direct Memory Access Controller )

DMA的传输过程#

1591499923991

DMA和中断的比较#

  1. 中断和DMA的响应时间------指令周期结束后响应中断, CPU周期结束后响应DMA

1591499999901

  1. 1591500090472

DMA的工作机制#

1. 周期挪用 ( 窃取 ) 方式 --- 现代计算机#

DMA 控制器对主存储器存取数据常采用周期挪用方式, 即是在中央处理器执行程序期间, DMA控制器为存取数据, 强行插入使用主存储器若干周期;

两种情况 :

  • 隐藏周期DMA : 窃取总线
  • 暂停CPU方式 : 抢总线

特点 :

  • 当主存工作速度都高于外设较多时, 可提高主存的利用率, 且对CPU的影响较小

2. 存储器分时方式 --- Motorala 6800系列#

  • 原来的一个存取周期分割成两个时间片, 一片分给CPU, 一片分给DMAC
  • 无需申请和归还总线
  • 需要主存的工作速度提高一倍
  • Motorola 6800系列8位CPU

3. 停止CPU方式 --- 早期计算机#

  • DMAC : 申请总线 --> 独占总线 --> 释放总线
  • 控制简单 ; 主存利用率不高

4. 扩展时钟周期方式#

两道例题#

例题1

1591512712815

例题2

1591513406478

两次传输 ( 中断 ) 的间隔时间 :

 

(1)14B0.5MB/s=8μs">14B0.5MB/s=8μs(1)(1)14B0.5MB/s=8μs

 

每次传输中断服务时间 :

 

(2)2(20+5)×2×1500×106=0.1μs">2(20+5)×2×1500×106=0.1μs(2)(2)2(20+5)×2×1500×106=0.1μs

 

因此, CPU用于该外设I/O时间占整个CPU时间的百分比为 :

 

(3)0.1μs8μs=1.25%">0.1μs8μs=1.25%(3)(3)0.1μs8μs=1.25%

 

两次DMA传输的间隔时间 :

 

(1)5000B5MB/s=1000μs">5000B5MB/s=1000μs(1)(1)5000B5MB/s=1000μs

 

每次DMA预处理时间 :

 

(2)250×1500×106=0.5μs">250×1500×106=0.5μs(2)(2)250×1500×106=0.5μs

 

百分比 :

 

(3)0.5μs1000μs=0.05%">0.5μs1000μs=0.05%(3)
(3)0.5μs1000μs=0.05%">(3)0.5μs1000μs=0.05%">(3)0.5μs1000μs=0.05%">

 

 

 

 

 

JAVA NIO知识点总结(3)——通道Channel的原理与获取方法

通道用于数据的传输,缓冲区用于数据的存取。

什么是通道

通道( Channel):由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。
 

那么通道与流有什么区别呢?带着这样的问题,我们来看看什么是通道:

之前已经说过,应用程序与磁盘之间的数据写入或者读出,都需要由用户地址空间和内存地址空间之间来回复制数据,内存地址空间中的数据通过操作系统层面的IO接口,完成与磁盘的数据存取。在应用程序调用这些系统IO接口时,由CPU完成一系列调度、任务分配,早先这些IO接口都是由CPU独立负责。所以当发生大规模读写请求时,CPU的占用率很高。

之后,操作系统为了避免CPU完全被各种IO接口调用占用,引入了DMA(直接存储器存储)。当应用程序对操作系统发出一个读写请求时,会由DMA先向CPU申请权限,申请到权限之后,内存地址空间与磁盘之间的IO操作就全由DMA来负责操作。这样,在读写请求的过程中,CPU不需要再参与,CPU去做其他事情。当然,DMA来独立完成数据在磁盘与内存空间中的来去,需要借助于DMA总线。但是当DMA总线过多时,大量的IO操作也会造成总线冲突,即也会影响最终的读写性能。

为了避免DMA总线冲突对性能的影响,后来便有了通道的方式。通道,它是一个完全独立的处理器。CPU是中央处理器,通道本身也是一个处理器,专门负责IO操作。既然是处理器,通道有自己的IO命令,与CPU无关。它更适用于大型的IO操作,性能更高。

总结几个要点:

直接存储器DMA有独立总线。

但在大量数据面前,可能会存在总线冲突,还是需要CPU来处理。

通道是一个独立的处理器

DMA方式还是需要向CPU申请DMA总线的。

通道有自己的处理器,适合与大量IO请求的场景,数据传输直接通过通道进行传输,不再需要请求CPU

(本文出自oschina的happyBKs的博文:https://my.oschina.net/happyBKs/blog/1595866)

如何获取通道

获取通道一般有以下两种主要方式:

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:

 FileInputStream
 FileOutputStream
 RandomAccessFile
 DatagramSocket
 Socket
 ServerSocket

获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。
 

下面我们看一组例子:

我们展示了三种将一个文件通过通道、缓冲区复制出另一个文件的方法:请注意获取通道的方式的不同,以及建立缓冲区方式的不同。

 

 

package com.happybks.nio.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import org.junit.Test;

/**
 * 
 * 一、通道(Channel):用于源节点与目标节点的连接。
 *  在Java NIO中负责缓冲区中数据的传输。
 *  Channel本身不存储数据,因此需要配合缓冲区进行传输(就像铁路必须配合火车才能完成乘客的运输)
 * 
 * 二、通道的主要实现类 
 * java.nio.channels.Channel接口: 
 * 		|-- FileChannel 用于本地文件数据传输 
 * 		|-- SocketChannel 用于网络,TCP 
 * 		|-- ServerSocketChannel 用于网络,TCP 
 * 		|-- DatagramChannel 用于网络,UDP
 * 
 * 三、获取通道(jdk1.7之后有三种方式)
 * 
 *  1、Java只针对支持通道的类提供了getChannel()方法 本地IO:
 * FileInputStream/FileOutputStream 
 * RandomAccessFile 
 * 
 * 网络IO: 
 * Socket 
 * ServerSocket
 * DatagramSocket 
 * 
 * 2、在JDK1.7中的NIO.2提供了针对各个通道提供了静态方法open()
 * 
 * 3、在JDK1.7中的NIO.2的Files工具类的newByteChannel()
 * 
 * 
 * 四、通道之间的数据传输
 * transferForm()
 * transferTo()
 * 
 * @author happyBKs
 *
 */
public class TestChannel {

	/**
	 * 1、利用通道完成文件复制(非直接缓冲区)
	 * @throws IOException
	 */
	@Test
	public void test1() throws IOException {
		long start=System.currentTimeMillis();
		FileInputStream fis = null;
		FileOutputStream fos = null;
		// 1、获取通道
		FileChannel inChannel = null;
		FileChannel outChannel = null;
		try {
			fis = new FileInputStream("D:/Test/NIO/1.jpg");
			fos = new FileOutputStream("D:/Test/NIO/2.jpg");
			
			inChannel = fis.getChannel();
			outChannel = fos.getChannel();

			// 2、分配指定大小的缓冲区
			ByteBuffer buf = ByteBuffer.allocate(1024);

			// 3、将通道中的数据存入缓冲区
			while (inChannel.read(buf) != -1) {
				buf.flip();
				// 4、将缓冲区中的数据写入通道中
				outChannel.write(buf);
				buf.clear();// 清空缓冲区
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			if (outChannel != null) {
				outChannel.close();
			}
			if (inChannel != null) {
				inChannel.close();
			}
			if (fos != null) {
				fos.close();
			}
			if (fis != null) {
				fis.close();
			}
			long end=System.currentTimeMillis();
			System.out.println(end-start);
		}

	}
	
	//2、使用直接缓冲区完成文件的复制(内存映射文件的方式)
	@Test
	public void test2() throws IOException{
		long start=System.currentTimeMillis();
		
		final Path path=Paths.get("D:/", "Test/","NIO/","1.jpg");
		final OpenOption options=StandardOpenOption.READ;
		FileChannel inChannel = FileChannel.open(path, options);
		//注意:StandardOpenOption的CREATE_NEW代表如果已存在则创建失败;CREATE代表如果已存在则覆盖
		//FileChannel outChannel = FileChannel.open(Paths.get("D:/Test/NIO/", "3.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
		FileChannel outChannel = FileChannel.open(Paths.get("D:/Test/NIO/", "3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);
		//注意:因为下面从通道得到的映射文件缓冲区的映射模式是读写模式,而这个outChannel只有写的打开选项,所以是不够,还要加入读配置。
		
		final MapMode mode=MapMode.READ_ONLY;
		final long position=0;
		final long size=inChannel.size();
		//这种利用通道通过映射文件建立直接缓冲区的方式和用缓冲区allocateDirect(int)的方式,两者的原理是一模一样的!
		//只是申请直接缓冲区的方式不同。
		MappedByteBuffer inMappedBuf = inChannel.map(mode, position, size);
		MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
		
		//申请的空间都在物理内存中。
		//注意:申请直接缓冲区,仅仅适用于ByteBuffer缓冲区类型,其他缓冲区类型不支持。
		//与之前的通过流获得的通道不同,这种通过映射文件的方式是直接把数据通过映射文件放到物理内存中,还需要通道进行传输吗?是不是就不用了吧。我现在只需要直接向直接缓冲区中放就可以了,不需要通道。
		//所以与之前相比,获取通道的操作都省去了,直接操作缓冲区即可。
		
		//直接对缓冲区进行数据的读写操作
		byte[] dst=new byte[inMappedBuf.limit()];
		inMappedBuf.get(dst);
		outMappedBuf.put(dst);
		
		inChannel.close();
		outChannel.close();
		long end=System.currentTimeMillis();
		System.out.println(end-start);
	}
	
	/**
	 * 通道之间的数据传输(直接缓冲区)
	 * @throws IOException
	 */
	@Test
	public void test3() throws IOException{
		FileChannel inChannel = FileChannel.open(Paths.get("D:/Test/NIO/", "1.jpg"), StandardOpenOption.READ);
		FileChannel outChannel = FileChannel.open(Paths.get("D:/Test/NIO/", "4.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE_NEW);

		inChannel.transferTo(0, inChannel.size(), outChannel);
		//outChannel.transferFrom(inChannel, 0, inChannel.size());
		inChannel.close();
		outChannel.close();
	}
	
}

 

第一种方式其实还是从流中获取通道。缓冲区用的是缓冲区allocate的非直接缓冲区。

第二种是用通道类本身的静态方法打开一个对应类型的通道。缓冲区用的是通过映射文件申请的直接缓冲区。注意内存映射文件MappedByteBuffer本质上与前面介绍的通过缓冲区类的allocateDirect(int)获取直接缓冲区的方式是本质上一样、方式上不同的。且内存映射文件MappedByteBuffer只有ByteBuffer支持,其他六种类型缓冲区不支持。

利用内存映射文件MappedByteBuffer,可以不要通过通道去完成读写,请看例子中,我们从活动获取了内存映射文件之后,直接操作的是两个内存映射文件MappedByteBuffer。

 

注意内存映射文件MappedByteBuffer和通道的读写模式的权限要匹配,否则会报错。

 

StandardOpenOption.CREATE_NEW和StandardOpenOption.CREATE的区别:

StandardOpenOption.CREATE_NEW 如果文件存在就创建;如果不存在则报错。你可以理解为二奶小三,就是要破坏别人家庭干掉原配,如果人家是单身狗她反而觉得没挑战。

StandardOpenOption.CREATE          如果文件存在就创建;如果不存在也创建。即不管存不存在,文件都创建,而且新的会覆盖旧的。

 

第三种方式仍是用通道类本身的静态方法打开一个对应类型的通道。但它完全利用通道完成整个数据传输,代码中没有任何缓冲区。

 

我们最后来看看实际非直接缓冲区和内存映射文件直接缓冲区,哪种方式的效率高,高多少。

我们将刚才的1.jpg等都换成小电影。别误会:)我找了个《XX动物城》,大约1.5G

但是我今天的实验做得比较失败,不知道是不是windows自身有什么样的缓存策略,还是什么原因,我发现allocateDirect与allocate并有没什么太大性能差别,而且缓冲区大小设置大了,也不能完全断定性能会提高。

	@Test
	public void test1() throws IOException {
		long start=System.currentTimeMillis();
		FileInputStream fis = null;
		FileOutputStream fos = null;
		// 1、获取通道
		FileChannel inChannel = null;
		FileChannel outChannel = null;
		try {
			fis = new FileInputStream("D:/Test/NIO/疯狂动物城1.mkv");//("D:/Test/NIO/1.jpg");
			fos = new FileOutputStream("D:/Test/NIO/疯狂动物城2.mkv");//("D:/Test/NIO/2.jpg");
			
			inChannel = fis.getChannel();
			outChannel = fos.getChannel();

			// 2、分配指定大小的缓冲区
			ByteBuffer buf = ByteBuffer.allocate(1024);

			// 3、将通道中的数据存入缓冲区
			while (inChannel.read(buf) != -1) {
				buf.flip();
				// 4、将缓冲区中的数据写入通道中
				outChannel.write(buf);
				buf.clear();// 清空缓冲区
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			if (outChannel != null) {
				outChannel.close();
			}
			if (inChannel != null) {
				inChannel.close();
			}
			if (fos != null) {
				fos.close();
			}
			if (fis != null) {
				fis.close();
			}
			long end=System.currentTimeMillis();
			System.out.println("非直接缓冲区耗时(ms):"+(end-start));
		}

	}

我运行了好几遍:

非直接缓冲区耗时(ms):37795
非直接缓冲区耗时(ms):29793
非直接缓冲区耗时(ms):26636
非直接缓冲区耗时(ms):45328

即使我把非直接缓冲区大小从1024调大10倍到10240,速度好像是快一些,但是还是差不太多

非直接缓冲区耗时(ms):26174
非直接缓冲区耗时(ms):20005

之后我将allocate方法改为allocateDirect,使用1024大小缓冲区对比实验:发现耗时真的变快了

直接缓冲区耗时(ms):18233
直接缓冲区耗时(ms):17599
直接缓冲区耗时(ms):18339

我把非直接缓冲区大小从1024调大10倍到10240,速度几乎完全一样,没什么提升

直接缓冲区耗时(ms):17153
直接缓冲区耗时(ms):17333

 

但是诡异的事情发生了:我这个时候再运行第一个非直接缓冲区的例子,居然1024大小的非直接缓冲区也能有18秒左右的时间完成。

非直接缓冲区耗时(ms):18905

所以,我看别的资料介绍的一些实验结论今天无法得到证实了,我只把那些经验之谈记下来把:

利用内存映射文件直接缓冲区来通道传输数据 要比 直接缓冲区来的快、性能高;但是内存映射文件直接缓冲区由于是os管理下的内存空间,所以垃圾的回收有时会存在回收问题,现象就是文件已经传输完毕,但是整个程序会有时卡住。但是,今天我的实验结果却完全没有这样的体验,也不能作证这样的观点。如果有哪位这方面了解的多,欢迎分享,尤其是分享code,我想多是一些典型例子。

 

分散(Scatter)和聚集(Gather)

 分散读取( Scattering Reads)是指从 Channel 中读取的数据“分散” 到多个 Buffer 中。

注意:按照缓冲区的顺序,从 Channel 中读取的数据依次将 Buffer 填满。

聚集写入( Gathering Writes)是指将多个 Buffer 中的数据“聚集”到 Channel。

注意:按照缓冲区的顺序,写入 position 和 limit 之间的数据到 Channel 。

 

 * 
 * 五、分散(Scatter)与聚集(Gather)
 * 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
 * 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
 * 
 * 
 * @author happyBKs
 *
 */
	//分散与聚集
	@Test
	public void test4() throws IOException{
		RandomAccessFile raf1=new RandomAccessFile("D:/Test/NIO/t1.txt", "rw");
		
		//1、获取通道
		FileChannel channel1 = raf1.getChannel();
		
		//2、分配指定大小的缓冲区
		ByteBuffer buf1=ByteBuffer.allocate(100);
		ByteBuffer buf2=ByteBuffer.allocate(1024);
		
		//3、分散读取
		ByteBuffer[] bufs={buf1,buf2};
		channel1.read(bufs);
		
		for(ByteBuffer byteBuffer:bufs){
			byteBuffer.flip();
		}
		
		System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
		System.out.println("-----------------------------------------");
		System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
		
		
		//4、聚集写入
		RandomAccessFile raf2=new RandomAccessFile("D:/Test/NIO/t2.txt", "rw");
		FileChannel channel2 = raf2.getChannel();
		
		channel2.write(bufs);
		
		channel2.close();
		raf2.close();
		channel1.close();
		raf1.close();

	}

 

程序运行的结果为:

控制台输出:

直接与非直接缓冲区的要点

 字节缓冲区要么是直接的,要么是非直�
-----------------------------------------
�的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用操作系统基础的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在 直接缓冲区能在程序性能方面带来明显好处时 分配它们。

 直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域

文件生成:

注意这里生成的文件比原先的小,是因为我申请的两个缓冲区的大小之和比t1.txt中的内容字节数少,代码中我只分散读取了一次,所以写入的也只有t1.txt开头的那100+1024=1124字节的内容。

 

最后对通道常用方法的总结:

方 法 描 述
int read(ByteBuffer dst) 从 Channel 中读取数据到 ByteBuffer
long read(ByteBuffer[] dsts) 将 Channel 中的数据“分散”到 ByteBuffer[]
int write(ByteBuffer src) 将 ByteBuffer 中的数据写入到 Channel
long write(ByteBuffer[] srcs) 将 ByteBuffer[] 中的数据“聚集”到 Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中


 

IO通道

 

本文原创,转载需标明原处。

 

通道,主要负责传输数据,相当于流,但流只能是输入或输出类型中的其一,而通道则可以兼并二者。

通道的基类是:Channel

  • boolean isOpen()
  • void close()

 

通道有同步方式和异步方式。

  • 同步方式:亲力亲为,不交给他人来做。
  • 异步方式:需要等待的事情,交给他人来做。做完之后,可以自己接着做,也可以由他人继续接着做。

 

通道有阻塞方式与非阻塞方式。

  • 阻塞方式:在做这件事时,可能需要等待,也可能不需要等待。
  • 非阻塞方式:在做这件事之前,确保不需要等待。换句话说,确保不需要等待的时候,我才来做。例如,银行人多的时候,我就直接离开,没人的时候,我就进入。

 

同步方式的输入输出通道

 

上图表示出,同步方式的输入输出通道的数据流通。这些通道的基类主要是ReadableByteChannel,WritableByteChannel。

  • ReadableByteChannel和WritableByteChannel,操作一个ByteBuffer进行读写字节数据。
  • ScatteringByteChannel和GatheringByteChannel分别是ReadableByteChannel,WritableByteChannel强化版,不仅可以操作一个ByteBuffer,还可以操作一组ByteBuffer。
  • FileChannel提供了更为快捷的操作,直接操作ReadableByteChannel和WritableByteChannel进行读写。
  • ByteChannel仅仅只是ReadableByteChannel和WritableByteChannel的组合,因此自然也是那些同时继承了ReadableByteChannel和WritableByteChannel的Channel的基类。
  • SeekableByteChannel(FileChannel是该类的唯一实现类),其表示不仅可以输入输出,还可以定位操作点,获取容量(文件大小)和缩减容量(文件大小)。

 

异步方式的输入输出通道

上图表示出,异步方式的输入输出通道的数据流通。

异步方式的具体通道类有两个,分别是AsynchronousSocketChannel和AsynchronousFileChannel。

 

输入输出通道

整合所有输入输出的通道,可以有五种源头类型的通道:文件,网络,管道,输入流,输出流。

 

可使用非阻塞方式的通道(以下简称可非阻塞通道)

这类通道的基类是SelectableChannel,默认是阻塞,通过方法configBlocking(boolean)设置是否阻塞,方法isBlocking()查看是否阻塞。

上图表示出具体的可非阻塞通道类,以及表示出非阻塞方式所使用到的各组件之间的关系图。

具体怎么使用,可以参考其它相关的文章,最重要还是记得一点,非阻塞的概念就是确保这件事情不需要等待的时候才去做。怎么确保,selector的select()系列方法,这些方法会阻塞,阻塞到它检测到那些事情不需要等待了,就会让我去做。

需要注意的是,在使用非阻塞方式的方法时,必须先使用configBlocking(false),设置为非阻塞。

 

异步方式的通道

异步的概念,就是把需要等待的事情,委托给他人来做,做完的时候,可以选择自己接着做,也可以继续由受委托的人接着做。

那么异步有两种结果,一种是自己接着做,一种是他人接着做。

自己接着做的方式,操作方法的返回值是一个Future对象,使用这个对象可以检测完成进度。

  • boolean isDone():测试是否完成。
  • boolean isCanceled():测试异步运行是否被中止。
  • boolean cannel(boolean):中止异步运行,并返回是否中止成功。
  • V get()/get(long, TimeUnit):有些操作需要获取运行的返回值,如读取时,需要知道读取的字节数。但如果异步运行未完成或未中止,就会进入阻塞。

他人接着做的方式,操作参数里需要传入一个CompletionHandler对象,这个对象的方法将由异步线程来调用。

  • void completed(V result, A attachment):完成时异步运行的方法。
  • void failed(Throwable exc, A attachment):失败时异步运行的方法。

异步方式的通道基类是AsynchronousChannel,但这个类仅仅只是一个身份类,并没有提供任何操作方法。它的子类有:

  • AsynchronousFileChannel
  • AsynchronousByteChannel:这是一个接口,表示不仅具有异步方式,同时是输入输出字节类型的通道。但唯一的实现类为AsynchronousSocketChannel。
  • AsynchronousServerSocketChannel
  • AsynchronousSocketChannel

 

其它通道

  • NetworkChannel:网络通道,用于网络的通道。
  • InterruptibleChannel:表示线程被中止时,可以中断输入输出的通道。

 

通道的建立

一般使用静态方法open建立,其它方式的建立,如下图:

 

 

待续更新……

 

1 阻塞IO

传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 accept(),read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。

阻塞accept()

当没有客户端连接时,用户进程会一直阻塞。

阻塞IO读取
 
image

对于一次IO读取,数据会先从网络设备缓冲区被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的缓冲区,最后返回。

如果在网络设备缓冲区中没有发现数据会导致应用程序进程阻塞等待。

阻塞IO写入
 
image

对于一次IO写入,数据会先应用程序的缓冲区被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到网络设备缓冲区,最后返回。

在网络阻塞严重的时候,由于网络设备的缓冲区数据无法发送到网络中,会一直堆积直到没有足够的内存来进行写操作,从而导致应用程序进程阻塞等待。

2 阻塞IO 单线程

        @Test
        public void test_server() throws Exception {
            /** 创建服务器套接字通道 ServerSocketChannel **/
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            /** 绑定监听 InetSocketAddress **/
            serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));

            /** 设置为阻塞IO模型 **/
            serverSocketChannel.configureBlocking(true);

            while (true){
                System.out.println("阻塞等待客户端连接" );
                SocketChannel socketChannel = serverSocketChannel.accept();
                System.out.println("客户端连接成功");

                ByteBuffer readbuf = ByteBuffer.allocate(100);
                System.out.println("阻塞等待客户端请求数据" );
                /**
                 * 等待  网络设备 --copy-->操作系统内核缓冲区
                 * 等待  操作系统内核缓冲区 --copy --> 用户内存
                 * 此函数一共经历2次拷贝等待
                 * **/
                socketChannel.read(readbuf);
                System.out.println("读取客户端请求数据成功" );
                System.out.println(new String(readbuf.array()));

            }
        }

        @Test
        public void test_readcanunblock_client() throws Exception {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(true);
            System.out.println("阻塞服务端端响应连接" );
            socketChannel.connect(new InetSocketAddress("localhost", 8888));
            System.out.println("连接服务端成功" );

            /** 等待一段事件向客户端后向服务请求数据 **/
            TimeUnit.SECONDS.sleep(30);
            socketChannel.write(ByteBuffer.wrap("hello server".getBytes()));
        }

由于整个接收请求和处理请求都是在同一个线程里,按照当前示例同时只能处理一个连接请求。如果某个请求很慢会导致其他请求阻塞。

 
image

阻塞IO 多线程

使用多线程技术将请求操作交给子线程完成,可以让多个请求同时处理。

@Test
    public void test_server2() throws Exception {
        /** 创建服务器套接字通道 ServerSocketChannel **/
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        /** 绑定监听 InetSocketAddress **/
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));

        /** 设置为阻塞IO模型 **/
        serverSocketChannel.configureBlocking(true);

        while (true){
            System.out.println("阻塞等待客户端连接" );
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("收到客户端");
            new BeginThread(socketChannel).start();
        }
    }
 
image

这种方式每次一个新的连接,都会启动一个线程。如果存在1百万个连接,那么需要创建1百万个线程,JVM会对限制进程线程的数量,如果超过这时会抛出异常。即使设置支持1百万个线程,那么按照一个连接最少64k内存来算,64k*1000000 约 61G,也足以让OOM将进程杀死

阻塞IO 线程池

为了解决同步阻塞IO面临一个链路需要一个线程处理的问题。后端通过一个线程池来处理多个客户端的请求。通过线程池可以灵活的调配线程池资源,设置线程的最大值,防止由于海量并发请求导致线程耗尽。

 @Test
    public void test_server3() throws Exception {
        /** 创建服务器套接字通道 ServerSocketChannel **/
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        /** 绑定监听 InetSocketAddress **/
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));

        /** 设置为阻塞IO模型 **/
        serverSocketChannel.configureBlocking(true);

        while (true){
            System.out.println("阻塞等待客户端连接" );
            SocketChannel socketChannel = serverSocketChannel.accept();
            System.out.println("收到客户端");

            ExecutorService executorService = Executors.newFixedThreadPool(10);
            executorService.execute(new BeginThread(socketChannel));
        }
    }

阻塞IO 线程池弊端

使用线程池是为管控进程内的线程资源。当遇到海量的请求时,线程池内部的工作线程数量N,一定远远小于请求任务数量M。当线程池内阻塞队列中任务满时只能阻塞新请求或拒绝新请求。这就需要我们能快速处理请求。然而阻塞IO 发起read() 或 write()时,该线程都是同步阻塞的。阻塞时间取决于对方IO线程的处理速度和网络IO的传输速度。相当于我们应用程序需要依赖对方处理速度,导致我们应用程序的可靠性降低。

案例

如果我们应用程序在和一个故障节点通信。

  • 1 阻塞IO读取故障节点的数据,由于读取输入流是阻塞的。因此同步阻塞时间又平时的10S变成60S

  • 2 假如大量线程池内工作线程都在读取这个故障节点数据,那么由于线程池内工作线程处理缓慢,导致新的请求处理不过来,被放入阻塞队列中。

  • 3 由于阻塞队列容量有限。当超过限制时只能阻塞或拒绝新的请求。

2 非阻塞IO

Java NIO 是非阻塞模式的。当一个线程调用 accept(),read() 或 write()时,该线程不会被阻塞,会直接返回。

非阻塞accept()

相对于阻塞IO,非阻塞会直接返回。当不存在客户端连接时serverSocketChannel.accept()会返回一个NULL。来告知应用程序没有客户端连接。

非阻塞IO读取

相对于阻塞IO读取,非阻塞如果在网络设备缓冲区中没有发现数据不会阻塞而会直接返回。我们可以通过判断返回读取数据大小。来判断网络设备缓冲区中是否存在数据读取。

非阻塞IO写入

相对于阻塞IO写入,在网络阻塞严重的时候,由于网络设备的缓冲区数据无法发送到网络中,会一直堆积直到没有足够的内存来进行写操作,这时并不会阻塞当前线程而是写入失败返回。我们可以通过判断返回写入数据大小。判断是否写入成功。

  /**
     * 非阻塞 IO
     */
    @Test
    public void test_UnBlock_server() throws Exception {
        /** 创建服务器套接字通道 ServerSocketChannel **/
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        /** 绑定监听 InetSocketAddress **/
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));

        /** 设置为非阻塞IO模式 **/
        serverSocketChannel.configureBlocking(false);

        boolean is_Run = true;
        while (is_Run) {
            /** 非阻塞IO模式,accept()会立刻返回,如果请求没有到达返回null **/
            SocketChannel socketChannel = serverSocketChannel.accept();
            /** 判断请求是否到达 **/
            if (Optional.ofNullable(socketChannel).isPresent()) {
                boolean is_Read = true;
                while (is_Run) {
                    /** 设置为非阻塞IO模式 **/
                    socketChannel.configureBlocking(false);
                    ByteBuffer byteBuffer = ByteBuffer.allocate(100);
                    /** 非阻塞IO模式,read()会立刻返回,,如果请求没有数据到达返回0 **/
                    int read = socketChannel.read(byteBuffer);
                    if (read > 0) {
                        System.out.println(new String(byteBuffer.array()));
                        break;
                    } else {
                        System.out.println("客户端请求数据未到达");
                        TimeUnit.SECONDS.sleep(1);
                    }
                }
            } else {
                System.out.println("客户端请求连接未到达");
                TimeUnit.SECONDS.sleep(2);
            }
        }
    }

    @Test
    public void test_UnBlock_client() throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        /** 设置为非阻塞IO模式 **/
        socketChannel.configureBlocking(false);
        /** 非阻塞IO模式,accept()会立刻返回  **/
        socketChannel.connect(new InetSocketAddress("localhost", 8888));

        /** 等待客户端连接成功 **/
        while (!socketChannel.finishConnect()){

        }

        /** 等待一段事件向客户端后向服务请求数据 **/
        TimeUnit.SECONDS.sleep(5);
        socketChannel.write(ByteBuffer.wrap("hello server".getBytes()));
        System.out.println("数据已发送");
        while (true) {
            TimeUnit.SECONDS.sleep(1);
        }
    }
    
//服务端
客户端请求连接未到达
客户端请求连接未到达
客户端请求连接未到达
客户端请求连接未到达
客户端请求连接未到达
客户端请求数据未到达
客户端请求数据未到达
客户端请求数据未到达
客户端请求数据未到达
hello server                                                                                        
客户端请求连接未到达
客户端请求连接未到达
客户端请求连接未到达
客户端请求连接未到达
客户端请求连接未到达

//客户端
数据已发送

非阻塞IO 和 Linux 非阻塞I/O模型 向对应

 
image

2 非阻塞IO + Selector

Selector 一般称为选择器,用来作为SelectableChannel通道的多路复用器。SelectableChannel类型通道可以被注册到多路复用器,通过多路复用器监听感兴趣的事件,这样就可以通过Selector实现单个线程可以管理多个SelectableChannel通道,从而管理多个网络连接。

 
image

选择器对应 I/O多路复模型

 
image
@Test
    public void test_unBlock_selector_server() throws Exception {
        /** 实例化一个选择器对象 **/
        Selector selector = Selector.open();

        /** 创建服务器套接字通道 ServerSocketChannel **/
        ServerSocketChannel serverSocketChannel1 = ServerSocketChannel.open();
        /** 绑定监听 InetSocketAddress **/
        serverSocketChannel1.bind(new InetSocketAddress("localhost", 7777));
        /** 设置为非阻塞IO模型 **/
        serverSocketChannel1.configureBlocking(false);
        /** 将serverSocketChannel通道注册到selector选择器中,并设置感兴趣的事件OP_ACCEPT,并返回SelectionKey**/
        serverSocketChannel1.register(selector, SelectionKey.OP_ACCEPT);

        boolean is_Run = true;
        while (is_Run) {

            /** 阻塞等待事件到达**/
            selector.select();

            /** 获取到达事件SelectionKey集合**/
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            /** 遍历SelectionKey**/
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                /** 将获取SelectionKey 从publicSelectedKeys集合中删除,防止重复处理**/
                iterator.remove();
                /** 判断OP_ACCEPT事件是否到达 **/
                if (key.isAcceptable()) {
                    /** 从SelectionKey获取对应通道ServerSocketChannel**/
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                    /** 获取SocketChannel**/
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    /** 设置为非阻塞IO模型 **/
                    socketChannel.configureBlocking(false);
                    /** 将serverSocketChannel通道注册到selector选择器中,并设置感兴趣的事件OP_READ(当客户端请求数据时事件到达) **/
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    /** 向客户端发送消息 **/
                    socketChannel.write(ByteBuffer.wrap("hello client".getBytes()));
                }
                /** 判断OP_READ事件是否到达 **/
                else if (key.isReadable()) {
                    /** 从SelectionKey获取对应通道SocketChannel**/
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    /** 读取客户端发送数据 **/
                    ByteBuffer byteBuffer = ByteBuffer.allocate(100);
                    socketChannel.read(byteBuffer);
                    System.out.println(new String(byteBuffer.array()));
                }
            }
        }
    }

    @Test
    public void test_unBlock_selector_client() throws Exception {
        /** 实例化一个选择器对象 **/
        Selector selector = Selector.open();

        /** 创建套接字通道 SocketChannel **/
        SocketChannel socketChannel = SocketChannel.open();
        /** 设置为非阻塞IO模型 **/
        socketChannel.configureBlocking(false);
        /** 发起连接 **/
        socketChannel.connect(new InetSocketAddress("localhost", 7777));
        /** 将socketChannel通道注册到selector选择器中,并设置感兴趣的事件OP_CONNECT,并返回SelectionKey**/
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        boolean is_Run = true;
        while (is_Run) {
            /** 阻塞等待事件到达**/
            selector.select();

            /** 获取到达事件SelectionKey集合**/
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            /** 遍历SelectionKey**/
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                /** 将获取SelectionKey 从publicSelectedKeys集合中删除,防止重复处理**/
                iterator.remove();
                /** 判断OP_CONNECT事件是否到达 **/
                if (key.isConnectable()) {
                    /** 从SelectionKey获取对应通道socketChannel**/
                    socketChannel = (SocketChannel) key.channel();
                    while (!socketChannel.finishConnect()){
                    }
                    /** 将serverSocketChannel通道注册到selector选择器中,并设置感兴趣的事件OP_READ(当客户端请求数据时事件到达被添加到selectedKeys集合中) **/
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    /** 向服务端发送消息 **/
                    socketChannel.write(ByteBuffer.wrap("hello server".getBytes()));
                }
                /** 判断OP_READ事件是否到达 **/
                else if (key.isReadable()) {
                    /** 从SelectionKey获取对应通道socketChannel**/
                    socketChannel = (SocketChannel) key.channel();
                    /**读取服务端发送消息 **/
                    ByteBuffer byteBuffer = ByteBuffer.allocate(100);
                    int read = socketChannel.read(byteBuffer);
                    System.out.println(new String(byteBuffer.array()));
                }
            }
        }
    }

阻塞IO VS 非阻塞IO+ Selector

从流程上来看,IO多路复用需要将通道注册到选择器上,通过选择器监听事件。用户通过select()阻塞当前线程等待事件到达。当事件到达后调用对应函数处理,如读取事件到达调用read()将数据从内核缓冲区拷贝到用户缓冲区。相对于阻塞IO来说进行了两个函数调用。同时也阻塞效率更差。

但是这个选择器作用在于可以用一个线程监听多个socket连接。只有当建立连接的socket发出请求时才会创建一个线程来处理。对于不活跃的连接并不会创建一个线程来处理。这样大大提供了CPU使用率。解决阻塞IO中单连接单线程1:1的问题。

服务端好比一个餐厅。刚开始时餐厅只有老板一个人作为服务员。每当来一个客户进入餐厅老板都会亲自为客户服务需要自己处理,直到客户离开。由于老板服务很周到,餐厅的客户越来越多,老板不得不开始雇佣服务员为更多客户同时服务。而这种方式在高峰时段老板不得 不雇佣超过成本上线得员工。老板为了控制成本,决定只雇佣一定数量员工。如果员工数量忙不过来只能让客户等待。时间长了老板发现起始很多客户大多数时间只有在点餐和加餐时候需要员工服务。大多数时候这种1对1得模式员工只是等待客户提需求并没有任何工作。因此老板购买了一个点餐系统。但客户进餐厅时,会给客户分配一个按铃。当客户需要点餐时。就可以点击这时会指派一名员工为客户服务。

 https://blog.51cto.com/u_15127692/3971359 一文说清 BIO、NIO、AIO 不同 IO 模型演进之路
一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_数据
引言

Netty 作为高性能的网络通信框架,它是 IO 模型演变过程中的产物。Netty 以 Java NIO 为基础,是一种基于异步事件驱动的网络通信应用框架,Netty 用以快速开发高性能、高可靠的网络服务器和客户端程序,很多开源框架都选择 Netty 作为其网络通信模块。本文主要通过分析 IO 模型的优化演进之路,比较不同 IO 模型的异同,让大家对于 Java IO 模型有着更加深刻的理解,我想这也是 Netty 如何实现高性能网络通信理解的重要基础。话不多说,我们赶紧发车了

 

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_文件描述符_02

 

IO 模型

1、什么是 IO

在阐述 BIO、NIO、AIO 之前,我们先来看下到底什么是 IO 模型。我们都知道无论是程序还是平台,它们的功能高度抽象之后其实可以描述为这样一个过程,即为通过外部条件以及数据的输入,经过程序或者平台的处理产生了新的输出,IO 模型实际上就是描述了计算机世界中的输入和输出过程的模式。

对于计算机来说,其键盘以及鼠标等就是输入设备,显示器以及磁盘等就是输出设备。举个栗子,如果我们在计算机上写一篇设计文档并进行保存,实际就是通过键盘对计算机进行了数据输入,完成设计文档后将其保存输出到了计算机的磁盘上。

 

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_文件描述符_03

 

上图中的 IO 描述,即为著名的计算机冯诺依曼体系,它大致描述了外部设备与计算机的 IO 交互过程。

2、应用程序 IO 交互

上文中我们介绍了计算机与外部设备交互的大致过程,那么我们的应用程序是如何进行 IO 交互的呢?我们平时编写的代码不会独立的存在,它总是被部署在 linux 服务器或者各种容器中,应用程序在服务器或者容器中启动后再对外提供服务。因此网络请求数据首先需要和计算机进行交互,才会被交由到对应的程序去进行后续的业务处理。

 

在 Linux 的世界中,文件是用来描述 Linux 世界的,目录文件、套接字等都是文件。那文件又是什么鬼呢?文件实际就是二进制流,二进制流就是人类世界与计算机世界进行交互的数据媒介。应用从流中读取数据即为 read 操作,当把流中的数据进行写入的时候就是 write 操作。但是 linux 系统又是如何区分不同类型的文件呢?实际是通过文件描述符(File Descriptor)来进行区分,文件描述符其实就是个整数,这个整数实际是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。所以对这个整数的操作、就是对这个文件(流)的操作。

 

就拿网络连接来说,我们创建一个网络 socket,通过系统调用(socket 调用)会返回一个文件描述符(某个整数),那么后续对 socket 的操作就会转化为对这个描述符的操作,主要涉及的操作包括 accept 调用、read 调用以及 write 调用。这里所说的各种调用就是程序通过 Linux 内核与计算机进行交互。那么问题又来了,这个计算机内核又是什么鬼。(PS:关于内核不是本文的重点,这里就简单和大家说明下)

 
//socket函数socket(PF_INET6,SOCK_STREAM,IPPROTO_IP)
  • 1.
 
 
 
 
 
复制代码
 

但是实际上应用程序并不是直接从计算机中的网卡中获取数据,也就是说大家编写的程序并不是直接操作计算机的底层硬件。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_java_04

 

如上图所示,在 Linux 的结构体系中,用户的应用程序都是通过 Linux Kernel 内核来操作计算机硬件。那么为什么应用程序不能直接与底层硬件进行交互还需要在中间再加一层内核呢?主要有以下几点考虑。

(1)计算机资源统一管理

Linux 内核的作用就是进程调度管理,同时对 cpu、内存等系统资源进行统一管理。因此内核管理的都是系统极其敏感的资源,采用内核制是为了实现系统的网络通信,用户管理,文件系统等安全稳定的进程管理,避免用户应用程序破坏系统数据。

(2)底层硬件调用统一封装

试想一下,如果没有内核这层系统进程,那么每个用户应用程序和硬件交互的时候都需要自己实现对应的硬件驱动。这样的设计很难让人接受,按照面向对象的设计思想,硬件的管理统一由 Kernel 内核负责,Kernel 向下管理所有的硬件设备,向上提供给用户进程统一的系统调用,方便应用程序可以像程序调用一样进行系统硬件交互。

 

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_文件描述符_05

3、5 种 IO 模型

(1)阻塞型 IO

当用户应用进程发起系统调用之后,在内核数据没有准备好的情况下,调用一直处于阻塞状态,直到内核准备好数据后,将数据从内核态拷贝到用户态,用户应用进程获取到数据后,本次调用才算完成。就好比你是外卖小哥,你到商家去取餐,商家的外卖还没有准备好,所以你只能在取餐的地方一直等待着,直到商家将做好的外卖准备好,你才能拿了外卖去送餐。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_java_06

(2)非阻塞型 IO

非阻塞 IO 式基于轮询机制的 IO 模型,应用进程 买QQ账号平台 地图不断轮询检查内核数据是否准备好,如果没有则返回 EWOULDBLOCK,进程继续发起 recvfrom 调用,此时应用可以去处理其他业务。当内核数据准备好后,将内核数据拷贝至用户空间。这个过程就好比外卖小哥在等待取餐的时候不断问商家外卖做好了没(这个外卖小哥比较着急,送餐时间比较临近了),每隔 30s 问一次,直到外卖做好送到。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_文件描述符_07

 

(3)多路复用 IO

Linux 主要提供了 select、poll 以及 epoll 等多路复用 I/O 的实现方式,为什么会有三个实现呢?实际上他们的出现都是有时间顺序的,后者的出现都是为了解决前者在使用中出现的问题。

在实际场景中,后端服务器接收大量的 socket 连接,IO 多路复用是实际是使用了内核提供的实现函数,在实现函数中有一个参数是文件描述符集合,对这些文件描述符(FD)进行循环监听,当某个文件描述符(FD)就绪时,就对这个文件描述符进行处理。

 

下面我们分别看下 select、poll 以及 epoll 这三个实现函数的实现原理:

select:

select 是操作系统的提供的内核系统调用函数,通过它可以将一组 FD 传给操作系统,操作系统对这组 FD 进行遍历,当存在 FD 处于数据就绪状态后,将其全部返回给调用方,这样应用程序就可以对已经就绪的 IO 流进行处理了。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_应用程序_08

select 在使用过程中存在一些问题:

1)select 最多只能监听 1024 个连接,支持的连接数较少;

2)select 并不会只返回就绪的 FD,而是需要用户进程自己一个一个进行遍历找到就绪的 FD;

3)用户进程在调用 select 时,都需要将 FD 集合从用户态拷贝到内核态,当 FD 较多时资源开销相对较大。

poll:

poll 机制实际与 select 机制区别不大,只是 poll 机制去除掉了监听连接数 1024 的限制。

epoll:

epoll 解决了 select 以及 poll 机制的大部分问题,主要体现在以下几个方面:

1)FD 发现的变化:内核不再通过轮询遍历的方式找到就绪的 FD,而是通过异步 IO 事件唤醒的方式,当 socket 有事件发生时,通过回调函数将就绪的 FD 加入到就绪事件链表中,从而避免了轮询扫描 FD 集合;

2)FD 返回的变化:内核将已经就绪的 FD 返回给用户,用户应用程序不需要自己再遍历找到就绪的 FD;

3)FD 拷贝的变化:epoll 和内核共享同一块内存,这块内存中保存的就是那些已经可读或者可写的的文件描述符集合,这样就减少了内核和程序的内存拷贝开销。

 

(4)信号驱动 IO

系统存在一个信号捕捉函数,该信号捕捉函数与 socket 存在关联关系,在用户进程发起 sigaction 调用之后,用户进程可以去处理其他的业务流程。当内核将数据准备好之后,用户进程会接收到一个 SIGIO 信号,然后用户进程中断当前的任务发起 recvfrom 调用从内核读取数据到用户空间再进行数据处理。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_java_09

 

(5)异步 IO

所谓异步IO模型,就是用户进程发起系统调用之后,不管内核对应的请求数据是否准备好,都不会阻塞当前进程,立即返回后进程可以继续处理其他的业务。当内核准备好数据之后,系统会从内核复制数据到用户空间,然后通过信号通知用户进程进行数据读取处理。

 

Java 中的 IO 模型

上文中我们阐述了 Linux 本身存在的几种 IO 模型,那么对应到 Java 程序世界中,Java 也有对应的 IO 模型,分别是 BIO、NIO 以及 AIO 三种 IO 模型。它们都提供了和 IO 有关的 API,这些 API 实际也是依赖系统层面的 IO 完成数据处理的,因此 Java 的 IO 模型,实际就是对系统层面 IO 模型的封装。接下来我们来一起看下 Java 的这几种 IO 模型。

BIO

BIO 即为 Blocking IO,顾名思义就是阻塞型 IO 模型,当用户进程向服务端发起请求后,一定等到服务端处理完成有数据返回给用户,用户进程才完成一次 IO 操作,否则就会阻塞住,像个痴心汉傻傻的一直等待数据返回,当数据完成返回后用户线程才会解除 block 状态,因此在整个数据读取过程中会发生阻塞。

另外从下图我们可以看出来,每一个客户端连接,服务端都有对应的处理线程来处理对应的请求。还是以餐厅吃饭的例子,你到餐厅去吃饭,假如每来一个消费者,餐厅都用一个服务员来接待直到消费者吃饱喝足走出餐厅,那么这个餐厅得配置多少个服务员才合适?这么多服务员,餐厅的老板估计得赔的内裤都没了。

因此在网络连接不多的情况下,BIO 还能发回作用。但是当连接数上来后,比如几十万甚至上百万连接,BIO 模型的 IO 交互就显得心有余而力不足了。当连接数不断攀高时,BIO 模型的 IO 交互方式存在以下几种弊端。

(1)频繁创建和销毁大量的线程会消耗系统资源给服务器造成巨大的压力;

(2)另外大量的处理线程会占用过多的 JVM 内存,你的程序不要干其他事情了,都被大量连接线程给占满了;

(3)实际上线程的上下文切换成本也是很高的。

基于 BIO 模型在处理大量连接时存在上述的问题,因此我们需要一种更加高效的线程模型来应对几十万甚至上百万的客户端连接。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_linux_10

 

NIO

通过上文的分析,由于在 BIO 模型下,Java 中在进行 IO 操作时候是没办法知道什么时候可以读数据或者什么时候可以写数据,BIO 又是一个实在孩子因此没有什么好的办法只能在哪里傻等着。由于 socket 的读写操作不能进行中断,因此当有新的连接到来时,只能不断创建新的线程来处理,从而导致存在性能问题。

那么如何解决这个问题呢?我们都知道问题的根源就是 BIO 模型中我们不知道数据的读取与写入的时机,才导致的阻塞等待,那么如果我们能够知道数据读写的时机,是不是就不用傻傻的等着响应,也不用再创建新的线程来处理连接了。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_应用程序_11

为了提升 IO 交互效率,避免阻塞傻等的情况发生。Java 1.4 中引入了 NIO,对于 NIO 来说,有人称之为 Non-blocking IO,但是我更愿意称之为 New IO。因为它是一种基于 IO 多路复用的 IO 模型,而不是简单的同步非阻塞的 IO 模型。所谓 IO 多路复用指的就是用同一个线程处理大量连接,多路指的就是大量连接,复用指的就是使用一个线程来进行处理。

 

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_文件描述符_12

那我们先来看看同步非阻塞模型有什么问题,NIO 的读写以及接受方法在等待数据就绪阶段都是非阻塞的。如上文中的描述,同步非阻塞模式下应用进程不断向内核发起调用,询问内核数据完成准备。相对于同步阻塞模型有了一定的优化,通过不断轮询数据是否准备好,避免了调用阻塞。但是由于应用不断进行系统 IO 调用,在此过程中十分消耗 CPU,因此还有进一步优化的空间。此时就该 IO 多路复用模型上场一展拳脚了,而 Java 的 NIO 正是借助于此实现了 IO 性能的提升。(这里以 epoll 机制来进行说明)

Java NIO 基于通道和缓冲区的形式来处理流数据,借助于 Linux 操作系统的 epoll 机制,多路复用器 selector 就会不断进行轮询,当某个 channel 的事件(读事件,写事件,连接事件等等)准备就绪的时候,就是会找到这个 channel 对应的 SelectionKey,去做相应的操作,进行数据的读写操作。

 

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_linux_13

AIO

所谓 AIO(Asynchronous IO)就是 NIO 第二代,它是在 Java 7 中引入的,是一种异步 IO 模型。异步 IO 模型是基于事件和回调机制实现的,当应用发起调用请求之后会直接返回不会阻塞在那里,当后台进行数据处理完成后,操作系统便会通知对应的线程来进行后续的数据处理。

从效率上来看,AIO 无疑是最高的,然而,美中不足的是目前作为广大服务器使用的系统 linux 对 AIO 的支持还不完善,导致我们还不能愉快的使用 AIO 这项技术,Netty 实际也是使用过 AIO 技术,但是实际并没有带来很大的性能提升,目前还是基于 Java NIO 实现的。

总结

本文主要从计算机 IO 交互出发,分别给大家介绍了什么是 IO 模型以及常见的五种 IO 模型,介绍了这几种 IO 模型的优缺点,从系统优化演进的角度分析了 Java BIO、NIO 以及 AIO 演化之路。从设计者的角度分析 Java BIO 存在的不足。我们再来回顾下整个演进过程的脉络。

一文说清 BIO、NIO、AIO 不同 IO 模型演进之路_文件描述符_14
 

Java IO模型:BIO、NIO、AIO

本来是打算直接学习网络框架Netty的,但是先补充了一下自己对Java 几种IO模型的学习和理解。分别是 BIO、NIO、AIO三种IO模型。

IO模型的基本说明#

image-20200324063448217

BIO模型图#

image-20200324064030007

缺点:

  1. 如果有很多个Client,则会产生很多个线程。压力主要是在服务器端。客户端的压力并不大。

  2. 另外建立连接之后,并不是在时时刻刻的使用。会有空间时间。

  3. 会阻塞。

NIO模型图#

image-20200324064940368

特点:

  1. 事件驱动
  2. 多路复用
  3. Netty底层使用的NIO模型

AIO模型#

目前还未得到广泛运用。异步非阻塞。先了解就可以。

BIO、NIO、AIO使用场景分析#

  1. BOI方式使用与连接数目比较小固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中。JDK1.4以前的唯一选择。但是程序简单容易理解。
  2. NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务期间通讯等。编程比较复杂,JDK1.4开始支持。
  3. AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS操作系统参与并发操作,编程比较复杂,JDK7开始支持。但是目前还未得到广泛运用。

JAVA BIO编程#

JAVA BIO 基本介绍#

image-20200324070601084

JAVA BIO 工作机制#

image-20200324070450273

JAVA BIO 应用实例#

image-20200324070636449

Copy
package com.dawa.netty.bio;

import com.sun.org.apache.xpath.internal.operations.String;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 功能需求
 * 1. 使用BIO模型编写一个服务器,监听6666端口,当有客户连接的时候,就启动一个客户端线程与之连接
 * 2. 要求使用县城连接机制,可以连接过个客户端
 * 3. 服务器端可以接受客户端发送的数据(TeInet方法即可)
 */
public class TestBIO {
    public static void main(String[] args) throws Exception {
        //1. 创建一个线程池. 这里 借助 Executors 这个工具类
        ExecutorService pool = Executors.newCachedThreadPool();
        //2. 建立一个监听服务,用来监听客户端连接
        ServerSocket serverSocket = new ServerSocket(6666);

        while (true) {
            final Socket socket = serverSocket.accept();
            System.out.println("一个客户端连接");
            //就创建一个线程与之通信
            pool.execute(new Runnable() {
                public void run() {
                    //编写一个处理方法.
                    handler(socket);
                }
            });
        }
    }

    public static void handler(Socket socket) {
        byte[] bytes = new byte[1024];

        try (InputStream inputStream = socket.getInputStream()) {
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    //注意这里,不能用String转换了.因为String已经不支持有参数的构造方法.
                    System.out.println(Arrays.toString(bytes));
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println("关闭连接");
        }

    }
}

JAVA BIO 问题分析#

image-20200324073218636

JAVA NIO编程#

NIO 基本介绍#

image-20200324193714767

image-20200324195127910

image-20200324194859453

  1. NIO的三个核心,channel相当于IO的Socket
  2. Buffer,Channel,Selector(选择器)三大核心组件。
  3. 通过Buffer实现非阻塞,
  4. 面向缓冲区,或者面向块编程(Buffer就是这样的)。
  5. NIO是事件驱动的

NIO Buffer 基本使用#

image-20200324200742367

这没有BooleanBuffer,另外StringBuffer继承自StringBuilder.

一个简单的Buffer子类的使用案例如下

Copy
package com.dawa.netty.bio;

import java.nio.IntBuffer;

public class TestNIO {
    public static void main(String[] args) {
        IntBuffer intBuffer = IntBuffer.allocate(5);

        for (int i = 0; i < intBuffer.capacity(); i++) {
            System.out.println(intBuffer.put(i*5));
        }

        intBuffer.flip();

        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

NIO 和 BIO 的比较#

image-20200324201708534

NIO 三大核心原理示意图#

image-20200324201933663

Selector、Channel和Buffer的关系图

  1. 每个Channel都会对应一个Buffer。
  2. Selector对应一个线程,一个线程对应多个Channel(连接)。
  3. 该图反应了有三个Channel注册到该Selector
  4. 程序切换到哪个Channel是由事件决定的。Event是一个重要概念。
  5. Select会根据不同的事件,在各个通道上切换。
  6. Buffer就是一个内存块、底层是由一个数组
  7. 数据的读取写入是通过Buffer,这个和BIO、BIO中要么是输入流、或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip()方法来进行切换。
  8. Channel也是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。

三大核心——Buffer缓冲区详解#

Buffer的Doc源码(java11)#

 

 

 

基本介绍#

image-20200324204604333

Buffer的子类#

image-20200324200742367

容器对象(函数组),如何理解?从源码中可以看到。Int,Float等,每一个子类Buffer对象,都是[]数组。

image-20200324204830461

具有的四个子类#

Copy
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
  1. 缓冲区的容量是它所包含的元素数量。 缓冲区的容量从不为负,从来没有改变。

  2. 缓冲区的限制是不应读取或写入的第一个元素的索引。 缓冲区的限制是从不为负,并且永远不会比它更大的容量。

  3. 缓冲区的位置要被读出或写入的下一个元素的索引。 缓冲区的位置永远不会为负,并且永远不会比它的极限。

  4. Copy
    // Invariants: mark <= position <= limit <= capacity
    

代码跟踪-详解#

image-20200324205035963

image-20200324205428253

Buffer类及其子类中的重要的方法#

image-20200324210320538

如,通过设置position的值,来读取指定位置的值。也可以修改limit的值等。

ByteBuffer#

ByteBuffer,是最常用的。二进制数据。

image-20200324210425809

三大核心—— Channel 通道详解#

Channel接口的Doc源码(java11)#

image-20200324211700736

基本介绍#

image-20200324212539027

image-20200324212207511

Channel的子类#

image-20200324211833203

FileChannel类#

image-20200324212248256

Channel应用实例#

Channel应用实例1——本地文件写数据#

image-20200325052304319

实例代码如下:

Copy
package com.dawa.netty.nio;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class TestNIOFileChannel01 {
    public static void main(String[] args) throws Exception{

        //准备字符串
        String string = "dawa,大娃,Bigbaby";

        //准备输出流.指定输出的文件地址
        FileOutputStream fileOutputStream = new FileOutputStream("dawa.txt");

        //准备Channel管道. 对输出流进行封装,封装为一个channel管道.
        FileChannel fileChannel = fileOutputStream.getChannel();

        //准备一个byte数组, 也就是一个 Buffer数组,来缓存数据
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        
        //读取数据
        byteBuffer.put(string.getBytes());

      	//这里第一次没有反转,文件里面乱码
        byteBuffer.flip();
      
        //完成写的操作
        fileChannel.write(byteBuffer);

        //关闭流
        fileOutputStream.close();
    }
}

注意:

  1. 是FileOutPutStream 包含 NIO FileChannel
  2. FileChannel的具体的实现类是:FileChannelImpl

image-20200325052449120

Channel应用实例2——本地文件读数据#

image-20200325052837040

代码案例如下

Copy
package com.dawa.netty.nio;

import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

//本地 读文件
public class TestNIOFileChannel02 {
    public static void main(String[] args) throws Exception {

        //读到文件
        File file = new File("dawa.txt");
        FileInputStream fileInputStream = new FileInputStream(file);

        //fileInputStream 包装为 Channel
        FileChannel fileChannel = fileInputStream.getChannel();

        //借助Buffer byte[]缓冲数
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());

        //将Channel的数据读入到byteBuffer
        fileChannel.read(byteBuffer);

        System.out.println(new String(byteBuffer.array()));

        fileInputStream.close();

    }
}

Channel应用案例3——使用Buffer完成文件的读写#

类似于拷贝的操作,使用文件Channel+Buffer完成

image-20200325054011127

代码案例如下

Copy
package com.dawa.netty.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

//使用一个Channel完成文件的读写
public class TestNIOFileChannel03 {
    public static void main(String[] args)  throws  Exception{

        FileInputStream fileInputStream = new FileInputStream("dawa.txt");
        FileChannel channel01 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("erwa.txt");
        FileChannel channel02 = fileOutputStream.getChannel();

        //Buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while (true) {
            //这里注意使用Clear操作,不然会进入死循环
            /**
             * public Buffer clear() {
             *         position = 0;
             *         limit = capacity;
             *         mark = -1;
             *         return this;
             *     }
             */
            byteBuffer.clear();

            int read = channel01.read(byteBuffer);
            if (read == -1) {
                break;
            }
            //反转,切换流
            byteBuffer.flip();
            channel02.write(byteBuffer);
        }
						fileInputStream.close();
            fileOutputStream.close();
    }
}

这里需要注意的是使用clear操作,重置缓冲区基本参数

Copy
public Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

Channel应用案例4——Transferform拷贝#

transferFrom方法

Copy
    public long transferFrom(ReadableByteChannel src,
                             long position, long count)
        throws IOException
    {
        ensureOpen();
        if (!src.isOpen())
            throw new ClosedChannelException();
        if (!writable)
            throw new NonWritableChannelException();
        if ((position < 0) || (count < 0))
            throw new IllegalArgumentException();
        if (position > size())
            return 0;
        if (src instanceof FileChannelImpl)
           return transferFromFileChannel((FileChannelImpl)src,
                                          position, count);

        return transferFromArbitraryChannel(src, position, count);
    }

案例如下

Copy
package com.dawa.netty.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

//使用一个Channel完成文件的读写
public class TestNIOFileChannel03 {
    public static void main(String[] args)  throws  Exception{

        FileInputStream fileInputStream = new FileInputStream("dawa.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("erwa.txt");

        FileChannel sourceCH = fileInputStream.getChannel();
        FileChannel destCH = fileOutputStream.getChannel();

        //直接通过通道,完成拷贝
        destCH.transferFrom(sourceCH, 0, sourceCH.size());

        fileInputStream.close();
        fileOutputStream.close();

    }
}

关于Buffer和Channel的注意事项和注意细节#

image-20200325060447130

  1. 存取类型需要保持一致(存取顺序一致)

    image-20200325061118654

  2. Buffer可以转为只读Buffer

    Copy
    byteBuffer.asReadOnlyBuffer();
    
Copy
   ![image-20200325061059067](https://tva1.sinaimg.cn/large/00831rSTly1gd5rck2zicj31ja0tyh7e.jpg)

3. **MappedBuffer 可以让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次**

   ![image-20200325061421909](https://tva1.sinaimg.cn/large/00831rSTly1gd5rge5zuaj30oi0b4tad.jpg)

   MappedByteBuffer是抽象类,实际能够操作的类型是 DirectByteBuffer

   > 代码案例如下:

   ```java
   package com.dawa.netty.nio;
   
   import java.io.RandomAccessFile;
   import java.nio.MappedByteBuffer;
   import java.nio.channels.FileChannel;
   
   //使用 MappedBuffer 直接完成文件在内存中的数据修改
   public class MappedBuffer01 {
       public static void main(String[] args) throws Exception {
   
           //获取一个读取文件流
           RandomAccessFile randomAccessFile = new RandomAccessFile("dawa.txt","rw");
   
           //获取指定的Channel
           FileChannel channel = randomAccessFile.getChannel();
   
           //读取模式. 0 代表从0开始, 5代表读取5个字节,也同时意味着只能在内存中操作这5个字节
           MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
   
           //对指定位置进行操作
           mappedByteBuffer.put(0, (byte) 'A');
           mappedByteBuffer.put(2, (byte) 9);
   
           randomAccessFile.close();
           channel.close();
       }
   }
  1. Scattering&Gathering的使用

    Scattering:将数据写入到Buffer时,可以采用Buffer数组,依次写入【分散】

    Gathering:从Buffer读取数据时,可以采用Buffer数组,依次读【聚合】

    解决的问题:当一个数组不够用的时候,可以用数组组,来完成类似的操作

    代码案例如下:使用 数组,来完成客户端 - 服务器端 读取操作

    Copy
    package com.dawa.netty.nio;
    
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Arrays;
    
    public class ScatteringGatheringGetPut {
        public static void main(String[] args) throws Exception {
    
            //创建服务器端的
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
            //监听端口号
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 7000);
    
            //绑定端口号到服务器端的Channel
            serverSocketChannel.socket().bind(inetSocketAddress);
    
            //创建Buffer数组
            ByteBuffer[] byteBuffers = new ByteBuffer[2];
            byteBuffers[0] = ByteBuffer.allocate(5);
            byteBuffers[1] = ByteBuffer.allocate(3);
    
    
            // 等待连接,获取连接, 并生成客户端的 Channel
            SocketChannel socketChannel = serverSocketChannel.accept();
    
            //假设从 客户端读取 8个字节
            int messageLength = 8;
    
            while (true) {
                //1. 将客户端的数据, 读取
                int byteRead = 0;
                while (byteRead < messageLength) {
                    long l = socketChannel.read(byteBuffers);
                    System.out.println("byteRead = " + byteRead);
                    byteRead += 1;//累积读取的字节数
    
                    //使用流打印,看看当前Buffer里面的position和limit
                    Arrays.asList(byteBuffers).stream()
                            .map(byteBuffer -> "position=" + byteBuffer.position() + ", limit=" + byteBuffer.limit()).forEach(System.out::println);
                }
    
                //将所有的 Buffer反转
                Arrays.asList(byteBuffers).forEach(ByteBuffer::flip);
    
                //2. 将读取到的数据,写回客户端
                int byteWrite = 0;
                while (byteWrite < messageLength) {
                    socketChannel.write(byteBuffers);
                    byteWrite += 1;
                }
    
                //将所有的Buffer进行Clear操作
                Arrays.asList(byteBuffers).forEach(ByteBuffer::clear);
    
                //读完之后,打印出来看看读写文件的长度
                System.out.println("byteRead = " + byteRead + ", byteWrite = " + byteWrite + ", messageLength" + messageLength);
    
            }
    
        }
    }
    

三大核心—— Selector 选择器详解#

Selector的Doc源码(java11)#

 

 

 

 

基本介绍#

image-20200325075828311

  1. 示意图:

    image-20200325075740264

Select示意图和特点说明#

image-20200325075854846

Selector的子类#

image-20200325080430928

Selector类的相关方法#

image-20200325080845317

PS:一个线程,对应一个Selector,每个Selector通过调用select()方法,获取不同的能够代表Channel的SelectionKey,得到一个能够被选择的Channel集合。

注意事项#

image-20200327042610721

NIO非阻塞网络编程原理分析图#

NIO非阻塞网络相关的(Selector、SelectionKey、ServerSocketChannel和SocketChannel)关系图梳理。

image-20200327042415803

对上图的说明

  1. 当客户端生成时,会通过ServerSocketChannel得到SocketChannel。

  2. Selector开始监听...Selector进行监听select方法,返回有事件发生的通道的个数。

  3. 将SocketChannel注册到Selector上.register(Selector sel,int ops).一个Selector上可以注册多个SocketChannel。

    SocketChannel的父类里面有注册方法

    SelectableChannel里面还有一个注册方法,这个用的比较多

  4. 注册后,返回一个SelectionKey,会和该Selector关联(集合)

  5. 进一步得到各个SelectionKey(有事件发生)

  6. 再通过SelectionKey 反向获取SocketChannel。

    SelectionKey类中的channel()方法

  7. 通过得到的Channel,完成业务处理

NIO非阻塞网络编程快速入门#

Copy
创建服务器端。
  1. 创建ServerSocketChannel ,并设置非阻塞
  2. 得到一个Selector对象
  3. 绑定一个端口6666.在服务器端监听
  4. 把servrSocketChannel 注册到 selector  关心事件为 SelectionKey.OP_ACCEPT
  5. 循环等待客户端连接
  	//这里我们等待一秒,如果没有事件发生,返回
  	1. if(selector.selecct(1000)==0){//没有事件发生
      sout("服务器等待了一秒");
      continue;
    }

		//如果返回的值>0,就获取到相关的selectionKey集合
		// 1. 表示已经获取到关注的事件。
		// 2. 通过selectionKeys()返回关注的集合。
		// 3. 通过selectionKeys
		seletor.selectedKeys().var;

		//遍历得到的selectionKeys.
			//1. 获取SelectionKey
			//2. 根据key 对应的通道发生的事件做处理
			//3. 如果是 OP_ACCEPT,有新的客户端连接
					//1. 给该客户端生成一个SocketChannel
					//2. 将SocketChannel 注册到select,关注事件为OP_READ,并关联一个Buffer
			//4. 如果是 OP_READ,读取数据
					//1. 通过key,反向获取对应的channel	
					//2. 获取到该channel关联的buffer 
			//5. 手动从集合中移动单签的SelectionKey,防止重复操作。

创建客户端。
  1. 得到一个网络通道SocketChannel.并设置非阻塞
  2. 提供服务区的IP和端口,连接服务器

服务器端代码

Copy
package com.dawa.netty.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

// NIO 服务端
public class TestNIOServer {
    public static void main(String[] args) throws Exception {

        //  1. 创建ServerSocketChannel ,并设置非阻塞
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        //  2. 得到一个Selector对象
        Selector selector = Selector.open();
        //  3. 绑定一个端口6666.在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //  4. 把serverSocketChannel 注册到 selector  关心事件为 SelectionKey.OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 循环,等待客户端连接
        while (true) {
            if (selector.select(1000) == 0) {// 没有事件发生
                System.out.println("服务器端等待1秒,没有客户端连接");
                continue;
            }
            Set selectionKeys = selector.selectedKeys();//得到所有被选中的Key
            //循环遍历每一个key,每一个key代表一个事件
            Iterator iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //根据key对应的事件,做响应的处理
                if (selectionKey.isAcceptable()) {//如果是 Accept事件, 连接事件,则生成对应的客户端Channel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    //将SocketChannel 注册到select,关注事件为OP_READ,并关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (selectionKey.isReadable()) {//如果是读事件
                    //1. 通过key,反向生成Channel
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //设置非同步NIO
                    socketChannel.configureBlocking(false);
                    //2. 获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    socketChannel.read(buffer);
                    //打印出获取到的Buffer
                    System.out.println("from 客户端:" + new String(buffer.array()));
                }
                //这里一定要记得把处理过的key给移除掉,自己遇到了死循环.
                iterator.remove();
            }
        }
    }
}

客户端代码

Copy
package com.dawa.netty.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

//NIO 客户端
public class TestNIOClient {
    public static void main(String[] args) throws Exception {

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        //服务器端的IP和端口
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6666);

        if (!socketChannel.connect(socketAddress)) {
            System.out.println("连接失败,但是可以干其他事情,非阻塞");
            while (!socketChannel.finishConnect()) {
                System.out.println("在连接完成之前,我一直干其他的事情");
            }
        }

        String string = "hello,dawa";
        socketChannel.write(ByteBuffer.wrap(string.getBytes()));
        System.in.read();
    }
}

SelectionKey#

Selector.keys() 是 列出所有的key。

Selector.selectedKeys()是列出所有被选中的key。

这两个是不一样的。

image-20200327063028759

image-20200327064315095

ServerSocketChannel#

SocketChannel#

image-20200327064850861

NIO网络编程应用实例——群聊系统#

功能示意图

image-20200327065308315

  1. 先写服务器端
    1. 服务器端启动并监听6667
    2. 服务器端接收客户端消息,并实现转发[处理上线和离线]
  2. 编写客户端
    1. 连接服务器
    2. 发送消息
    3. 接受服务器消息
Copy

服务器端代码#

  1. 构造器初始化

image-20200327065929587

  1. 监听方法
    Listen()
    image-20200327070215028
    里面循环的写法:
    image-20200327070542182

里面读数据的方法:

image-20200327071122366

image-20200327071218197

try catch完成离线处理
image-20200327071738717

里面转发给其他客户端的方法

image-20200327071538520

客户端代码#

  1. 构造器初始化

    image-20200327072218159

  2. 向服务器发消息

    image-20200327072357745

  3. 读取从服务器端回复的消息

    image-20200327072652113

    image-20200327072740228

启动客户端和服务器端#

  1. 启动客户端的方法

    image-20200327073043538

    image-20200327073116852

  2. 启动服务器端的方法

    image-20200327073200172

自己编码:实现群发#

客户端代码

Copy
package com.dawa.netty.nio.group;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

//客户端
public class GroupCharClient {

    private SocketChannel socketChannel;
    private static final int PORT = 6667;
    private static final String HOST = "127.0.0.1";
    private Selector selector;
    private String userName;

    public GroupCharClient() throws IOException {
        selector = Selector.open();
        //连接服务器
        socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        userName = socketChannel.getRemoteAddress().toString().substring(1);
    }

    //发送消息
    public void sendMessage(String message){
        message = userName + "说" + message;
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
        try {
            socketChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读取服务器端发来的消息
    public void readMessage() {
        try {
            int readChannels = selector.select();
            if (readChannels > 0) {//有可用的通道
                Iterator iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        socketChannel.read(buffer);

                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
            } else {
                //没有可用的通道
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        GroupCharClient groupCharClient = new GroupCharClient();

        new Thread(() -> {
            while (true) {
                groupCharClient.readMessage();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            String message = scanner.nextLine();
            groupCharClient.sendMessage(message);
        }
    }

}

服务器端代码

Copy
package com.dawa.netty.nio.group;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * 服务器端代码
 */
public class GroupChatServer {
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private static final int PORT = 6666;

    public GroupChatServer() {
        try {
            //得到选择器
            selector = Selector.open();
            //绑定端口
            serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(PORT));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //服务器端监听的方法
    public void listen() {
        try {
            //循环监听
            while (true) {
                int count = selector.select();
                if (count > 0) {
                    Iterator selectionKeyIterator = selector.selectedKeys().iterator();
                    while (selectionKeyIterator.hasNext()) {
                        //取出Key
                        SelectionKey key = selectionKeyIterator.next();

                        //判断事件
                        if (key.isAcceptable()) {//监听访问
                            //key 转Channel
                            SocketChannel channel = serverSocketChannel.accept();
                            SocketAddress remoteAddress = channel.getRemoteAddress();
                            System.out.println(remoteAddress + ":上线了");
                        }
                        if (key.isReadable()) {//读取事件
                            //处理读
                            readData(key);
                        }
                        //移除已经处理的key
                        selectionKeyIterator.remove();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //读数据
    public void readData(SelectionKey key) {
        SocketChannel channel = null;
        try {
            //根据key,取得Channel
            channel = (SocketChannel) key.channel();
            channel.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = channel.read(buffer);
            if (read > 0) {
                String message = new String(buffer.array());
                System.out.println("from: 客户端" + message);

                // 向其他用户,转发消息
                sendMessageToOtherCLient(message,key);
            }

        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " :离线了");
                key.cancel();
                channel.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    //向其他用户转发消息
    public void sendMessageToOtherCLient(String message,SelectionKey self){
        System.out.println("服务器转发消息ing");

        selector.keys().forEach(key -> {
            //根据Key,取出对应的SocketChannel.或者是ServerSocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self) {
                //转型
                SocketChannel dest = (SocketChannel) targetChannel;
                //Buffer
                ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
                try {
                    dest.write(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

    }

    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

NIO与零拷贝#

什么是零拷贝?

零拷贝,指的是没有CPU拷贝,而不是不拷贝。从操作系统角度看的。

基本介绍#

image-20200330051538095

传统IO数据读写#

image-20200330051657640

传统IO模型图,状态切换:用户态和内核态的切换:4次拷贝,3次切换

MMAP优化#

image-20200330052042194

Mmap优化:3次拷贝,3次切换

DMA拷贝:direct memory accect:直接内存访问

sendFile优化#

image-20200330052351584

sendFile优化:三次拷贝,两次切换

零拷贝#

零拷贝,指的是没有CPU拷贝,而不是不拷贝。从操作系统角度看的。

image-20200330052437619

这里其实还有一次CPU拷贝的:kernel buffer->socket buffer但是,拷贝的信息很少,比如length,offset,消耗低,可以忽略

零拷贝:2次拷贝,2次切换。

image-20200330052906579

零拷贝是我们在进行网络传输的重要优化手段。

mmap和sendFile的区别:#

image-20200330052955634

NIO零拷贝案例#

image-20200330053238276

传统IO流案例

image-20200330053408571

image-20200330053600538

传统IO耗费时间:60毫秒

零拷贝案例。(NIO)

transferTo 底层用的就是零拷贝

image-20200330055530665

image-20200330055035445

NIO零拷贝耗时时间:20毫秒

Java AIO编程#

Java AIO基本介绍#

image-20200330055721122

这里暂时不深入扩展。

BIO、NIO、AIO对比表#

image-20200330055912676

 

Java-NIO(四):通道(Channel)的原理与获取

http://www.mamicode.com/info-detail-1933220.html

  • 通道(Channel):

  由java.nio.channels包定义的,Channel表示IO源与目标打开的连接,Channel类似于传统的“流”,只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。通道主要用于传输数据,从缓冲区的一侧传到另一侧的实体(如文件、套接字...),反之亦然;通道是访问IO服务的导管,通过通道,我们可以以最小的开销来访问操作系统的I/O服务;顺便说下,缓冲区是通道内部发送数据和接收数据的端点。

  在标准的IO当中,都是基于字节流/字符流进行操作的,而在NIO中则是是基于Channel和Buffer进行操作,其中的Channel的虽然模拟了流的概念,实则大不相同。

区别StreamChannel
支持异步 不支持 支持
是否可双向传输数据 不能,只能单向 可以,既可以从通道读取数据,也可以向通道写入数据
是否结合Buffer使用 必须结合Buffer使用
性能 较低 较高

早一代IO操作是由CPU负责IO接口:

技术分享

新一代DMA授权处理IO接口:

技术分享

通道(Channel)模式:

技术分享

  • 通道的获取方法:

java.nio.channels.Channel接口:

  |--FileChannel

  |--SocketChannel

  |--ServerSocketChannel

  |--DatagramChannel

获取通道的方法

1)Java针对支持通道的类提供了getChannel()方法

本地IO:

|--FileInputStream/FileOutputStream

|--RandomAccessFile

网络IO:

|--Socket

|--ServerSocket

|--DatagramSocket

|--Pip.***

2)在JDK1.7中的NIO.2针对各个通过提供了静态方法open()

3)在JDK1.7中的NIO.2的Files工具类的newByteChannel()

4)Channles工具类中提供了静态方法newChannel()。

 

 

 

 

 

 

一、七层网络模型

  OSI是Open System Interconnection的缩写,意为开放式系统互联。国际标准化组织(ISO)制定了OSI模型,该模型定义了不同计算机互联的标准,它是一个七层的、抽象的模型体。

      

  1、物理层

  并不是物理媒体本身,它只是开放系统中利用物理媒体实现物理连接的功能描述和执行连接的规程,建立、维护、断开物理连接,传输单位是比特(bit)。

  物理层的媒体包括架空明线、平衡电缆、光纤、无线信道等。通信用的互连设备指DTE(Data Terminal Equipment)和DCE(Data Communications Equipment)间的互连设备。DTE即数据终端设备,又称物理设备,如计算机、终端等都包括在内。而DCE则是数据通信设备或电路连接设备,如调制解调器等。数据传输通常是经过DTE-DCE,再经过DCE-DTE的路径。互连设备指将DTE、DCE连接起来的装置,如各种插头、插座。LAN中的各种粗、细同轴电缆、T型接头、插头、接收器、发送器、中继器等都属物理层的媒体和连接器。

  物理层的主要功能是:

  ① 为数据端设备提供传送数据的通路,数据通路可以是一个物理媒体,也可以是多个物理媒体连接而成。一次完整的数据传输,包括激活物理连接、传送数据和终止物理连接。所谓激活,就是不管有多少物理媒体参与,都要在通信的两个数据终端设备间连接起来,形成一条通路。

  ② 传输数据。物理层要形成适合数据传输需要的实体,为数据传送服务。一是要保证数据能在其上正确通过,二是要提供足够的带宽(带宽是指每秒钟内能通过的比特(Bit)数),以减少信道上的拥塞。传输数据的方式能满足点到点,一点到多点,串行或并行,半双工或全双工,同步或异步传输的需要。

  2、数据链路层

  可以粗略地理解为数据通道,传输单位是帧(Frame)。物理层要为终端设备间的数据通信提供传输介质及其连接。介质是长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。每次通信都要经过建立通信联络和拆除通信联络两个过程。这种建立起来的数据收发关系就叫做数据链路。而在物理媒体上传输的数据难免受到各种不可靠因素的影响而产生差错,为了弥补物理层上的不足,为上层提供无差错的数据传输,就要能对数据进行检错和纠错。链路层应具备如下功能:

  ① 链路连接的建立、拆除和分离;

  ② 差错检测和恢复。还有链路标识,流量控制等等。

  独立的链路产品中最常见的当属网卡、网桥、二路交换机等。

  3、网络层

  在网络层: 有IP (IPV4、IPV6)协议、ICMP协议、ARP协议、RARP协议和BOOTP协议,负责建立“主机”到“主机”的通讯,传输单位是分组(数据包Packet)。

  当数据终端增多时。它们之间有中继设备相连,此时会出现一台终端要求不只是与惟一的一台而是能和多台终端通信的情况,这就产生了把任意两台数据终端设备的数据链接起来的问题,也就是路由或者叫寻径。另外,当一条物理信道建立之后,被一对用户使用,往往有许多空闲时间被浪费掉。人们自然会希望让多对用户共用一条链路,为解决这一问题就出现了逻辑信道技术和虚拟电路技术。

  4、传输层

  在传输层: 有TCP协议与UDP协议,负责建立“端口”到“端口”的通信,传输单位是数据段(Segment)。 

  有一个既存事实,即世界上各种通信子网在性能上存在着很大差异。例如电话交换网,分组交换网,公用数据交换网,局域网等通信子网都可互连,但它们提供的吞吐量,传输速率,数据延迟通信费用各不相同。对于会话层来说,却要求有一性能恒定的界面。传输层就承担了这一功能。

  5、会话层

  会话单位的控制层,其主要功能是按照在应用进程之间约定的原则,按照正确的顺序收、发数据,进行各种形态的对话。会话层规定了会话服务用户间会话连接的建立和拆除规程以及数据传送规程。

  会话层提供的服务是应用建立和维持会话,并能使会话获得同步。会话层使用校验点可使通信会话在通信失效时从校验点继续恢复通信。这种能力对于传送大的文件极为重要。

  6、表示层

  其主要功能是把应用层提供的信息变换为能够共同理解的形式,提供字符代码、数据格式、控制信息格式、加密等的统一表示。表示层的作用之一是为异种机通信提供一种公共语言,以便能进行互操作。这种类型的服务之所以需要,是因为不同的计算机体系结构使用的数据表示法不同。例如,IBM主机使用EBCDIC编码,而大部分PC机使用的是ASCII码。在这种情况下,便需要表示层来完成这种转换。

  7、应用层

  向应用程序提供服务,这些服务按其向应用程序提供的特性分成组,并称为服务元素。有些可为多种应用程序共同使用,有些则为较少的一类应用程序使用。应用层是开放系统的最高层,是直接为应用进程提供服务的。其作用是在实现多个系统应用进程相互通信的同时,完成一系列业务处理所需的服务。

  在应用层: 有FTP、HTTP、TELNET、SMTP、DNS等协议。

二、七层网络模型传输过程

            

            

  TCP/IP中的数据包传输过程如下:

  每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。

  网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。

  ① 应用程序处理

  首先应用程序会进行编码处理,这些编码相当于 OSI 的表示层功能;编码转化后,邮件不一定马上被发送出去,这种何时建立通信连接何时发送数据的管理功能,相当于 OSI 的会话层功能。

  ② TCP 模块的处理

  TCP 根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要在应用层数据的前端附加一个 TCP 首部。

  ③ IP 模块的处理

  IP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。IP 包生成后,参考路由控制表决定接受此 IP 包的路由或主机。

  ④ 网络接口(以太网驱动)的处理

  从 IP 传过来的 IP 包对于以太网来说就是数据。给这些数据附加上以太网首部并进行发送处理,生成的以太网数据包将通过物理层传输给接收端。

  ⑤ 网络接口(以太网驱动)的处理

  主机收到以太网包后,首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包,若不是则丢弃数据。

  如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如 IP、ARP 等。这里的例子则是 IP 。

  ⑥ IP 模块的处理

  IP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP。
  另外,对于有路由器的情况,接收端地址往往不是自己的地址,此时,需要借助路由控制表,在调查应该送往的主机或路由器之后再进行转发数据。

  ⑦ TCP 模块的处理

  在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。

  ⑧ 应用程序的处理

  接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。

  传输过程中协议如下:

    

 三、什么是SOCKET 

  Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

  Socket 接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,用以开发TCP/IP网络上的应用程序。

  Socket为了实现以上的通信过程而建立成来的通信管道,其真实的代表是客户端和服务器端的一个通信进程,双方进程通过socket进行通信,而通信的规则采用指定的协议。socket只是一种连接模式,不是协议,tcp,udp,简单的说(虽然不准确)是两个最基本的协议,很多其它协议都是基于这两个协议如,http就是基于tcp的,用socket可以创建tcp连接,也可以创建udp连接,这意味着,用socket可以创建任何协议的连接,因为其它协议都是基于此的。

  综上所述:需要IP协议来连接网络;TCP是一种允许我们安全传输数据的机制,使用TCP协议来传输数据的HTTP是Web服务器和客户端使用的特殊协议。HTTP基于TCP协议,但是却可以使用socket去建立一个TCP连接。

  如图:

          

四、长短连接

  短连接:连接->传输数据->关闭连接

  也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

  长连接:连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。

  长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

  什么时候用长连接,短连接?

  长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

  而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。
总之,长连接和短连接的选择要视情况而定。

五、三次握手四次分手

        

  SYN,ACK,FIN存放在TCP的标志位,一共有6个字符,这里就介绍这三个:

SYN:代表请求创建连接,所以在三次握手中前两次要SYN=1,表示这两次用于建立连接,至于第三次什么用,在疑问三里解答。

FIN:表示请求关闭连接,在四次分手时,我们发现FIN发了两遍。这是因为TCP的连接是双向的,所以一次FIN只能关闭一个方向。

ACK:代表确认接受,从上面可以发现,不管是三次握手还是四次分手,在回应的时候都会加上ACK=1,表示消息接收到了,并且在建立连接以后的发送数据时,都需加上ACK=1,来表示数据接收成功。

seq: 序列号,什么意思呢?当发送一个数据时,数据是被拆成多个数据包来发送,序列号就是对每个数据包进行编号,这样接受方才能对数据包进行再次拼接。初始序列号是随机生成的,这样不一样的数据拆包解包就不会连接错了。(例如:两个数据都被拆成1,2,3和一个数据是1,2,3一个是101,102,103,很明显后者不会连接错误)

ack: 这个代表下一个数据包的编号,这也就是为什么第二请求时,ack是seq+1

   TCP是双向的,所以需要在两个方向分别关闭,每个方向的关闭又需要请求和确认,所以一共就4次分手。

六、文件描述符

  在UNIX、Linux的系统调用中,内核系统把应用程序可以操作的资源都抽象成了文件概念,比如说硬件设备,socket,流,磁盘,进程,线程;文件描述符就是索引(指针)。

  文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行I/O操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。程序刚刚启动时,第一个打开的文件是0,第二个是1,以此类推。也可以理解为文件的身份ID。如:

         

  标准输入输出说明

  stdin,标准输入,默认设备是键盘,文件编号为0

  stdout,标准输出,默认设备是显示器,文件编号为1,也可以重定向到文件

  stderr,标准错误,默认设备是显示器,文件编号为2,也可以重定向到文件

  /proc/[进程ID]/fd  这个目录专门用于存放文件描述符,可以到目录下查看文件描述符使用情况,同时也可以通过ulimit查看文件描述符限制,如:

192:~ XXX$ ulimit -n  //-n打开文件描述符的最大个数
256
192:~ XXX$ ulimit -Sn  //-S是软性限额
256
192:~ XXX$ ulimit -Hn  //-H是硬性限额
unlimited

  Linux中最大文件描述符的限制有两个方面,一个是用户级限制,一个是系统级限制,文件描述符限制均可进行修改,但是也有一个限制,规则如下:

  a.  所有进程打开的文件描述符数不能超过/proc/sys/fs/file-max

  b.  单个进程打开的文件描述符数不能超过user limit中nofile的soft limit

  c.  nofile的soft limit不能超过其hard limit

  d.  nofile的hard limit不能超过/proc/sys/fs/nr_open

七、零拷贝

  应用程序获取数据的两个阶段:

  数据准备:应用程序无法直接操作我们的硬件资源,需要操作系统资源时,先通知我们的内核,内核检查是否有就绪的资源,如果有则先把对应数据加载到内核空间。

  数据拷贝:把数据资源从内核空间复制到应用程序的用户空间。

  补充知识 -> 零拷贝

  现代操作系统都使用虚拟内存,使用虚拟的地址取代物理地址,这样做的好处是:

  1.一个以上的虚拟地址可以指向同一个物理内存地址,

  2.虚拟内存空间可大于实际可用的物理地址;

  利用第一条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址,这样DMA就可以填充对内核和用户空间进程同时可见的缓冲区了,大致如下图所示:

            

  关于mmap以及sendfile零拷贝,可以参考:如何实现高性能的IO及其原理? 

八、Linux 网络IO模型

  什么是同步和异步,阻塞和非阻塞?

  同步和异步关注的是结果消息的通信机制

  同步:同步的意思就是调用方需要主动等待结果的返回

  异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。

  阻塞和非阻塞主要关注的是等待结果返回时调用方的状态

  阻塞:是指结果返回之前,当前线程被挂起,不做任何事

  非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。

  Linux有5种IO模型,如下图所示:

        

  1、阻塞I/O模型

  应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。

  当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。

            

  2、非阻塞IO模型

  我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。上述模型绝不被推荐。

  把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。如图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。

            

  3、IO复用模型

  简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;

  I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

  当用户进程调用了select,那么整个进程会被block;而同时,kernel会“监视”所有select负责的socket;当任何一个socket中的数据准备好了,select就会返回。这个时候,用户进程再调用read操作,将数据从kernel拷贝到用户进程。

  这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

           

  在这种模型中,这时候并不是进程直接发起资源请求的系统调用去请求资源,进程不会被“全程阻塞”,进程是调用select或poll函数。进程不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll帮助用户进程去轮询那些IO操作是否完成。

  不过你可以看到之前都只使用一个系统调用,在IO复用中反而是用了两个系统调用,但是使用IO复用你就可以等待多个描述符也就是通过单进程单线程实现并发处理,同时还可以兼顾处理套接字描述符和其他描述符。

   4、信号驱动IO 

  简介:两次调用,两次返回;

  首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

           

  5、异步IO模型

  当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。

  在linux的异步IO模型中,并没有真正实现异步通道,最终的实现还是等同于调用Epoll。

           

   LInux IO模型总结如图所示:

          

 九、多路复用IO原理详解

  在linux 没有实现epoll事件驱动机制之前,我们一般选择用select或者poll等IO多路复用的方法来实现并发服务程序。但在大数据、高并发、集群出现后,select和poll的性能瓶颈无法在支撑,于是epoll出现了。

  1、select

  首先来说说select,select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fd_set,来找到就绪的描述符。

int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);

  具体select步骤如图所示:

            

  1. 使用copy_from_user从用户空间拷贝fd_set到内核空间。
  2. 注册回调函数__pollwait。
  3. 遍历所有FD,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll, udp_poll或者datagram_poll)
  4. 以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
  5. __pollwait的主要工作就是把当前进程挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络IO)或填写完文件数据(磁盘IO)后,会唤醒设备等待队列上睡眠的进程,这时当前进程便被唤醒了。
  6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  7. 如果遍历完所有的FD,还没有返回一个可读写的mask掩码,则会调用schedule_timeout让调用select的当前进程进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过设定的超时时间,还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历FD,判断有没有就绪的FD。
  8. 把fd_set从内核空间拷贝到用户空间。
  9. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

  注意:select的实现依赖于文件的驱动函数poll,在unix中无论是调用 select、poll 还是epoll,最终都会调用该函数。

  2、poll

int poll(struct pollfd fds[], nfds_t nfds, int timeout);

  不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

  和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

  3、epoll

  在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

  epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?红黑树)。epoll提供了三个函数,epoll_create, epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};
// events可以是以下几个宏的集合:
// EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
// EPOLLOUT:表示对应的文件描述符可以写;
// EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
// EPOLLERR:表示对应的文件描述符发生错误;
// EPOLLHUP:表示对应的文件描述符被挂断;
// EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
// EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

  一棵红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。

  ① 执行 epoll_create
    内核在epoll文件系统中建了个file结点,(使用完,必须调用close()关闭,否则导致fd被耗尽)
       在内核cache里建了红黑树存储epoll_ctl传来的socket,
       在内核cache里建了rdllist双向链表存储准备就绪的事件。
  ② 执行 epoll_ctl
    如果增加socket句柄,检查红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,告诉内核如果这个句柄的中断到了,就把它放到准备就绪list链表里。所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,相应的事件发生时,会调用回调方法。

  ③ 执行 epoll_wait

    立刻返回准备就绪表里的数据即可(将内核cache里双向列表中存储的准备就绪的事件  复制到用户态内存),当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。 

        

   对于select的三个缺点以及epoll的解决方案:

  (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。

  (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

  (3)select支持的文件描述符数量太小了,默认是1024。

  对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

  4、select、poll、epoll优缺点对比

        

          

  5、epoll 的水平触发与边缘触发

  Level_triggered(水平触发):

  当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

  Edge_triggered(边缘触发):

  当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!

  select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

Stay hungry,stay foolish !

 

posted @ 2021-10-01 13:47  CharyGao  阅读(563)  评论(0编辑  收藏  举报