【Java I/O】NIO 详解
概述
Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。
NIO 三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。
NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入, 这个线程同时可以去做别的事情。
NIO 和 BIO 的比较
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
- BIO 是阻塞的,NIO 则是非阻塞的
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
工作机制
示意图
NIO 简单原理示意如下图所示:
- Buffer 是内存块,底层是数组,数据的读取写入通过Buffer ,Buffer 可以读也可以写,需要flip 方法切换
- Channel 是双向的,可以反映底层操作系统的情况,比如Linux 底层的操作系统通道就是双向的,每个Channel 都会对应一个Buffer
- Selector 对应一个线程,一个线程对应多个Channel(连接),Selector 会根据不同的事件,在各个通道上切换
缓冲区 Buffer
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
Buffer 类内部结构如下:
private int mark = -1; // 标记
private int position = 0; // 下一个要被读或写的元素索引
private int limit; // 缓冲区当前终点,不能越界,可以修改
private int capacity; // 最大容量,创建时设置
Buffer 类相关方法一览:
// @since JDK1.4
public final int capacity() // 返回此缓冲区容量
public final int position() // 返回此缓冲区位置
public final Buffer position(int newPosition) // 设置此缓冲区位置
public final int limit() // 返回此缓冲区限制
public final Buffer limit(int newLimit) // 设置缓冲区限制
public final Buffer mark() // 在此缓冲区的位置设置标记
public final Buffer reset() // 将此缓冲区的位置重置为之前标记的位置
public final Buffer clear() // 清除此缓冲区,即将各个标记恢复到初始状态,数据并没有真正擦除
public final Buffer flip() // 反转此缓冲区
public final Buffer rewind() // 重绕此缓冲区
public final int remaining() // 返回当前位置与限制之间的元素数
public final boolean hasRemaining() // 判断当前位置与限制之间是否有元素
public boolean isReadOnly() // 判断此缓冲区是否只读
// @since JDK1.6
public boolean hasArray() // 判断此缓冲区是否具有可访问的底层实现数组
public Object array() // 返回此缓冲区底层实现数组
public int arrayOffset() // 返回此缓冲区底层实现数组中第一个缓冲区元素的偏移量
public boolean isDirect() // 判断此缓冲区是否为直接缓冲区
Java 中的基本数据类型(boolean除外),都有Buffer 实现类,其中最常用的是ByteBuffer (二进制数据),该类主要方法如下:
public static ByteBuffer allocateDirect(int capacity) // 创建直接缓冲区
public static ByteBuffer allocate(int capacity) // 设置缓冲区初始容量
public static ByteBuffer wrap(byte[] array) // 把一个数组放到缓冲区使用
public static ByteBuffer wrap(byte[] array, int offset, int length) // 初始化位置offset,上界length的缓冲区
public byte get() // 从当前位置position上get,get后position会自动加1
public byte get(int index) // 从指定位置get
public ByteBuffer put(byte b) // 从当前位置put,put后position自动加1
public ByteBuffer put(int index, byte b) // 从指定位置put
通道 Chnanel
NIO 的通道(Channel)类似于流(stream,如FileInputStream),但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
Channel 是一个接口,常用的Channel 类有:FileChannel 、DatagramChannel 、ServerSocketChannel 和SocketChannel。(ServerSocketChannel类似ServerSocket ,SocketChannel 类似Socket)
- FileChannel 用于文件的数据读写
- DatagramChannel 用于 UDP 的数据读写
- ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写
FileChannel 类主要用来对本地文件进行IO 操作,常用方法有:
public int read(ByteBuffer dst) // 从通道读取数据并放到缓冲区
public int write(ByteBuffer src) // 把缓冲区数据写到通道
public long transferFrom(ReadableByteChannel src, long position, long count) // 从目标通道复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target) // 把数据从当前通道复制到目标通道
应用示例
文件读取、写入
使用Channel、Buffer将文件1中的内容写入到文件2
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel inFileChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel outFileChannel = fileOutputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(512);
// 循环读取
while(true) {
// 重置buffer
buffer.clear();
int read = inFileChannel.read(buffer);
// 已读完
if (read == -1) {
break;
}
// 将buffer 中的数据写入到2.txt
buffer.flip();
outFileChannel.write(buffer);
}
fileInputStream.close();
fileOutputStream.close();
}
}
拷贝文件 transferFrom 方法
使用 FileChannel和方法transferFrom 完成文件的拷贝
public class FileChannelDemo2 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("a.png");
FileOutputStream fileOutputStream = new FileOutputStream("b.png");
// 获取各个流对应的fileChannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
// 使用 transferForm 完成拷贝
destCh.transferFrom(sourceCh,0,sourceCh.size());
// 关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
注意事项
- ByteBuffer 支持类型化的put 和get,put 放入的是什么数据类型,get就应该使用相应的数据类型来取出
- 可以将普通Buffer 转成只读Buffer,asReadOnlyBuffer()
- NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由 NIO 来完成
/**
* MappedByteBuffer 可让文件直接在内存(堆外内存)修改, 操作系统不需要拷贝一次
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
// 获取对应的通道
FileChannel fileChannel = randomAccessFile.getChannel();
// param1: 读写模式, param2: 可以修改的起始位置, param3: 映射到内存的大小(不是索引大小)即1.txt有多少字节映射到内存
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'A');
mappedByteBuffer.put(3, (byte) 'B');
// mappedByteBuffer.put(5, (byte) 'C'); // IndexOutOfBoundsException
randomAccessFile.close();
}
}
- NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering
/**
* Scattering:将数据写入到buffer 时, 可以采用 buffer 数组,依次写入 [分散]
* Gathering: 从buffer 读取数据时, 可以采用 buffer 数组,依次读
*/
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws IOException {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到 socket, 并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建 buffer 数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
// 等客户端连接(telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
// 假定从客户端接收8个字节
int messageLength = 8;
while (true) {
int byteRead = 0;
while(byteRead < messageLength) {
long length = socketChannel.read(byteBuffers);
byteRead += length;
System.out.println("byteRead=" + byteRead);
// 打印当前buffer 的position 和limit
Arrays.stream(byteBuffers)
.map(buffer -> "position=" + buffer.position() + ", limit=" + buffer.limit())
.forEach(System.out::println);
}
// 将所有的 buffer 进行 flip
Arrays.asList(byteBuffers).forEach(Buffer::flip);
// 将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite < messageLength) {
long length = socketChannel.write(byteBuffers);
byteWrite += length;
}
// 将所有的 buffer 进行 clear
Arrays.asList(byteBuffers).forEach(Buffer::clear);
System.out.println("byteRead=" + byteRead + ", byteWrite=" + byteWrite + ", messageLength=" + messageLength);
}
}
}
选择器 Selector
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。避免了多线程之间的上下文切换导致的开销。
Selector 类是一个抽象类, 常用方法和说明如下:
// 得到一个选择器对象
public static Selector open()
// 从内部集合中得到所有SelectionKey
public Set<SelectionKey> selectedKeys()
// 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回
// 参数用来设置超时时间
public int select(long timeout)
// 阻塞
public int select()
// 不阻塞,立马返还
public int selectNow()
// 唤醒 selector
public Selector wakeup()
NIO 非阻塞网络编程原理分析
NIO 非阻塞 网络编程相关的(Selector、SelectionKey、ServerSocketChannel 和 SocketChannel) 关系梳理图如下:
- 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
- Selector 进行监听 select 方法, 返回有事件发生的通道的个数
- 将 socketChannel 注册到 Selector 上, register(Selector sel, int ops), 一个 selector 上可以注册多个 SocketChannel
- 注册后返回一个 SelectionKey, 会和该 Selector 关联(集合)
- 进一步得到各个 SelectionKey (有事件发生)
- 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
- 通过得到的 channel , 完成业务处理
代码示例
/**
* 服务端
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建 ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 得到一个 Selector 对象
Selector selector = Selector.open();
// 绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 把serverSocketChannel注册到selector 关心事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 循环等待客户端连接
while (true) {
// 等待1s没有事件发生就返回
if (selector.select(1000) == 0) {
System.out.println("服务器等待1s, 无连接");
continue;
}
// 如果返回的>0, 表示已经获取到关注的事件, 就获取相关的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()) {
// 获取到 SelectionKey
SelectionKey key = iterator.next();
// 根据 key 对应的通道发生的事件做相应处理
// 如果是 OP_ACCEPT, 有新的客户端连接
if (key.isAcceptable()) {
// 给该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端已连接, socketChannel: " + socketChannel.hashCode());
// 将SocketChannel设置为非阻塞
socketChannel.configureBlocking(false);
// 将socketChannel注册到selector, 关注事件为OP_READ, 同时给socketChannel关联一个 Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 发生OP_READ
if (key.isReadable()) {
// 通过key反向获取对应的channel
SocketChannel channel = (SocketChannel) key.channel();
// 获取到channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("客户端消息: " + new String(buffer.array()));
}
// 手动从集合中移动当前的selectionKey, 防止重复操作
iterator.remove();
}
}
}
}
/**
* 客户端
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
// 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置非阻塞
socketChannel.configureBlocking(false);
// 提供服务器端的ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
// 连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间, 客户端不会阻塞, 可以做其它工作...");
}
}
// 如果连接成功, 就发送数据
ByteBuffer buffer = ByteBuffer.wrap("hello nio server".getBytes());
// 发送数据, 将buffer数据写入channel
socketChannel.write(buffer);
System.in.read();
}
}
SelectionKey
SelectionKey 表示 Selector 和网络通道的注册关系, 共四种:
public static final int OP_READ = 1 << 0; // 代表读操作,值为 1
public static final int OP_WRITE = 1 << 2; // 代表写操作,值为 4
public static final int OP_CONNECT = 1 << 3; // 代表连接已经建立,值为 8
public static final int OP_ACCEPT = 1 << 4; // 有新的网络连接可以 accept,值为 16
相关方法如下:
public Selector selector() // 得到与之关联的selecter
public SelectableChannel channel() // 得到与之关联的channel
public final Object attachment() // 得到与之关联的共享数据
public SelectionKey interestOps(int ops) // 设置或改变监听事件
public final boolean isAcceptable() // 是否可以accept
public final boolean isReadable() // 是否可以读
public final boolean isWritable() // 是否可以写
ServerSocketChannel
ServerSocketChannel 在服务器端监听新的客户端 Socket 连接
相关方法如下:
public static ServerSocketChannel open() // 得到一个ServerSocketChannel通道
public final ServerSocketChannel bind(SocketAddress local) // 设置服务器端端口
public final SelectableChannel configureBlocking(boolean block) // 设置阻塞true或非阻塞false模式
public SocketChannel accept() // 接受一个连接, 返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops) // 注册一个选择器并设置监听事件
SocketChannel
SocketChannel 是网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
相关方法如下:
public static SocketChannel open() // 得到一个SocketChannel通道
public final SelectableChannel configureBlocking(boolean block) // 设置阻塞true或非阻塞false模式
public boolean connect(SocketAddress remote) // 连接服务器
public boolean finishConnect() // 如果connect失败,用此方法完成连接操作
public int write(ByteBuffer src) // 往通道写数据
public int read(ByteBuffer dst) // 从通道读数据
public final SelectionKey register(Selector sel, int ops, Object att) // 注册一个选择器并设置监听事件,最后一个参数设置共享数据
案例:群聊系统
- 编写一个 NIO 多人群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息
/**
* 服务端
*/
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel listenerChannel;
private final int PORT = 6667;
public GroupChatServer() {
try {
// 得到Selector
selector = Selector.open();
// 得到ServerSocketChannel
listenerChannel = ServerSocketChannel.open();
// 绑定端口
listenerChannel.bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
listenerChannel.configureBlocking(false);
// 将该listenChannel注册到 selector
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listener() {
try {
while(true) {
int count = selector.select(2000);
// 有事件处理
if (count > 0) {
// 遍历得到selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
// 监听到accept
// 获得socketChannel
SocketChannel socketChannel = listenerChannel.accept();
// 非阻塞
socketChannel.configureBlocking(false);
//将该socketChannel注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + " 已上线");
} else if (selectionKey.isReadable()) {
readData(selectionKey);
}
// 防止重复处理
iterator.remove();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取客户端消息
*/
private void readData(SelectionKey selectionKey) {
// 取到关联的socketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// 读消息
int count = socketChannel.read(buffer);
if (count > 0) {
String msg = new String(buffer.array());
System.out.println(socketChannel.getRemoteAddress() + " 消息: " + msg);
sendInfo(socketChannel, msg);
}
} catch (IOException e) {
try {
System.out.println(socketChannel.getRemoteAddress() + " 已下线");
// 取消注册
selectionKey.cancel();
// 关闭通道
socketChannel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
}
/**
* 向其它的客户端转发消息
*/
private void sendInfo(SocketChannel selfChannel, String msg) {
System.out.println("开始转发消息");
try {
// 遍历所有注册到selector上的socketChannel, 并排除self
for (SelectionKey key : selector.keys()) {
Channel channel = key.channel();
if(channel instanceof SocketChannel && channel != selfChannel) {
// 将消息写到channel
((SocketChannel)channel).write(ByteBuffer.wrap(msg.getBytes()));
}
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("转发完成");
}
public static void main(String[] args) {
GroupChatServer chatServer = new GroupChatServer();
chatServer.listener();
}
}
/**
* 客户端
*/
public class GroupChatClient {
private final String HOST = "127.0.0.1";
private final int PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public GroupChatClient() {
try {
selector = Selector.open();
// 连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
// 非阻塞
socketChannel.configureBlocking(false);
// 将channel注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " 准备完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void sendInfo(String msg) {
msg = username + ": " + msg;
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
public void readMsg() {
try {
int readChannels = selector.select();
// 有可以用的通道
if (readChannels > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从channel读出消息
socketChannel.read(buffer);
String msg = new String(buffer.array());
System.out.println(msg);
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
GroupChatClient chatClient = new GroupChatClient();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
chatClient.readMsg();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}