写在前面
笔者的另一篇博客详解了NIO中的各个方面,包括缓冲区,通道,分散聚集等等NIO核心内容,附博客链接:https://blog.csdn.net/CPrimer0/article/details/113889308
阻塞与非阻塞
首先我们先看一下官方对于阻塞与非阻塞的定义,有一个初步的认知
- 传统的IO 流都是阻塞式的。也就是说,当一个线程调用
read()
或write()
时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。 - Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞IO 的空闲时间用于在其他通道上执行IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
传统IO如图所示:server端在不能确定传输数据的真实有效时,该线程就会阻塞,直到有一些数据被读取或者写入,否则改线程不会执行任何操作,当有大量用户请求时,多线程模式下也会造成性能的下降
而NIO非阻塞模式的实现依赖于NIO的核心组成部分选择器(Selector,后文会详细介绍),选择器主要是将每一个传输数据的通道注册到选择器上,选择器作用是监控这些IO的状态(读,写,连接状态),然后用选择器监控通道的状况,等待所有的线程准备就绪时,选择器将任务分配到服务端一个或者多个线程上再去运行。
举个栗子:选择器就相当于菜鸟驿站,只有当快递准备就绪时,才会给你打电话让你去取快递,所以选择器就是这么个角色,当数据准备就绪时,才会分配到服务端运行
选择器(Selector)
什么是选择器?
选择器是SelectableChanne
l对象的多路复用器,Selector可以同时监控多个SelectableChannel
的IO状况,即Selector可监控的对象必须式SelectChanne
l的子类,Selector是非阻塞IO的核心
选择器的使用方式
- 创建Selector:通过调用
Selector.open()
方法创建一个Selector:Selector selector = Selector.open() - 向选择器注册通道:SelectableChannel.register(Selector sel, int ops),其中关于ops表示指定选择器所要监听的事件
可以监听的事件类型可以使用SelectionKey的四个常量表示:
读: SelectionKey.OP_READ (1)
写: SelectionKey.OP_WRITE (4)
连接: SelectionKey.OP_CONNECT(8)
接收: SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“|”未获操作符连接
选择器常用的方法
一个具体实例展示如何使用非阻塞式IO
public class TestNonBlockingNIO {
//客户端
@Test
public void client() throws IOException{
//1、获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
//2、切换非阻塞模式
socketChannel.configureBlocking(false);
//3、分配指定大小的缓冲区
ByteBuffer buf=ByteBuffer.allocate(1024);
//4、发送数据服务器
buf.put(new Date().toString().getBytes());
buf.flip();
socketChannel.write(buf);
buf.clear();
//5、关闭通道
socketChannel.close();
}
//服务端
@Test
public void server() throws IOException{
//1、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2、切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3、绑定连接
serverSocketChannel.bind(new InetSocketAddress(8888));
//4、获取选择器
Selector selector = Selector.open();
//5、将通道注册到选择器上(第二个选项参数叫做选择键,用于告诉选择器需要监控这个通道的什么状态或者说什么事件(读、写、连接、接受))
//选择键是整型值,如果需要监控该通道的多个状态或事件,可以将多个选择键用位运算符“或”“|”来连接
//这里服务端首先要监听客户端的接受状态
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6、轮询式地获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//7、获取当前选择中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
//8、获取准备“就绪”的是事件
SelectionKey sk=iterator.next();
//9、判断是什么事件准备就绪
if(sk.isAcceptable()){
//10、若接受就绪,获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
//11、客户端连接socketChannel也需要切换非阻塞模式
socketChannel.configureBlocking(false);
//12、将该通道注册到选择器上,监控客户端socketChannel的读就绪事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if(sk.isReadable()){
//13、获取当前选择器上“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel) sk.channel();
//14、读取数据
ByteBuffer buf=ByteBuffer.allocate(1024);
int len=0;
while((len=socketChannel.read(buf))>0){
buf.flip();
System.out.println(new String(buf.array(),0,len));
buf.clear();
}
}
//15、取消选择键SelectionKey
iterator.remove();
}
}
}
}