阻塞IO |
之间在java NIO(一)-----NIO基本概念中提到传统的IO是阻塞式的,而NIO是非阻塞式的(相对于网络通信而言)。通过下面图中可以了解IO阻塞的过程。
1. 客户端向服务端发起一个读写请求,但是服务端不确定数据是否有效,此时该线程就会进入阻塞状态,也就是说此线程在此期间无法做其他任何事情。
- 针对于上面的情况,后面有了一个治标不治本的方法-------使用多线程
使用多线程技术之后,1号线程阻塞了还有2号线程可用来做其他事,2号堵塞了还有3号线程可以用,相对于上面单线程来说确实能够解决一部分问题,但是线程池的线程数量总是有限制了,不可能无限使用。而且线程占用的都是实实在在的内存,开销非常大。所以使用多线程解决IO阻塞治标不治本。
非阻塞IO |
从前文中知道了阻塞式IO的弊端,再来了解一下非阻塞式IO的原理:
非阻塞式IO中有三个核心:
1.Channel 通道
2.Buffer 缓冲区
3.Selector 选择器
Selector选择器在java API文档中被称作是多路复用器
,用于注册一个或多个Channel通道,Selector会不断地检测通道状态是否是可读的或者可写的。它实现了集中管理监控通道状态,避免了因数据的不确定性导致的IO阻塞等待。(Selector的作用让我想到了Zookeeper注册中心)
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
方法的源码:
其中第二个参数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个常量中的其中一个,用来表示该通道在选择器上注册的是什么事件。