Java NIO概述
Java共支持3种网络编程IO模式:BIO,NIO,AIO
NIO它是Java1.4引入的一个新的IO API,可以替代标准的Java IO API。NIO支持面向缓冲区的,基于通道的ID操作,NIO将以更加高效的方式进行文件的读写操作
BIO就是你教小孩写作业,他遇到一个不会的就卡住就来问你一次,因为要辅导作业导致你无法再做其他事情。
NIO就是你教小孩写作业,他遇到一个不会的先空着慢慢做,然后继续做下一题,最后做完了再等你去检查作业。
AIO就是你教小孩写作业,他遇到一个不会的先空着慢慢想,然后继续做下一题,最后做完了还会自己检查对错再告诉你结果。
1.1 同步阻塞IO(BIO)
同步:发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
传统服务器会为每一个客户端请求建立一个线程,由于线程单独负责一个请求,这种模式会带来线程数量剧增,消耗服务器的资源,为了规避这个问题,都采用线程池模型,并设置最大线程池数量,但是也会有新的问题,比如超过线程池设置的最大数量,会导致多的请求分配不到线程无法处理。
1.2 异步非阻塞IO(AIO)
异步:发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发
发起请求操作之后不用管了,这个任务会交给操作系统完成,还会自动告诉操作结果
1.3 同步非阻塞IO(NIO)
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
它是采用了基于Reactor模式的工作方式,IO调用不会被阻塞,是注册感兴趣的特定I/O事件,在发生特定的事件的时候再通知我们去处理,本质就是延迟IO的操作,直到真正发生IO的时候才执行。
Java NIO由以及几个核心部分组成:Channels
、Buffers
、Selectors
Channels(通道)
和IO中的stream流差不多是一个等级的,stream是单向的,要么读要么写,主要实现有inputstream、outputstream等,而Channel是双向的能读能写,主要实现有:FileChannle、DatagramChannel、SocketChannle和ServerSocketChannel
NIO中封装了对数据源的操作,通过Channle可以操作数据源,不用关心数据源的具体结构。
Channel是一个对象可以用来读取写入数据,每个 channel 对应一个 buffer缓冲区,但它不是直接读写,所有数据都通过Buffer对象来处理,写数据先写入Buffer、读数据先读Buffer。从通道读取数据到缓冲流,从缓冲区写数据到通道
Buffers(缓冲区)
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。
Selectors(选择器)
通过一个Selectors去监听多个Channels,当通道中有感兴趣的事情发生就会告诉我们,我们再进行执行。从Selectors中获得响应的key,然后在key里面找到事件具体的SelectorChannel以获得客户端发来的数据
NIO-Channel
NIO的通道类似于流,既可以写入数据到通道又可以从通道中读取数据,但流的读写是单向的,通道可以异步读写。但总是要先读到一个buffer中,或者从buffer中写入
Channel实现
- FileChannel:文件通道,用于文件的读和写
- DatagramChannel:用于 UDP 连接的接收和发送
- SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
- ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求
FileChannel 介绍
FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法
从 FileChannel 读取数据
@Test
public void test() throws IOException {
//通过RandomAccessFile来获取一个FileChannel 实例
RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//分配48字节的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
//从 FileChannel 中读取数据
int bytesRead = inChannel.read(buf);
//关闭FileChannel
inChannel.close();
System.out.println(bytesRead);
}
从 FileChannel 写入数据
@Test
public void test() throws IOException {
RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
FileChannel inChannel = aFile.getChannel();
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf1 = ByteBuffer.allocate(48);
//往ByteBuffer里面写入数据
buf1.put(newData.getBytes());
buf1.flip();
//直到Buffer 中已经没有尚未写入通道的字节
while(buf1.hasRemaining()) {
inChannel.write(buf1);
}
inChannel.close();
}
其他方法
- position:在 FileChannel 的某个特定位置进行数据的读/写操作
- size:将返回FileChannel 实例所关联文件的大小
- truncate:截取一个文件指定长度后面的部分将被删除
- force:操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用 该方法
- transferFrom:从源通道传输到 FileChannel 中
- transferTo():从 FileChannel 传输到其他的 channel 中
- Scatter(分散):将从 Channel 中读取的数据分散到多个 Buffer 中
- Gather (聚集):将多个 Buffer 中的数据聚集后发送到 Channel
SocketChannel 介绍
SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在 socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了传统的单向流 API,,Channel同时支持读写。socket 通道类主要分为 DatagramChannel、SocketChannel 和 ServerSocketChannel,
ServerSocketChannel
ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行
public static final String GREETING = "Hello java nio.\r\n";
public static void main(String[] argv) throws Exception {
int port = 1234; // default
if (argv.length > 0) {
port = Integer.parseInt(argv[0]);
}
ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
//打开 ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(port));
ssc.configureBlocking(false);
//监听新的连接
while (true) {
System.out.println("Waiting for connections");
//监听新的连接 accept方法会一直阻塞直到有新连接到达
SocketChannel sc = ssc.accept();
//判断为null直接返回,可以将阻塞变为非阻塞模式
if (sc == null) {
System.out.println("null");
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
buffer.rewind();
sc.write(buffer);
sc.close();
}
}
}
由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。使用对等 ServerSocket 的 API来根据需要设置其他的 socket 选项
accept()方法会一直阻塞直到有新连接到达,可以设置成非阻塞模式。在非阻塞模式下accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null
SocketChannel
SocketChannel 是一个连接到 TCP 网络套接字的通道
- SocketChannel 是用来连接 Socket 套接字
- SocketChannel 主要用途用来处理网络 I/O 的通道
- SocketChannel 是基于 TCP 连接传输
- SocketChannel 实现了可选择通道,可以被多路复用的
@Test
public void test4() throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
socketChannel.isOpen(); // 测试 ocketChannel是否为 open 状态
socketChannel.isConnected(); //测试SocketChannel是否已经被连接
socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接
socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接
socketChannel.configureBlocking(false);//false 表示非阻塞,true 表示阻塞
//设置socket套接字的相关参数
socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");
}
DatagramChannel 介绍
每一个 DatagramChannel 对象有一个关联的 DatagramSocket 对象,SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)。DatagramChannel 可以发送单独的数据报给不同的目的地址,DatagramChannel 对象也可以接收来自任意地址的数据包。 每个到达的数据报都含有关于它来自何处的信息(源地址)
接收数据
DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(10086));
ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);
发送数据
DatagramChannel server = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086));
示例
客户端发送,服务端接收的例子
/*DatagramChannel发包*/
@Test
public void sendDatagram() throws IOException, InterruptedException {
DatagramChannel sendChannel = DatagramChannel.open();
InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1", 9999);
while (true) {
sendChannel.send(ByteBuffer.wrap("发包".getBytes("UTF-8")), sendAddress);
System.out.println("发包端发包");
Thread.sleep(1000);
}
}
/**
* DatagramChannel接收包
* */
@Test
public void receive() throws IOException {
DatagramChannel receiveChannel = DatagramChannel.open();
InetSocketAddress receiveAddress = new InetSocketAddress(9999);
receiveChannel.bind(receiveAddress);
ByteBuffer receiveBuffer = ByteBuffer.allocate(512);
while (true) {
receiveBuffer.clear();
SocketAddress sendAddress = receiveChannel.receive(receiveBuffer);
receiveBuffer.flip();
System.out.print(sendAddress.toString() + " ");
System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));
}
}
NIO-Buffer
Buffer实现
Buffer有三个属性:capacity,position 和 limit
- capacity:作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”。只能往里写capacity 个 byte、long,char 等类型,常写48等等,表示48个ByteBuffer块,满了需要清空才能写入
- position :写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1。当调用ByteBuffer.flip()position会被清为0
- limit:
- 写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于Buffer 的 capacity
- 读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据
Buffer关键实现有:ByteBuffer、Charbuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,分贝对应byte、char、double、float、int、long、short
Buffer写入数据
分配
要想获得一个 Buffer 对象首先要进行分配,一个分配 48 字节 capacity 的 ByteBuffer 的例子
ByteBuffer buf = ByteBuffer.allocate(48);
向 Buffer 中写数据
1、从 Channel 写到 Buffer
int bytesRead = inChannel.read(buf);
2、通过 Buffer 的 put()方法写到Buffer 里
buf.put(127);
flip
flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并将 limit 设置成之前 position 的值,就是标记当前读到的位置
最后效果
RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
//从Channel 写到 Buffer
int bytesRead = inChannel.read(buf);
inChannel.close();
System.out.println(bytesRead);
Buffer读数据
RandomAccessFile aFile = new RandomAccessFile("/Users/yanglingcong/Desktop/IdeaProjects/design-pattern/Builder-pattern-02/src/test/temp.txt", "rw");
FileChannel inChannel = aFile.getChannel();
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf1 = ByteBuffer.allocate(48);
//往ByteBuffer里面写入数据
buf1.put(newData.getBytes());
buf1.flip();
//直到Buffer 中已经没有尚未写入通道的字节
while (buf1.hasRemaining()) {
inChannel.write(buf1);
}
inChannel.close();
NIO-Selector
Selector介绍
Selector 一般称 为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销
SelectableChannel 介绍
- 不是所有的 Channel 都可以被 Selector 复用的。比方说,FileChannel 就不能被选择器复用。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复用,否则不能
- SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有 socket 通道,都继承了 SelectableChannel 类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继承 SelectableChannel,因此是不是可选通道
- 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的
Selector使用
@Test
public void testSelector() throws IOException {
// 1、获取 Selector 选择器
Selector selector = Selector.open();
//2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(9999));
// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。这意味着,FileChannel 不能与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以
一个通道,并没有一定要支持所有的四种操作。比如服务器通道ServerSocketChannel 支持 Accept 接受操作,而 SocketChannel 客户端通道则不支持。可以通过通道上的 validOps()方法,来获取特定通道下所有支持的操作集合
查询已经就绪的通道
Set selectionKeys = selector.selectedKeys();
Iterator keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = (SelectionKey) keyIterator.next();
if(key.isAcceptable()){
}else if (key.isConnectable()){
}else if(key.isReadable()){
}else if(key.isWritable()){
}
keyIterator.remove();
}
总结NIO步骤
- 创建 Selector 选择器
- 创建 ServerSocketChannel 通道,并绑定监听端口
- 设置 Channel 通道是非阻塞模式
- 把 Channel 注册到 Socketor 选择器上,监听连接事件
- 调用 Selector 的 select 方法(循环调用),监测通道的就绪状况
- 调用 selectKeys 方法获取就绪 channel 集合
- 遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
- 根据业务,决定是否需要再次注册监听事件,重复执行第3步操作
NIO-Pipe
Java NIO 管道是 2 个线程之间的单向数据连接。Pipe 有一个 source 通道和一个 sink通道。数据会被写到 sink 通道,从 source 通道读
示例
@Test
public void testPipe() throws IOException {
//1、获取通道
Pipe pipe = Pipe.open();
//2、获取 sink 管道,用来传送数据
Pipe.SinkChannel sinkChannel = pipe.sink();
//3、申请一定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("atguigu".getBytes()); byteBuffer.flip();
//4、sink 发送数据
sinkChannel.write(byteBuffer);
//5、创建接收 pipe 数据的 source 管道
Pipe.SourceChannel sourceChannel = pipe.source();
//6、接收数据,并保存到缓冲区中
ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
int length = sourceChannel.read(byteBuffer2);
System.out.println(new String(byteBuffer2.array(), 0, length));
sourceChannel.close(); sinkChannel.close();
}
NIO-FileLock
文件锁是在多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件, 或者程序都只能读此文件,这就解决了同步问题
文件锁是进程级别的,不是线程级别的。例如两个应用程序不能去修改同一个文件,但是同一个进程中的多个线程可以修改
文件锁又分为:排他锁和共享锁
NIO-其他
Path
Java Path 接口是 Java NIO 更新的一部分,Java Path 接口是在 Java7 中添加到 Java NIO 的,Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。java.nio.file.Path 接口类似于 java.io.File 类,但是有一些差别。可以使用 Path 接口来替换 File 类的使用
Path path = Paths.get("d:\\atguigu\\001.txt");
Files
Files 类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法
1、Files.createDirectory()方法,用于根据 Path 实例创建一个新目录
2、Files.copy(),从一个路径拷贝一个文件到另外一个目录
3、Files.move()用于将文件从一个路径移动到另一个路径
4、Files.delete()方法可以删除一个文件或者目录
5、Files.walkFileTree()方法包含递归遍历目录树功能,将 Path 实例和 FileVisitor作为参数。Path 实例指向要遍历的目录,FileVisitor 在遍历期间被调用
AsynchronousFileChannel
在 Java 7 中,Java NIO 中添加了 AsynchronousFileChannel,也就是是异步地将数据写入文件