本文共 2965 字,大约阅读时间需要 9 分钟。
阻塞IO |
之间在中提到传统的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注册中心) 当我们的客户端向服务端请求一个数据的时候,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.获取当前选择器中所有注册的选择键(已就绪的监听事件) Iteratoriterator = 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选择器上,其中的参数有:
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个常量中的其中一个,用来表示该通道在选择器上注册的是什么事件。
转载地址:http://fnhwi.baihongyu.com/