阻塞与非阻塞
- 传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
选择器(Selector)
- 选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
- SelectableChannle 的结构图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/dce6716f18d0fb8aa0f8b9b8cd3f0929.png)
选择器(Selector)的应用
- 当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
- 可以监听的事件类型(可使用 SelectionKey 的四个常量表示):
- 读 : SelectionKey.OP_READ (1)
- 写 : SelectionKey.OP_WRITE (4)
- 连接 : SelectionKey.OP_CONNECT (8)
- 接收 : SelectionKey.OP_ACCEPT (16)
- 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
- 例:
int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
- 表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
方 法 | 描 述 |
---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
Selector 的常用方法
方 法 | 描 述 |
---|
Set keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的SelectionKey 集合中,该方法返回这些 Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
SocketChannel
- java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
- 操作步骤:
- 打开 SocketChannel
- 读写数据
- 关闭 SocketChannel
ServerSocketChannel
- Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。
DatagramChannel
- Java NIO中的DatagramChannel是一个能收发UDP包的通道。
- 操作步骤:
- 打开 DatagramChannel
- 接收/发送数据
管道 (Pipe)
- Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
public void test(){
try {
Pipe pipe = Pipe.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
Pipe.SinkChannel channel = pipe.sink();
buffer.put("通过单向管道发送数据".getBytes());
buffer.flip();
channel.write(buffer);
Pipe.SourceChannel sourceChannel = pipe.source();
sourceChannel.read(buffer);
System.out.println(new String(buffer.array(),0,buffer.limit()));
} catch (IOException e) {
e.printStackTrace();
}
}
非阻塞IO,例:
public void client() throws IOException {
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
sChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner in = new Scanner(System.in);
int i =20;
while (i-- > 0) {
buffer.put((in.next() + ":" + LocalDateTime.now().toString()).getBytes());
buffer.flip();
sChannel.write(buffer);
buffer.clear();
}
sChannel.close();
}
public void server() throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (sk.isAcceptable()) {
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
SocketChannel socketChannel = (SocketChannel) sk.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
byteBuffer.clear();
}
}
iterator.remove();
}
}
}