这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
简述
Java传统的网络编程为BIO(blocking I/O)网络编程。其最大的特点就是同步阻塞的,不管是ServerSocket.accept(),还是Socket的read()和write(),都是同步阻塞的。
今天我们学习的NIO网络编程模型,可以理解为新的(new)网络编程模型,也可以理解为非阻塞(non-blocking)网络编程模型。
核心组成部分
Java NIO 三个核心部分:
-
Buffer 缓冲区,用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。
-
Channel 通道,类似「流」,我们可以从通道中读取数据,又可以写数据到通道。这点与「流」不同,「流」的读写通常都是单向的,比如流通常分为「输入流」和「输出流」。
-
Selector 选择器,是NIO模型中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
Buffer 缓冲区
Buffer(缓冲区)本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。Buffer在JDK里是一个抽象类,常见的实现类有:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Buffer是可以任意读写的一块内存。内存有三个基本操作:分配内存、读、写。与之对应的Buffer,基本操作也是分配、读、写。
以ByteBuffer为例:
- 分配内存
ByteBuffer buffer = ByteBuffer.allocate(10);
- 写入操作
buffer.put((byte) 'a');
- 读操作
byte b = buffer.get();
通常我们需要把Buffer从写模式切换到读模式,调用flip()
方法。
buffer.flip();
Buffer有三个重要属性:capacity,position和limit。Buffer的内存分配、读、写都离不开这三个属性。
-
capacity,分配内存的容量大小。一旦分配下来,其基本不会变。
-
position,当前内存操作的位置。当处于写模式下,其为下一个待写入的位置;当处于读模式下,其为当前可读的位置。
-
limit,限制位置(大小)。当处于写模式下,其等于capacity,即可写满;当处于读模式下,其为限制读最大的位置。
示例:
public class BufferDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("init:" + buffer);
buffer.put((byte) 'a');
System.out.println("after write:" + buffer);
buffer.flip();
System.out.println("after flip:" + buffer);
buffer.get();
System.out.println("after read:" + buffer);
}
}
// 输出结果
init:java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
after write:java.nio.HeapByteBuffer[pos=1 lim=10 cap=10]
after flip:java.nio.HeapByteBuffer[pos=0 lim=1 cap=10]
after read:java.nio.HeapByteBuffer[pos=1 lim=1 cap=10]
Selector 选择器
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
最基本的用法就是:向Selector注册通道,并设置感兴趣事件,其将会返回返回SelectionKey对象。
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);
通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”;一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。分别用四个常量表示:
- SelectionKey.OP_CONNECT 连接就绪
- SelectionKey.OP_ACCEPT 接受就绪
- SelectionKey.OP_READ 读就绪
- SelectionKey.OP_WRITE 写就绪
如果想要监听多个就绪状态,可以用「位或」操作连起来。
int allInterestSet = SelectionKey.OP_CONNECT | SelectionKey.OP_ACCEPT | SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey 相关操作
获取通道在选择器选择的感兴趣的事件集合
int interestSet = selectionKey.interestOps();
通过简单的「与」操作,判断是否对相关事件感兴趣
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
获取通道已准备就绪的操作的集合
int readyOps = selectionKey.readyOps();
可以像上面「与」操作来判断某个事件是否就绪。但我们可以使用以下方法直接获取相关事件是否就绪
boolean acceptable = selectionKey.isAcceptable();
boolean connectable = selectionKey.isConnectable();
boolean readable = selectionKey.isReadable();
boolean writable = selectionKey.isWritable();
// 获取Channel和Selector
SelectableChannel selectableChannel = selectionKey.channel();
Selector selector1 = selectionKey.selector();
完整代码:
public class SelectorDemo {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
// Channel设置为非阻塞模式
channel.configureBlocking(false);
// 向Selector注册通道,并设置感兴趣事件,OP_READ表示读就绪事件,并返回SelectionKey对象
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);
// 获取通道在选择器选择的感兴趣的事件集合
int interestSet = selectionKey.interestOps();
// 通过简单的「与」操作,判断是否对相关事件感兴趣
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
// 获取通道已准备就绪的操作的集合
int readyOps = selectionKey.readyOps();
// 可以像上面「与」操作来判断某个事件是否就绪。但我们可以使用以下方法直接获取相关事件是否就绪
boolean acceptable = selectionKey.isAcceptable();
boolean connectable = selectionKey.isConnectable();
boolean readable = selectionKey.isReadable();
boolean writable = selectionKey.isWritable();
// 获取Channel和Selector
SelectableChannel selectableChannel = selectionKey.channel();
Selector selector1 = selectionKey.selector();
// 附加对象设置方式1:selectionKey.attach(theObject);
// 附加对象设置方式2:selectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
// 获取附加对象方式:Object attachedObj = selectionKey.attachment();
// 获取准备就绪(自上次调用select()方法后有多少个通道变成就绪状态)的通道数。如果当前还未有就绪的通道,则会阻塞到至少一个通道在你注册的事件上就绪。
int selectCount = selector.select();
// int select(long timeout); 可以设置timeout毫秒阻塞事件
// selectNow() 不会阻塞,不管什么通道就绪都立刻返回
// 获取已就绪「键」集
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey next = keyIterator.next();
// SelectionKey相关操作
next.interestOps();
next.readyOps();
next.isAcceptable();
next.cancel();
// Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中
keyIterator.remove();
}
// 当某个线程调用select方法阻塞时,其他线程可通过调用wakeUp()方法唤醒
// selector.wakeup();
// 关闭Selector,注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
selector.close();
}
}