Java NIO学习笔记

目录

〇、前言

一、简介

二、缓冲区(Buffer)和通道(Channel)

缓冲区

通道

三、分散读取与聚集写入

四、字符集Charset

五、阻塞与非阻塞

1、简单的客户端/服务端通信:实现文件的传输

2、客户端/服务端通信阻塞的情况:服务端等待客户端继续发送,客户端等待服务端的反馈。

3、非阻塞式IO的实现——选择器(Selector)

阶段总结

六、总结


〇、前言

这篇文章记录了我在 NIO 学习过程中的一些笔记,附上配套视频和代码,不看视频只看文章也能够对 NIO 有一个很好的认识。希望能给正在初步接触 NIO 的你带来帮助,不喜勿喷!

视频:链接:https://pan.baidu.com/s/1HgYESmnrur9yZibFVK525A  提取码:hl82

代码:链接:https://pan.baidu.com/s/1uh6toSkdqbSq87WdSxtkEg  提取码:l1dr

一、简介

Java NIO(New IO)是从JDK1.4开始引入的新的Java I/O类库,NIO 和原来的 IO 有同样的作用和目的,但是实现的方式完全不同,NIO 支持面向缓冲区的、基于通道的 IO 操作,NIO 将以更高效的方式进行文件的读写操作。 实际上,旧的 I/O 包已经使用 NIO 重新实现过,以便充分利用这种速度,因此,即使我们不显式地使用 NIO 编写代码,也能从中受益。NIO 速度的提高来自于所使用的结构更接近于操作系统执行 I/O 的方式:通道和缓冲器。

Java NIO 和 IO 的比较
IONIO
面向流(Stream Oriented)面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO)非阻塞IO(Non Blocking IO)
 选择器(Selectors)

① 什么是阻塞和非阻塞?

  • 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
  • Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

② 什么是同步和异步?

  • 同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;
  • 异步则与同步相反,后续的任务不用等待当前调用返回,通常依靠事件和回调机制来实现任务间的次序关系。

注: 很多情况下,同步和阻塞、异步和非阻塞可以看做是相等的,但在网络IO编程中是区分的,不同在于同步异步关注的是任务,阻塞和非阻塞关注的是线程。

二、缓冲区(Buffer)和通道(Channel)

 Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。

" 我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送到矿藏的卡车。卡车满载煤炭而归,我们再从卡车上获得煤矿。也就是说,我们并没有直接和通道交互;我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。"

                                                                                                                                   ——《Java编程思想》P552

 简而言之,Channel 负责数据的传输,Buffer 负责数据的存储。

缓冲区

缓冲区(Buffer):一个用于特定基本数据类型的容器。由java.io包定义,所有缓冲区都是Buffer抽象类的子类。

关于Buffer四个基本属性的测试:

/*
 * 一、缓冲区(Buffer):在 Java NIO 中负责数据的存取,缓冲区底层是数组,数组用于存储不同数据类型的数据.
 * 即根据数据类型不同(boolean除外),有不同类型的缓冲区
 * ByteBuffer(最常用,网络、磁盘,只有ByteBuffer支持直接缓冲区)
 * ShortBuffer
 * CharBuffer
 * IntBuffer
 * LongBuffer
 * FloatBuffer
 * DoubleBuffer
 * 上述缓冲区的管理方式几乎一致,通过 allocate()获取缓冲区
 * 
 * 二、缓冲区用于存取数据的两个核心方法
 * put():存入数据到缓冲区中
 * get():获取缓冲区中的数据
 * 
 * 要想对缓冲区中的数据进行正确的存取,必须要了解的几个缓冲区的属性
 * 三、缓冲区中的四个核心属性
 * ① capaticy : 容量, 表示缓冲区中最大存储数据的容量,一旦声明不能改变(联想,缓冲区底层就是数组)。
 * ② limit : 界限, 表示缓冲区中可以操作的数据的大小(超过limit大小后的数据是不能进行读写的)
 * ③ position : 位置, 表示缓冲区中下一个将要操作的数据的位置(索引从0开始)
 * ④ mark : 标记, 用于记录当前 position 的位置.可以通过 reset() 使 position 恢复到 mark 最后一次记录的位置
 * 
 * 打开源码,规定 : mark <= position <= limit <= capacity
 * 
 */
class TestBuffer {
	public static void printBufferState(ByteBuffer buf,String op) {
		System.out.println("----------------------"+ op);
		System.out.println("position:" + buf.position());
		System.out.println("limit:" + buf.limit());
		System.out.println("capacity:" + buf.capacity());
	}
	
	/**
	 * 测试 position limit capacity 3个属性
	 */
	@Test
	void test01() {
		String str = "abcde";
		//1.通过 allocate()方法获取一个指定大小的缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		printBufferState(buf,"allocate()");
		/* output:
		   ----------------------allocate()
			position:0
			limit:1024
			capacity:1024
		 */
		
		//2. put()向缓冲区中放入 str(写数据模式)
		buf.put(str.getBytes());
		printBufferState(buf,"put()");
		/* output:
		   ----------------------put()
			position:5
			limit:1024
			capacity:1024
		 */
		
		//3. flip()切换到读数据模式
		buf.flip();
		printBufferState(buf,"flip()");
		/* output:
		  ----------------------flip()
			position:0
			limit:5
			capacity:1024
			abcde
		 */
		
		//4. get()读取缓冲区中的数据
		byte[] dst = new byte[buf.limit()];
		buf.get(dst);
		System.out.println(new String(dst, 0, dst.length));
		printBufferState(buf,"get()");
		/* output:
		  ----------------------get()
			position:5
			limit:5
			capacity:1024
		 */
		
		//5. rewind() 实现可重复读,四个属性恢复到get()方法之前状态
		buf.rewind();
		printBufferState(buf,"rewind()");
		/* output:
		  ----------------------rewind()
			position:0
			limit:5
			capacity:1024
		 */
		
		//6. clear() 清空缓冲区,但是缓冲区中的数据依然存在,但是这些数据处于“被遗忘”的状态,
		//即无法通过limit了解到缓冲区中数据的大小
		buf.clear();
		printBufferState(buf,"clear()");
		//测试执行完 clear() 之后,缓冲区中有无数据
		System.out.println((char)buf.get());
		/* output:
		 ----------------------clear()
			position:0
			limit:1024
			capacity:1024
			0
		 */
	}

        /**
	 * mark属性
	 */
	@Test
	void test02() {
		String str = "abcde";
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		buf.put(str.getBytes());
		
		buf.flip();//读模式
		
		byte[] dst = new byte[buf.limit()];
		buf.get(dst, 0, 2);
		System.out.println(new String(dst, 0, 2));
		/* output:ab */
		System.out.println(buf.position());
		/* output:2 */
		//mark() 标记, 记录 position 的位置
		buf.mark();
		
		//继续读
		buf.get(dst, 2, 2);
		System.out.println(new String(dst, 2, 2));/* output:cd */
		System.out.println(buf.position());/* output:4 */
		//重置
		buf.reset();
		System.out.println("reset:" + buf.position());/* output:reset:2 */
		//hasRemaining()看看缓冲区中还有没有剩余的数据,position之后的剩余数据
		if(buf.hasRemaining()) {
			System.out.println(buf.remaining());/* output:3 */
		}
	}

}

通道

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

回顾:》》操作系统中的几种I/O控制方式

通道的主要实现类

获取通道的几种方式

用1、2两种方式进行文件复制(文件:视频文件,203510KB)

//1.利用通道完成文件的复制(非直接缓冲区)
	@Test
	void test01() {
		/*耗费时间:6985*/
		long start = System.currentTimeMillis();
		FileInputStream fis = null;
		FileOutputStream fos = null;
		//① 获取通道
		FileChannel inChannel = null;
		FileChannel outChannel = null;
		try {
			fis = new FileInputStream("d:/1.avi");
			fos = new FileOutputStream("d:/2.avi");
			
			inChannel = fis.getChannel();
			outChannel = fos.getChannel();
			
			//② 分配指定大小的缓冲区
			ByteBuffer buf = ByteBuffer.allocate(1024);
			
			//③ 将通道中的数据存入缓冲区中
			while(inChannel.read(buf) != -1) {
				buf.flip();//切换成读数据模式
				//④ 将缓冲区中的数据写入通道
				outChannel.write(buf);
				buf.clear();//清空缓冲区
			}
		} catch (IOException e) {
			e.printStackTrace();
		}finally {
			if(outChannel != null) {
				try {
					outChannel.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if(inChannel != null) {
				try {
					inChannel.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if(fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}				
			}
			if(fis != null) {
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("耗费时间:" + (end - start));
	}
	
	//2. 使用直接缓冲区完成文件的复制(内存映射文件)
	@Test
	void test02() throws IOException {
		/*耗费时间:708*/
		long start = System.currentTimeMillis();
		FileChannel inChannel = FileChannel.open(Paths.get("d:/1.avi"), StandardOpenOption.READ);
		//StandardOpenOption.CREATE : 如果文件存在就覆盖,不存在就覆盖;
		//StandardOpenOption.CREATE_NEW : 如果文件存在就报错,不存在就报错;
		FileChannel outChannel = FileChannel.open(Paths.get("d:/3.avi"),
				StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
		
		//内存映射文件(现在的缓冲区在物理内存中)
		MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
		MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
		
		//直接对缓冲区进行读写操作
		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));
	}

为了便于大家理解代码,在后续的代码中凡遇到异常一律抛出,但是在实际生产应用中应该做try catch处理

方式二通过 FileChannel 的 map() 方法将文件区域直接映射到内存中来创建。该方式同通过allocateDirect()工厂方法来创建原理是相同的,缓冲区都是直接建立在物理内存中。

使用直接缓冲区进行文件复制时问题:文件复制完,但是程序并没有终止,CPU占用率较使用非直接缓冲区显著提高。

这是因为垃圾收集机制未能及时释放引用所指向的资源所致。所以建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区(因为是直接缓冲区是在物理内存中,所以不会对应用程序的内存需求造成影响)

简便的文件复制的写法(原理也是直接缓冲区)

//通道之间的数据传输(直接缓冲区)
@Test
void test3() throws IOException {
	FileChannel inChannel = FileChannel.open(Paths.get("d:/1.avi"), StandardOpenOption.READ);
	FileChannel outChannel = FileChannel.open(Paths.get("d:/3.avi"),
			StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
	
	inChannel.transferTo(0, inChannel.size(), outChannel);
	//outChannel.transferFrom(inChannel, 0, inChannel.size());//或者这么写
}

三、分散读取与聚集写入

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

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

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

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

/*
 * 五、分散(Scatter)和聚集(Gather)
 * 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
 * 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
 * 对比之前的文件复制区别在于,原来得操作对象是缓冲区,现在的操作对象是字节数组
 */
@Test
void test01() throws IOException {
	RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");

	//1. 获取通道
	FileChannel channel1 = raf1.getChannel();
	
	//2. 分配指定大小的缓冲区
	ByteBuffer buf0 = ByteBuffer.allocate(1024);
	ByteBuffer buf1 = ByteBuffer.allocate(1024);
	
	//3. 分散读取
	ByteBuffer[] bufs = {buf0,buf1};
	channel1.read(bufs);
	System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
	System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
	
	//4. 聚集写入
	RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
	FileChannel channel2 = raf2.getChannel();
	
	for(ByteBuffer bb : bufs) {
		bb.flip(); //只有将缓冲区都切换成读模式,才能保证数据能被复制
	}
	
	channel2.write(bufs);
}

四、字符集Charset

/*
 * 六、字符集
 * 编码:字符串 ——> 字节数组
 * 解码:字节数组 ——> 字符串
 * 编码解码实质就是 ByteBuffer 和 CharBuffer 之间的转换
 */
@Test
void test02() throws IOException {
	Charset cs1 = Charset.forName("GBK");
	//获取编码器和解码器
	CharsetEncoder ce = cs1.newEncoder();
	CharsetDecoder cd = cs1.newDecoder();
	
	CharBuffer cBuf = CharBuffer.allocate(1024);
	cBuf.put("好好学习!");

	cBuf.flip();//编码肯定要进行读取,因此flip()转换为读模式
	//编码
	ByteBuffer bBuf = ce.encode(cBuf);
	//输出编码后结果
	for(int i = 0; i < bBuf.limit(); i++) {
		System.out.print(bBuf.get() + " ");
	}
	/*output:-70 -61 -70 -61 -47 -89 -49 -80 33 */
	System.out.println();
	
	//解码
	bBuf.flip();
	CharBuffer cBuf2 = cd.decode(bBuf);
	System.out.println(cBuf2.toString());/*output:好好学习!*/
	//__________________________________获取乱码
	Charset cs2 = Charset.forName("UTF-8");
	bBuf.flip();
	CharBuffer cBuf3 = cs2.decode(bBuf);
	System.out.println(cBuf3.toString());/*output:?ú???!*/
}

五、阻塞与非阻塞

  • 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
  • Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

阻塞与非阻塞是针对网络 I/O 而言的,FileChannel不能切换成非阻塞模式。

使用NIO完成网络通信的三个核心要点


 

1、简单的客户端/服务端通信:实现文件的传输

public class TestBlockingNIO {
	//客户端
	@Test
	void client() throws IOException {
		//1. 获取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		//2. 分配指定大小的缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//3. 读取本地文件,并发送到服务端
		FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
		while(inChannel.read(buf) != -1) {
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		inChannel.close();
		sChannel.close();
	}
	
	//服务端
	@Test
	void server() throws IOException {
		//1. 获取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		//2. 绑定连接端口号
		ssChannel.bind(new InetSocketAddress(9898));
		
		//3. 获取客户端连接的通道
		SocketChannel sChannel = ssChannel.accept();
		
		//4. 分配指定大小的缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//5. 接收客户端的数据,并保存到本地
		FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,
				StandardOpenOption.CREATE);
		while(sChannel.read(buf) != -1) {
			buf.flip();
			outChannel.write(buf);
			buf.clear();
		}
		
		//6. 关闭通道
		sChannel.close();
		outChannel.close();
		ssChannel.close();
	}
}

2、客户端/服务端通信阻塞的情况:服务端等待客户端继续发送,客户端等待服务端的反馈。

public class TestBlockingNIO2 {
	//客户端
	@Test
	void client() throws IOException {
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
		while(inChannel.read(buf) != -1) {
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		//解决线程阻塞的情况告知客户端,发送完毕
		//如果没有这条语句告知服务端客户端已经发送完毕,则服务端会一直等待客户端发送信息。因此导致线程阻塞
		sChannel.shutdownOutput();
		
		//接收服务端反馈
		int len = 0;
		while((len = sChannel.read(buf)) != -1) {
			System.out.print(1);
			buf.flip();
			System.out.println(new String(buf.array(), 0, len));
		}
		inChannel.close();
		sChannel.close();
	}
	
	//服务端
	@Test
	void server() throws IOException {
		ServerSocketChannel ssChannel = ServerSocketChannel .open();
		
		ssChannel.bind(new InetSocketAddress(9898));
		
		SocketChannel sChannel = ssChannel.accept();
		
		ByteBuffer buf = ByteBuffer.allocate(1024);
		FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), 
				StandardOpenOption.WRITE,StandardOpenOption.CREATE);
		while(sChannel.read(buf) != -1) {
			buf.flip();
			outChannel.write(buf);
			buf.clear();
		}
		
		//发送反馈给客户端
		buf.put("服务端接收数据成功".getBytes());
		buf.flip();
		sChannel.write(buf);
		buf.clear();
		
		sChannel.close();
		outChannel.close();
		ssChannel.close();
	}
}

3、非阻塞式IO的实现——选择器(Selector)

选择器(Selector) 是 SelectableChannle 对象的多路复用器, Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。 Selector 是非阻塞 IO 的核心。

SelectableChannle 的结构

什么是多路复用?

此处 I/O 即网络 I/O。多路,指多个 TCP 连接(或多个 Channel)。复用,复用一个或少量线程。

I/O多路复用,即多个网络 I/O 复用一个或少量的线程来处理这些连接。 

1、客户端/服务端非阻塞式I/O通信的实现——SocketChannel/ServerChannel

SocketChannel是一个连接到TCP网络套接字的通道。

ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。

public class TestNonBlockingNIO {
	//客户端
	@Test
	void client() throws IOException {
		//1. 获取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		//2. 切换成非阻塞模式
            //为什么我们要明确配置非阻塞模式呢?
            //这是因为阻塞模式下注册操作是不允许的,会抛出 IllegalBlockingModeException异常
		sChannel.configureBlocking(false);
		
		//3. 分配指定大小的缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//4. 发送数据给服务端
		Scanner sc = new Scanner(System.in);
		while(sc.hasNext()) {
			String str = sc.next();
			buf.put((new Date().toString() + "\n" + str).getBytes());
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		//5. 关闭通道
		sChannel.close();
	}
	
	//服务端
	@Test
	void server() throws IOException {
		//1. 获取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		//2. 切换成非阻塞模式
		ssChannel.configureBlocking(false);
		
		//3. 绑定连接
		ssChannel.bind(new InetSocketAddress(9898));
		
		//4. 获取选择器
		Selector selector = Selector.open();
		
		//5. 将通道注册到选择器上,并且指定监听"接收事件"
		//当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器
		//对通道的监听事件,需要通过第二个参数 ops 指定。
		ssChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//6. 选择器轮询获取其上已经准备就绪的的事件
		while(selector.select() > 0) {// > 0 表明有事件已经准备就绪
			//7. 获取当前选择器中所有注册的“选择键(已就绪的监听状态)”
			java.util.Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			
			while(it.hasNext()) {
				//8. 获取准备就绪的事件
				SelectionKey sk = it.next();
				//9. 判断具体是什么事件准备就绪了
				if(sk.isAcceptable()) {
					//10. 若“接收就绪”,获取客户端连接
					SocketChannel sChannel = ssChannel.accept();
					
					//11. 切换非阻塞模式(客户端,服务端都要配)
					sChannel.configureBlocking(false);
					
					//12. 将该通道注册到选择器上
					sChannel.register(selector, SelectionKey.OP_READ);
				}else if(sk.isReadable()) {
					//13. 获取当前选择器上“读就绪”状态的通道
					SocketChannel sChannel = (SocketChannel)sk.channel();
					
					//14. 读取数据
					ByteBuffer buf = ByteBuffer.allocate(1024);
					
					int len = 0;
					while((len = sChannel.read(buf)) > 0) {
						buf.flip();
						System.out.println(new String(buf.array(), 0, len));
						buf.clear();
					}
				}
				//15. 选择键selectKey用完后一定要取消掉
				it.remove();
			}
		}
	}
}

主要步骤:

  • 首先,通过Selector.open()创建一个Selector,作为类似调度员的角色。
  • 然后创建一个 ServerSocketChannel,并向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,这条通道关注的是新的连接请求。
  • Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒 

阶段总结

传统 B/S 要求服务器能够同时响应多个客户端的请求,通过阻塞式 I/O 的实现:① 服务端创建多线程响应客户端的请求 ② 作为①的改进,通过一个固定大小的线程池来负责管理工作线程,避免频繁创建、销毁线程的开销,这是构建并发服务的典型方式。

阻塞式客户端/服务端通信可以用下图来表示

 

如果连接数不是非常多,最多只有几百个连接同时访问服务器,这种模式往往可以工作得很好。但是,如果连接数量急剧上升,这种实现方式就无法很好地工作了,因为线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。

而NIO则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel ,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。下面这张图说明了这种实现思路。

 

2、客户端/服务端非阻塞式I/O通信的实现——DatagramChannel

DatagramChannel是一个能收发UDP包的通道。

public class TestNonBlockingNIO2 {
	@Test
	void send() throws IOException {
		DatagramChannel dc = DatagramChannel.open();
		
		dc.configureBlocking(false);
		
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		Scanner sc = new Scanner(System.in);
		
		while(sc.hasNext()) {
			String str = sc.next();
			buf.put((new Date().toString() + ":\n" + str).getBytes());
			buf.flip();
			dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
			buf.clear();
		}
		
		dc.close();
	}
	
	@Test
	void receive() throws IOException {
		DatagramChannel dc = DatagramChannel.open();
		
		dc.configureBlocking(false);
		
		dc.bind(new InetSocketAddress(9898));
		
		Selector selector = Selector.open();
		
		dc.register(selector, SelectionKey.OP_READ);
		
		while(selector.select() > 0) {
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			while(it.hasNext()) {
				SelectionKey sk = it.next();
				if(sk.isReadable()) {
					ByteBuffer buf = ByteBuffer.allocate(1024);
					
					dc.receive(buf);
					buf.flip();
					System.out.println(new String(buf.array(),0,buf.limit()));
					buf.clear();
				}
			}
			it.remove();
		}
	}
}

3、管道通信——Pipe

管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

public class TestPipe {
	@Test
	void test() throws IOException, InterruptedException {
		//1. 获取管道
		Pipe pipe = Pipe.open();
		Pipe.SinkChannel sinkChannel = pipe.sink();//sink通道 : 数据写入 
		Pipe.SourceChannel sourceChannel = pipe.source();//source通道 : 从中读取数据
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				//2. 将缓冲区中的数据写入管道
				
				ByteBuffer buf = ByteBuffer.allocate(1024);
				System.out.println("数据开始写入sink通道...");
				buf.put("Hello!我是数据!".getBytes());
				
				buf.flip();
				try {
					sinkChannel.write(buf);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}).start();
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				//3. 读取缓冲区中的数据
				ByteBuffer buf = ByteBuffer.allocate(1024);
				System.out.print("从source中读取的数据:");
				try {
					sourceChannel.read(buf);
				} catch (IOException e) {
					e.printStackTrace();
				}
				System.out.println(new String(buf.array(), 0, buf.limit()));
			}
		}).start();;
		
		Thread.sleep(3000);//让两个线程有足够的时间进行数据传输,以免主线程提前关闭通道
		sourceChannel.close();
		sinkChannel.close();
	}
}

六、总结

1、传统的客户端/服务端通信是同步阻塞的(BIO),NIO是同步非阻塞的,JDK 1.7 后针对 NIO 进行改进后的 NIO2(AIO) 是异步非阻塞的。

2、NIO的多路复用的局限性是什么?

实际上 NIO 是同步非阻塞的,是一个线程在同步地进行事件处理,当一组Channel 处理完成后,就去检查有没有准备就绪的 channel 可以处理(单线程轮询时事件机制)。同步指的是每个准备就绪的 channel 处理是以此进行的,非阻塞,是指线程不会阻塞,只有等到 channel 准备好后,才会进行数据处理。

那么就存在一个问题,当每一个 channel 所进行的都是耗时操作(传输大量数据)由于 channel 处理是同步操作,就会积压很多 channel 任务,从而造成影响。那么就需要对 NIO 进行类似负载均衡的操作,利用线程池去进行管理读写,将 channel 分给其他线程去还行,这样即充分利用了每一个线程,又不至于任务都堆积一个线程中,等待执行。

......持续更新

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值