Java NIO
Java NIO 简介
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
Java NIO与IO的主要区别
通道(Channel)与缓冲区(Buffer)
Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
缓冲区(Buffer)
缓冲区(Buffer):一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。
Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:
1.ByteBuffer
2.CharBuffer
3.ShortBuffer
4.IntBuffer
5.LongBuffer
6.FloatBuffer
7.DoubleBuffer
上述 Buffer 类 他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个Buffer对象:
static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象
Buffer 中的重要概念
1.容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
2.限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
3.位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
4.标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的位置position,之后可以通过调用 reset() 方法恢复到这个position。
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
Buffer的常用方法
Buffer 所有子类提供了两个用于数据操作的方法:get() 与 put() 方法
获取 Buffer 中的数据
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)
放入数据到 Buffer 中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)
String b="asbcd";
ByteBuffer a=ByteBuffer.allocate(10);
a.put(b.getBytes());
a.flip();
byte[] dst=new byte[a.limit()];
System.out.println(a.get(dst));
System.out.println(new String(dst));
直接缓冲区与非直接缓冲区
非直接缓冲区:通过allocate( )方法分配缓冲区,将缓冲区建立在JVM的内存中。
直接缓存区:通过allocateDirect( )方法分配直接缓存区,将缓存区建立在物理内存中。可以提高效率。
通道(Channel)
通道(Channel):由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与Buffer 进行交互。
Java 为 Channel 接口提供的最主要实现类如下:
1.FileChannel:用于读取、写入、映射和操作文件的通道。
2.DatagramChannel:通过 UDP 读写网络中的数据通道。
3.SocketChannel:通过 TCP 读写网络中的数据。
4.ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
获取通道的方式
对支持通道的对象调用getChannel() 方法。支持通道的类如下:
1.FileInputStream
2.FileOutputStream
3.RandomAccessFile
4.DatagramSocket
5.Socket
6.ServerSocket
使用 Files 类的静态方法newByteChannel() 获取字节通道。或者通过通道的静态方法open() 例如:FileChannel.open打开并返回指定通道。
FileChannel 的常用方法
通道的数据传输
getChannel( )方法获取的通道:
操作通道中的数据到缓存中再写出
FileInputStream fis=new FileInputStream("a.txt");
FileOutputStream fos=new FileOutputStream("c1.txt");
FileChannel channel = fis.getChannel();
FileChannel channel2 = fos.getChannel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
while (channel.read(allocate)!=-1) {
allocate.flip();
channel2.write(allocate);
allocate.clear();
}
channel.close();
channel2.close();
fis.close();
fos.close();
通过通道中的直接缓存区:
操作缓存中的数据到数组中再写出
FileChannel in=null;
FileChannel out=null;
try {
in = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
out = FileChannel.open(Paths.get("c.txt"), StandardOpenOption.WRITE,
StandardOpenOption.READ, StandardOpenOption.CREATE);
MappedByteBuffer map = in.map(MapMode.READ_ONLY, 0, in.size());
MappedByteBuffer map2 = out.map(MapMode.READ_WRITE, 0, in.size());
byte[] dst = new byte[map.limit()];
map.get(dst);
map2.put(dst);
} catch (Exception e) {
// TODO: handle exception
}finally {
if (in!=null) {
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (out!=null) {
try {
out.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
直接通过通道:
FileChannel in= FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel out=FileChannel.open(Paths.get("c.txt"), StandardOpenOption.WRITE,StandardOpenOption.READ, StandardOpenOption.CREATE);
in.transferTo(0, in.size(), out);
in.close();
out.close();
分散(Scatter)和聚集(Gather)
分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散”到多个 Buffer 中。
聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集”到 Channel。
FileInputStream fis=new FileInputStream("a.txt");
FileOutputStream fos=new FileOutputStream("c1.txt");
FileChannel channel = fis.getChannel();
FileChannel channel2 = fos.getChannel();
ByteBuffer allocate = ByteBuffer.allocate(2);
ByteBuffer allocate1 = ByteBuffer.allocate(1024);// 多个缓存区加在一起必须足够大,否则可能一次装不下
ByteBuffer [] a=new ByteBuffer[] {allocate,allocate1};
channel.read(a);
for (ByteBuffer byteBuffer : a) {
byteBuffer.flip();
}
channel2.write(a);
channel.close();
channel2.close();
fis.close();
fos.close();
字符集
编码,解码
Charset a=Charset.forName("utf-8");
CharsetEncoder newEncoder = a.newEncoder(); // 获取编码器
CharsetDecoder newDecoder = a.newDecoder(); // 获取解码器
CharBuffer in = CharBuffer.allocate(1024);
in.put("范德萨氛围");
in.flip();
ByteBuffer encode = newEncoder.encode(in); // 编码
CharBuffer decode = newDecoder.decode(encode); // 解码
System.out.println(decode.toString());
NIO的非阻塞式网络通信
阻塞与非阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
选择器(Selector)
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个Channel。Selector 是非阻塞 IO 的核心。
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
Selector selector=Selector.open();
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型(可使用 SelectionKey 的四个常量表示):
1.读 : SelectionKey.OP_READ
2.写 : SelectionKey.OP_WRITE
3.连接 : SelectionKey.OP_CONNECT
4.接收 : SelectionKey.OP_ACCEPT
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
dc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
SelectionKey
SelectionKey selectionKey = dc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
SelectionKey:表示 通道SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
Selector 的常用方法
代码演示非阻塞式网络通信
TCP:
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道,就像标准IO中 的ServerSocket一样。
服务器端:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocketChannel open = ServerSocketChannel.open();
open.configureBlocking(false);
FileChannel out=FileChannel.open(Paths.get("c1.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
open.bind(new InetSocketAddress(9898));
// 获取选择器
Selector open2 = Selector.open();
// 将通道注册到选择器,指定监听接收事件
open.register(open2, SelectionKey.OP_ACCEPT);
// 轮询获取选择器上已经准备就绪的事件
while (open2.select()>0) {
// 获取当前选择器中所有注册的选择键(已就绪的监听事件)
Set<SelectionKey> selectedKeys = open2.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
// 获取准备就绪的事件
SelectionKey next = iterator.next();
// 判断具体是什么事件准备就绪
if (next.isAcceptable()) {
SocketChannel accept = open.accept();
// 切换非阻塞模式
accept.configureBlocking(false);
// 将通道注册到选择器上
accept.register(open2, SelectionKey.OP_READ);
}else if (next.isReadable()) {
// 获取当前选择器上就读绪状态的通道
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer buf=ByteBuffer.allocate(1024);
while (channel.read(buf)!=-1) {
buf.flip();
out.write(buf);
buf.clear();
}
}
iterator.remove(); // 取消选择键SelectionKey
}
}
}
}
客户端:
public class ApplicationMain {
public static void main(String[] args) throws IOException {
// 获取通道
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
open.configureBlocking(false); // 切换为非阻塞
FileChannel in=FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer allocate = ByteBuffer.allocate(1024);
while (in.read(allocate)!=-1) {
allocate.flip();
open.write(allocate);
allocate.clear();
}
in.close();
open.close();
}
}
UDP:
Java NIO中的DatagramChannel是一个能收发UDP包的通道。
接收端:
public class Server {
public static void main(String[] args) throws IOException {
DatagramChannel dc=DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector=Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while (selector.select()>0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
if (next.isReadable()) {
ByteBuffer buffer=ByteBuffer.allocate(1024);
dc.receive(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
}
}
iterator.remove();
}
}
}
发送端:
public class ApplicationMain {
public static void main(String[] args) throws IOException {
DatagramChannel dc=DatagramChannel.open();
ByteBuffer buffer=ByteBuffer.allocate(1024);
System.out.println("shuru");
Scanner scan=new Scanner(System.in);
while (scan.hasNext()) {
String next = scan.next();
buffer.put((new Date().toString()+next).getBytes());
buffer.flip();
dc.send(buffer, new InetSocketAddress("127.0.0.1", 9898));
buffer.clear();
}
dc.close();
}
}
管道 (Pipe)
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
public static void main(String[] args) throws IOException {
// 获取管道
Pipe open = Pipe.open();
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("饭团团".getBytes());
buf.flip();
SinkChannel sink = open.sink();
// 写数据
sink.write(buf);
SourceChannel source = open.source();
buf.flip();
// 读数据
int read = source.read(buf);
System.out.println(new String(buf.array(),0,read));
source.close();
sink.close();
}
Path,Paths,Files
Path 与 Paths
java.nio.file.Path 接口代表一个平台无关的平台路径,描述了目
录结构中文件的位置。
Paths 提供的 get() 方法用来获取 Path 对象:
Path get(String first, String … more) : 用于将多个字符串串连成路径。
Path 常用方法:
1.boolean endsWith(String path) : 判断是否以 path 路径结束
2.boolean startsWith(String path) : 判断是否以 path 路径开始
3.boolean isAbsolute() : 判断是否是绝对路径
4.Path getFileName() : 返回与调用 Path 对象关联的文件名
5.Path getName(int idx) : 返回的指定索引位置 idx 的路径名称
6.int getNameCount() : 返回Path 根目录后面元素的数量
7.Path getParent() :返回Path对象包含整个路径,不包含 Path 对象指定的文件路径
8.Path getRoot() :返回调用 Path 对象的根路径
9.Path resolve(Path p) :将相对路径解析为绝对路径
10.Path toAbsolutePath() : 作为绝对路径返回调用 Path 对象
11.String toString() : 返回调用 Path 对象的字符串表示形式
Files 类
java.nio.file.Files 用于操作文件或目录的工具类。
Files常用方法:
1.Path copy(Path src, Path dest, CopyOption … how) : 文件的复制
2.Path createDirectory(Path path, FileAttribute<?> … attr) : 创建一个目录
3.Path createFile(Path path, FileAttribute<?> … arr) : 创建一个文件
4.void delete(Path path) : 删除一个文件
5.Path move(Path src, Path dest, CopyOption…how) : 将 src 移动到 dest 位置
6.long size(Path path) : 返回 path 指定文件的大小
Files常用方法:用于判断
1.boolean exists(Path path, LinkOption … opts) : 判断文件是否存在
2.boolean isDirectory(Path path, LinkOption … opts) : 判断是否是目录
3.boolean isExecutable(Path path) : 判断是否是可执行文件
4.boolean isHidden(Path path) : 判断是否是隐藏文件
5.boolean isReadable(Path path) : 判断文件是否可读
6.boolean isWritable(Path path) : 判断文件是否可写
7.boolean notExists(Path path, LinkOption … opts) : 判断文件是否不存在
8.public static < A extends BasicFileAttributes > A readAttributes(Path path,Class< A > type,LinkOption… options) : 获取与 path 指定的文件相关联的属性。
Files常用方法:用于操作内容
1.SeekableByteChannel newByteChannel(Path path, OpenOption…how) : 获取与指定文件的连接,how 指定打开方式。
2.DirectoryStream newDirectoryStream(Path path) : 打开 path 指定的目录。
3.InputStream newInputStream(Path path, OpenOption…how):获取 InputStream 对象。
4.OutputStream newOutputStream(Path path, OpenOption…how) : 获取 OutputStream 对象。