阻塞IO

  之间在java NIO(一)-----NIO基本概念中提到传统的IO是阻塞式的,而NIO是非阻塞式的(相对于网络通信而言)。通过下面图中可以了解IO阻塞的过程。

1.   客户端向服务端发起一个读写请求,但是服务端不确定数据是否有效,此时该线程就会进入阻塞状态,也就是说此线程在此期间无法做其他任何事情。

java nio 非阻塞 java nio阻塞和非阻塞_选择器

  1.   针对于上面的情况,后面有了一个治标不治本的方法-------使用多线程

使用多线程技术之后,1号线程阻塞了还有2号线程可用来做其他事,2号堵塞了还有3号线程可以用,相对于上面单线程来说确实能够解决一部分问题,但是线程池的线程数量总是有限制了,不可能无限使用。而且线程占用的都是实实在在的内存,开销非常大。所以使用多线程解决IO阻塞治标不治本。

java nio 非阻塞 java nio阻塞和非阻塞_服务端_02

非阻塞IO

  从前文中知道了阻塞式IO的弊端,再来了解一下非阻塞式IO的原理:
非阻塞式IO中有三个核心: 1.Channel 通道
2.Buffer 缓冲区
3.Selector 选择器

  Selector选择器在java API文档中被称作是多路复用器,用于注册一个或多个Channel通道,Selector会不断地检测通道状态是否是可读的或者可写的。它实现了集中管理监控通道状态,避免了因数据的不确定性导致的IO阻塞等待。(Selector的作用让我想到了Zookeeper注册中心)

java nio 非阻塞 java nio阻塞和非阻塞_服务端_03


Selector API文档介绍

  当我们的客户端向服务端请求一个数据的时候,Channel通道因为注册到了Selector选择器,所以Selector会等到服务端的数据完全准备就绪之后,再将此请求发送到服务器端的一个或者多个线程上去执行,因为数据此时是已经准备好了的,所以线程不会进入阻塞状态。

下面代码演示了客户端向服务端发送消息的实现过程;

@Test
	void Client() throws IOException {
		//1.获取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8083));
		//2.切换成非阻塞模式
		sChannel.configureBlocking(false);
		//3.分配指定大小的缓冲区
		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
		//4.发送数据给服务器端
		byteBuffer.put("客户端发送的数据".getBytes());
		//5.切换为读模式
		byteBuffer.flip();

		sChannel.write(byteBuffer);
		byteBuffer.clear();
		//6.关闭通道
		sChannel.close();

	}

	@Test
	void Server() throws IOException {
		//1.获取通道
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		//2.切换为非阻塞模式
		serverChannel.configureBlocking(false);
		//3.绑定连接
		serverChannel.bind(new InetSocketAddress(8083));
		//4.获取选择器
		Selector selector = Selector.open();
		//5.注册通道到选择器(注册的是监听事件)
		serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		//6.通过选择器轮询式的获取选择器上已经准备就绪的事件
		while (selector.select() > 0) {
			//当选择器上准备就绪的时间大于0(至少有一个准备好了)
			//7.获取当前选择器中所有注册的选择键(已就绪的监听事件)
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()) {
				//8.获取准备就绪的事件
				SelectionKey next = iterator.next();
				//9.判断该事件是什么时间(读、写、连接、接收)
				if (next.isAcceptable()) {
					//10.若接收就绪,获取客户端连接
					SocketChannel channel = serverChannel.accept();
					//11.切换为非阻塞模式
					channel.configureBlocking(false);
					//12.将该通道注册到选择器上
					channel.register(selector, SelectionKey.OP_READ);
				} else if (next.isReadable()) {
					//13.获取当前选择器上读就绪状态的通道
					SocketChannel channel = (SocketChannel) next.channel();
					//14.获取数据
					ByteBuffer bBuffer = ByteBuffer.allocate(1024);
					int len = 0;
					while ((len = channel.read(bBuffer)) > 0) {
						bBuffer.flip();
						System.out.println(new String(bBuffer.array(),0,len));
						bBuffer.clear();
					}
				}
				//15.取消选择器
				iterator.remove();
			}
		}
	}

  上面代码中的第5步中的register方法是用于将Channel通道注册到Selector选择器上,其中的参数有:
1.Selector 选择器对象
2.SelectionKey 选择注册的事件

看一下register方法的源码:

java nio 非阻塞 java nio阻塞和非阻塞_java nio 非阻塞_04


其中第二个参数ops源码中注明了来源于SelectionKey类。该类中有以下4个常量:

1.表示读事件

public static final int OP_READ = 1 << 0;

2.表示写事件

public static final int OP_WRITE = 1 << 2;

3.表示连接事件

public static final int OP_CONNECT = 1 << 3;

4.表示接收事件

public static final int OP_ACCEPT = 1 << 4;

也就是说,register(Selector sel, int ops)方法中的第二个参数ops应该是来自于SelectionKey类中的4个常量中的其中一个,用来表示该通道在选择器上注册的是什么事件。