IO
BIO
通常在进行同步IO操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户端请求。这种模式带来的一个问题就是线程数量剧增,大量的线程会增大服务器开销。大多数的实现为了避免这个问题,都采用了线程池,并设置线程池的最大数量。但是 这带来了一个新的问题,如果线程池中有100个线程,而且有100个用户都在进行大文件下载,会导致第101个用户的请求无法及时处理,即便是下载几KB的文件。
同步阻塞IO(传统阻塞型IO),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程来进行处理,如果这个连接不做任何事情就会造成不必要的线程开销。

NIO
NIO中非阻塞IO采用了基于Reactor模式的工作方式,IO调用不会被阻塞,相反是注册感兴趣的特定IO事件,如可读数据到达。新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞IO的核心对象是Selector,Selector就是注册各种IO事件的地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件。
同步非阻塞IO,服务器实现模式为一个线程可以处理多个连接请求,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理。

从上图中可以看出,当有读或者写等任何注册的事件发生时,可以从Selector中获取到相应的SelectionKey,同事同SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获取到客户端发过来的数据。
非阻塞值的是IO事件本身不阻塞,但是获取IO事件的select()方法是需要阻塞等待的。
区别是阻塞的IO会阻塞在IO操作上,NIO阻塞在事件的获取上,没有事件就没有IO,从高层次看IO就不阻塞了。也就是说只有IO发生我们才评估IO是否阻塞,但是select()阻塞的时候IO还没有发生,谈何IO阻塞呢?NIO的本质是延迟IO操作到真正发生IO的时候,而不是以前的只要IO流打开了就一直等待IO操作。


Channel简述
channel是一个通道,可以通过他读取和写入数据,它就像水管一样,网络数据通过Channel读取和写入,通道和流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutoutStream的子类),而且通道可以用于读、写或者是同时用于读写。因为通道是全双工的,所以他可以比流更好的映射底层操作系统的API。
NIO中通过channel封装了对数据源的操作,通过channel我们可以操作数据源,但是又不必关心数据源的具体物理结构。这个数据源可能是多重的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel与文件描述符或者socket是一一对应的,Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

与缓冲区不同,通道API主要由接口指定。不同的操作系统上通道实现会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然的,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
Channel是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做个对比。通道就像是流。所有数据都通过Buffer对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样的您不会直接从通道中读取字节,而是将数据从通道读入到缓冲区,然后再从缓冲区获取字节。
Java NIO的通道类似于流,但是又有些不同:
- 既可以从通道中读取数据,又可以写数据到通道中。但是流的读写通常是单向的。
- 通道可以异步地进行读写。
- 通道中的数据总是要先读取到一个Buffer,或者总是要先从一个Buffer中进行写入。

Channel的实现
NIO中最重要的Channel的实现:
- FileChannel 从文件中读写数据
- DatagramChannel 能通过UDP读写网络中的数据
- SocketChannel 能通过TCP读写网络中的数据
- ServerSocketChannel 可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
如上可以看出,这些通道涵盖了UDP、TCP网络IO以及文件IO
FileChannel的介绍和示例
FileChannel类可以实现常用的read、write以及scatter、gather操作,同时他也提供了很多专门用于文件的新方法。这些方法中的很多都是我们熟悉的文件操作。
| 方法 | 描述 |
|---|---|
| int read(ByteBuffer dst) | 从channel中读取数据到ByteBuffer中 |
| long read(ByteBuffer[] dsts) | 从Channel中的数据“分散到”ByteBuffer[] |
| int write(ByteBuffer src) | 将ByteBuffer中的数据写入到Channel |
| long write(ByteBuffer[] srcs) | 将ByteBuffer[]中的数据“聚集”到Channel |
| long position() | 返回此通道的文件位置 |
| FileChannel position(long newPosition) | 设置此通道的文件位置 |
| long size() | 返回此通道文件的当前大小 |
| FileChannel truncate(long size) | 将此通道文件截取为指定大小 |
| void force(boolean metaData) | 强制将所有对此通道文件更新写入到存储设备中 |
Buffer通常的操作
- 将数据写入到缓冲区
- 调用buffer.flip()反转读写模式
- 从缓冲区中读取数据
- 调用buffer.clear()或buffer.compact()清除缓冲区内容
public class FileChannelReadTest {
public static void main(String[] args) throws Exception {
// 创建一个FileChannel
RandomAccessFile accessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/file_createTest_01.txt","rw");
FileChannel channel = accessFile.getChannel();
// 创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到buffer中
// bytesRead = -1 表示文件读取结束
int bytesRead = channel.read(buffer);
while (bytesRead != -1) {
System.out.println("读取了 : " + bytesRead);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
accessFile.close();
System.out.println("=== 操作结束 ===");
}
}
FileChannel操作详解
1、打开FileChannel
在使用FileChannel之前,必须先打开它。
但是我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或者RandomAccessFile来获取一个FileChannel示例。如下:
// 创建一个FileChannel
RandomAccessFile accessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/file_createTest_01.txt","rw");
FileChannel channel = accessFile.getChannel();
2、从FileChannel中读取数据
先将管道的数据读到buffer中,然后再从Buffer中读取数据
调用多个read()方法之一从FileChannel中读取数据。如:
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到buffer中
// bytesRead = -1 表示文件读取结束
int bytesRead = channel.read(buffer);
首先,分配一个Buffer并初始化Buffer缓冲区的大小。从FileChannel中读取的数据将被读到Buffer中。
然后调用FileChannel.read()方法。这个方法将数据从FileChannel读取到Buffer中。
read()方法返回值表示有多少字节被读到了Buffer中。如果返回 -1 表示到了文件末尾。
3、向FileChannel写数据
先将内容写入到Buffer,然后再从Bufer中写入到管道
public class FileChannelWriteTest {
public static void main(String[] args) throws Exception {
// 获取文件流
RandomAccessFile accessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/file_write_test.txt","rw");
// 打开FileChannel
FileChannel channel = accessFile.getChannel();
//创建Buffer对象
ByteBuffer buffer = ByteBuffer.allocate(1024);
String newData = "write file test h哈哈哈";
buffer.clear();
buffer.put(newData.getBytes());
buffer.flip();
// filechannel完成写
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// 关闭Channel
channel.close();
}
}
注意FileChannel.write()实在wile循环中调用的。因为无法保证write()方法能一次性向FileChannel写多多少字节,因此需要重复调用write()方法,知道Buffer中没有尚未写入通道中的数据字节。
4、关闭FileChannel
用完FileChannel后必须将其关闭。
// 关闭Channel
channel.close();
5、FileChannel中的position方法
有时可能需要在FileChannel的某个特定位置进行数据的读写操作。可以通过调用position()方法获取FileChannel的当前位置。也可以通过调用position(long pos)方法设置FileChannel的当前位置。
一下是获取到FileChannel的当前位置,
FileChannel channel = accessFile.getChannel();
long position = channel.position();
以下就是在获取到Channel后通过position(1)方法设置了当前管道从第一个字节后开始读取内容。
FileChannel channel = accessFile.getChannel();
channel = channel.position(1);
注意以下会产生一个问题:
position相当于获取到channel全部的字节大小,如果在文件结束符之后向通道中写数据,文件将撑大到设置的位置并写入数据。这将会导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
FileChannel channel = accessFile.getChannel();
long position = channel.position();
channel = channel.position(position + 123);
6、FileChannel的size方法
返回该FileChannel示例所关联的文件大小。如:
FileChannel channel = accessFile.getChannel();
long size = channel.size();
7、FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将会将指定长度后面的部门进行删除。如:
FileChannel channel = accessFile.getChannel();
channel.truncate(10);
这个例子将会截取文件前10个字节。
8、FileChannel的force方法
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写入到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上,为了保证这一点,需要使用该方法。
force()方法有一个boolean参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
9、FileChannel的transferTo和transferFrom方法
通道之间的数据传输
如果两个通道中有一个是FileChannel,那么我们可以将数据从一个Channel传输到另外一个Channel。
- transferFrom方法
参数中的channel传给调用channel
可以将数据从源通道传输到FileChannel中。
public class FileChannelTransferFromTest {
public static void main(String[] args) throws Exception {
RandomAccessFile fromAccessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/transferFrom_01.txt","rw");
FileChannel fromChannel = fromAccessFile.getChannel();
RandomAccessFile toAccessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/transferTo_01.txt","rw");
FileChannel toChannel = toAccessFile.getChannel();
long position = 0;
long count = fromChannel.size();
// 将fromChannel的数据传输到toChannel
toChannel.transferFrom(fromChannel,position,count);
fromAccessFile.close();
toAccessFile.close();
System.out.println("over!!!");
}
}

- transferTo() 方法
调用channel传给参数channel
transferTo()方法将数据从FileChannel中传输到其他channel中
public class FileChannelTransferFromTest {
public static void main(String[] args) throws Exception {
RandomAccessFile fromAccessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/transferFrom_01.txt","rw");
FileChannel fromChannel = fromAccessFile.getChannel();
RandomAccessFile toAccessFile
= new RandomAccessFile("/Users/ClearRain/Downloads/transferTo_01.txt","rw");
FileChannel toChannel = toAccessFile.getChannel();
long position = 0;
long count = fromChannel.size();
// 将fromChannel数据传输到toChannel
fromChannel.transferTo(position,count,toChannel);
fromAccessFile.close();
toAccessFile.close();
System.out.println("over!!!");
}
}
AIO
异步非阻塞IO

浙公网安备 33010602011771号