前言: 之前的文章《Java文件IO常用归纳》主要写了Java 标准IO要注意的细节和技巧,由于网上各种学习途径,所以并没有详细示例等。本文主要简单看看java的NIO库的用法,并做个小归纳,可以对比标准IO参考一下。
NIO概述
(一)背景
NIO(New IO),在Java 1.4引入的一个新的IO API。【可替代标准IO API】
(二)工作方式
Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道
NIO 与 标准IO 的区别
IO面向Stream(流),NIO面向Buffer(缓存)
面向Stream:每次从流中读取一个或多个字节,直到读取所有字节,并没有缓存字节的地方。不能前后移动流中的数据(因为如果要前后移动从流中读取的数据,就需先将其缓存到一个缓存区中)。
面向Buffer【更灵活】:数据读取到一个稍后处理的缓冲区,需要时即可在缓冲区中前后移动(注意:移动前首先需要检查是否该缓冲区中包含你需要处理的数据)。需要确保当更多数据读入缓冲区时,不会覆盖掉区中原有的尚未处理的数据。
IO流都是阻塞的,而NIO有非阻塞模式
IO的流:当一个线程threadA使用IO调用read()/write()操作时,threadA被阻塞,直到一些数据被读取或写入完成,此过程中threadA不能做任何事。
NIO的非阻塞模式:
【非阻塞读】线程threadA从某channel发送请求读取数据时,threadA仅能得到目前可用的数据,若目前没有可用数据,那么threadA不会获取任何数据并可以先做别的事情,而不是保持阻塞,直到有可用数据在这个通道出现。
【非阻塞写】一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
【应用】线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出channels。
NIO独有选择器(Selector)
常用API总结
(一)核心接口与类关系图解与分析
FileChannel fileChannel = accessFile.getChannel();
SocketChannel
创建SocketChannel对象:
SocketChannel sc = SocketChannel.open();
设置非阻塞IO状态:
sc.configureBlocking(false);
开始打开连接
sc.connect(new InetSocketAddress("http://jianshu.com", 80));
非阻塞状态下,成功连接前,干别的事
sc.configureBlocking(false);
///...............
while(!sc.finishConnect()){ do other sth.... }
保证非阻塞IO状态下,read()过程不会read 空数据
sc.configureBlocking(false);
///...............
while((int len = sc.read(buf))==0){ do other sth.... }
保证非阻塞IO状态下,write()过程不会write空数据
sc.configureBlocking(false);
///...............
while((int len = sc.write(buf))==0){ do other sth.... }
ServerSocketChannel、DatagramChannel、Selector和Pipe的写法与方法说明,由于篇幅原因,可以参考下文的示例。
(三)文件IO --- FileChannel
读文件
public static byte[] readBytes(String fileName) {
try {
///获取对应文件的FileChannel对象
RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
FileChannel fileChannel = accessFile.getChannel();
/// 创建一个缓冲区(大小为48byte)
ByteBuffer byteBuffer = ByteBuffer.allocate(48);
StringBuilder builder = new StringBuilder();
int bytesRead = fileChannel.read(byteBuffer);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
///翻转buffer
byteBuffer.flip();
///每次读取完之后,输出缓存中的内容
while (byteBuffer.hasRemaining()) {
System.out.println((char) byteBuffer.get());
builder.append((char) byteBuffer.get());
}
///然后清空缓存区
byteBuffer.clear();
///重新再读数据到缓存区中
bytesRead = fileChannel.read(byteBuffer);
}
accessFile.close();
return builder.toString().getBytes();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
写入文件
public static void writeBytes(String fileName, byte[] data) {
try {
RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
FileChannel channel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put(data);
channel.write(buffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
通道间内容传输
/**
* channel 间的传输
*
* @param sFileName 源文件
* @param dFileName 目标文件
*/
public static void channelToChannel(String sFileName, String dFileName) {
try {
RandomAccessFile sAccess = new RandomAccessFile(sFileName, "rw");
RandomAccessFile dAccess = new RandomAccessFile(dFileName, "rw");
FileChannel sChannel = sAccess.getChannel();
FileChannel dChannel = dAccess.getChannel();
long pos = 0;
long sCount = sChannel.size();
long dCount = dChannel.size();
// dChannel.transferFrom(sChannel,pos,sCount);//dChannel 必须是FileChannel
sChannel.transferTo(pos, dCount, dChannel);///sChannel 是FileChannel
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
(四)TCP通信 --- SocketChannel
基本的C/S TCP通信
Client客户端写法:
/**
* Client SocketChannel 写法:
*/
public static void client(String fileName) {
SocketChannel sc = null;
try {
// 创建一个SocketChannel 通道
TODO: FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
sc = SocketChannel.open();
///TODO:非阻塞IO状态下,socketChannel就可以异步地执行read()、write()、connect()方法了
sc.configureBlocking(false);
sc.connect(new InetSocketAddress("http://jianshu.com", 80));
while (!sc.finishConnect()) {///保证在connect成功之前,可以做别的事情
//做点别的事。。。。。
}
while((int len = sc.read(xxx))==0){ ///保证NBIO下,read数据不会read空
// 做别的事。。。
}
while((int len = sc.write(xxx))==0){///保证NBIO下,write数据不会write空
// 做别的事。。。
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (sc != null) {
sc.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
Server服务端写法:
/**
* 关于:ServerSocketChannel
*/
public static void serverSocketChannel() {
ServerSocketChannel serverSocketChannel = null;
try {
///打开
serverSocketChannel = ServerSocketChannel.open();
///连接并开始监听TCP 9999端口
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
///TODO:可设置非阻塞状态(需要检查accept到的socketChannel是否为null)
serverSocketChannel.configureBlocking(false);
while (true) {
SocketChannel socketChannel =
serverSocketChannel.accept();
//TODO: 非阻塞时需要考虑返回的socketChannel对象是否为null
if(socketChannel != null){
//do something with socketChannel...
}
//do something with socketChannel...
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocketChannel != null)
try {
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
配合Selector,简化SocketChannel在非阻塞IO状态下的Null情况监测逻辑
/**
* 关于 选择器 和 SocketChannel 的配合使用
*/
public static void selectorAndSocketChannel(String fileName) {
SocketChannel sc1 = null;
SocketChannel sc2 = null;
SocketChannel sc3 = null;
try {
// 创建几个SocketChannel 通道
TODO: FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
sc1 = SocketChannel.open();
sc2 = SocketChannel.open();
sc3 = SocketChannel.open();
///TODO:非阻塞IO状态下,socketChannel就可以异步地执行read()、write()、connect()方法了
sc1.configureBlocking(false);
sc2.configureBlocking(false);
sc3.configureBlocking(false);
sc1.connect(new InetSocketAddress("http://jenkov.com", 80));
sc2.connect(new InetSocketAddress("http://jenkov.com", 80));
sc3.connect(new InetSocketAddress("http://jenkov.com", 80));
// 创建Selector
Selector selector = Selector.open();
// 注册channels
SelectionKey key1 = sc1.register(selector, SelectionKey.OP_READ);
SelectionKey key2 = sc2.register(selector, SelectionKey.OP_READ);
SelectionKey key3 = sc3.register(selector, SelectionKey.OP_READ);
// 持续监控selector的四个事件(接受、连接、读、写)是否就绪
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = (SelectionKey) keyIterator.next();
if (key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
///我的这个连接请求被服务端接受了
} else if (key.isConnectable()) {
// a connection was established with a remote server.
///已经连接上
} else if (key.isReadable()) {
// a channel is ready for reading
///可读数据
} else if (key.isWritable()) {
// a channel is ready for writing
///可写数据
}
}
keyIterator.remove();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (sc1 != null) {
sc1.close();
}
if (sc2 != null) {
sc2.close();
}
if (sc3 != null) {
sc3.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
(五)UDP通信 --- DatagramChannel
收发UDP数据包 的简单示例
/**
* 关于:DatagramChannel
* UDP 无连接网络协议
* 发送和接收的是数据包
*/
public static void datagramChannel() {
DatagramChannel datagramChannel = null;
try {
///打开
datagramChannel = DatagramChannel.open();
///连接并开始监听UDP 9999端口
datagramChannel.socket().bind(new InetSocketAddress(9999));
// 接收数据包(receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。 )
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
datagramChannel.receive(buf);
// 发送数据 send()
String sendMsg = "要发送的数据";
ByteBuffer sendBuf = ByteBuffer.allocate(48);
sendBuf.clear();
sendBuf.put(sendMsg.getBytes());
sendBuf.flip();
datagramChannel.send(sendBuf,new InetSocketAddress("xxxxx",80));
// TODO: 连接到特定的地址(锁住DatagramChannel ,让其只能从特定地址收发数据 因为UDP无连接,本身没有真正的连接产出)
datagramChannel.connect(new InetSocketAddress("jenkov.com", 80));
///连接后,也可以使用Channal 的read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证
} catch (IOException e) {
e.printStackTrace();
} finally {
if (datagramChannel != null)
try {
datagramChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
(六)NIO管道(Pipe)
首先,什么是NIO管道,下图可以看出其内部结构和功能特点:
NIO Pipe,是两个线程之间的单向连接通道(读下图可知)
Pipe类内部有两个成员属性,分别是:
Pipe.SinkChannel:数据入口通道
Pipe.SourceChannel:数据出口通道
整体原理:ThreadA中获取的数据通过SinkChannel传入(写入)管道,当ThreadB要读取ThreadA的数据,则通过管道的SourceChannel传出(读取)数据。
NIO Pipe原理图解
示例: 管道传输数据
/**
* 关于NIO管道(Pipe)
* 定义:2个线程之间的单向数据连接
*/
public static void aboutPipe(){
Pipe pipe=null;
try {
/// 打开管道
pipe = Pipe.open();
///TODO: 一、 向管道写入数据
/// 访问Pipe.sinkChannel,向Pipe写入数据
/// 首先,获取Pipe.sinkChannel
Pipe.SinkChannel sinkChannel = pipe.sink();
/// 然后,调用write(),开始写入数据
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()){
sinkChannel.write(buf);
}
// TODO: 二、读取管道中的数据
// 首先,获取Pipe.sourceChannel
Pipe.SourceChannel sourceChannel = pipe.source();
/// 读取数据到buffer
ByteBuffer buf2 = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf2);
} catch (IOException e) {
e.printStackTrace();
}
}
附录:java.nio包相关接口与类图一览
Buffer类图
java.nio.channels 相关接口关系图
java.nio.channels 相关类图
java.nio.charset 相关类图(这个包主要做不同编码格式的加解密等工作)
参考文章