Java NIO之七 阻塞与非阻塞、选择器(Selector)、SelectionKey、SocketChannel、管道(Pipe)
阻塞与非阻塞
- 传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取或写入,改线程在此期间不能执行其他任务。因此,在完成网络通信进行IO操作时,由于线程会阻塞,所以服务端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。
在采用多线程进行读写的时候,每个客户端会有一条线程,并发量大的时候效率不高,且有可能在阻塞状态下永远拿不到缺失执行的资源,线程没有死掉,极大浪费CPU资源。 - Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以执行其他任务。线程通常将非阻塞IO的空闲时间用于在其通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此,在NIO可以让服务器使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
在客户端和和服务端建立了Channel和Selector,每一个Channel都要在Selector上注册,Selector会对所有注册的Channel进行监测,只有Channel的传输到服务端的数据都准备完成之后,Selector才去开启一个或者多个服务端线程执行Client端的网络IO操作。
选择器(Selector)
- 选择器(Selector)是SlectableChannel 对象的多路复用器,==Selector可以同时监控多个SelectableChannel 的 IO 状况 也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞的IO的核心。
- SelectableChannel 的结构如下图:
选择器(Selector)的应用
- 创建Selector : 通过调用Selector.open() 方法创建一个Selector。
//创建选择器
Selector selector = Selector.open();
- 向选择器注册通道:SelectableChannel.register( Selector sel, int ops)
//创建一个Socket
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9898);
//获取SocketChannel
SocketChannel channel = socket.getChannel();
//创建选择器
Selector selector = selector.open();
//将 SocketChannel 切换到非阻塞模式
channel.configureBlocking(false);
//向Selector 注册 Channel
Selectionkey key = channel.register(selector,SelectionKey.OP_READ);
- Selector 的常用放方法
SelectionKey
- 当调用register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。
- 可以监听的事件类型(可使用SelectionKey 的四个常量表示):
- 读:SelectionKey.OP_READ (1)
- 写:SelectionKey.OP_WRITE (4)
- 连接:SelectionKey.OP_CONNECT (8)
- 接收:SelectionKey.OP_ACCEPT (16)
- 若注册时不止一个监听事件,则可以使用“位或”操作符连接。
//注册“监听事件”
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE;
- SelectionKey : 表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件 (选择键)。选择键包含两个表示为整数的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
SocketChannel
- Java NIO 中的SocketChannel 是一个连接到TCP网络的套接字通道。
- 操作步骤:
- 打开SocketChannel
- 读写数据
- 关闭SocketChannel
- Java NIO 中的ServerSocketChannel 是一个可以监听新进的TCP连接的通道,就像标准IO中的ServerSocket一样。
DatagramChannel
- Java NIO中的DatagramChannel是一个能收发UDP包的通道。
- 操作步骤:
- 打开DatagramChannel
- 接收/发送数据
管道(Pipe)
- Java NIO管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,然后从source通道读取。
- 向通道写数据
@Test
public void test1() throws IOException{
String str = "测试数据";
//创建管道
Pipe pipe = Pipe.open();
//向管道写入
pipe.SinkChannel sinkChannel = pipe.sink();
//通过SinkChannel 的 write() 方法写数据
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
buf.put(str.getBytes());
buf.flip();
while(buf.hasRemaining()){
sinkChannel.write(buf);
}
}
- 从管道读取数据
从管道读取的数据,需要访问source通道。
//从管道读取数据
Pipe.SourceChannel sourceChannel = pipi.source();
调用source通道的read()方法来读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
sourceChannel.read(buf);
- 使用Demo
通常在使用是在多线程中,由一个线程向管道里面传输数据,另一个线程从里面取出数据。
public class TestPipe {
@Test
public void test() throws IOException{
//1. 获取管道
Pipe pipe = Pipe.open();
//2. 将缓冲区中的数据写入管道
ByteBuffer buf = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通过单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
//3. 读取缓冲区中的数据
Pipe.SourceChannel sourceChannel = pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(), 0, len));
sourceChannel.close();
sinkChannel.close();
}
}