2.1 Channel概述
Channel是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的(全双工),流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读、写或者同时用于读写操作。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。
NIO中通过Channel封装了对数据源的操作,通过Channel我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,Channel与文件描述符或者socket是一一对应的。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
与缓冲区不同,通道API主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
Channel是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做个比较,通道就像是流。所有数据都通过Buffer对象来处理 。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
Java NIO的通道类似流,但又有些不通:
- 既可以从通道中读取数据,也可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是从一个Buffer中写入。
2.2 Channel实现
下面是Java NIO中最重要的Channel实现:
- FileChannel:从文件中读写数据
- DatagramChannel:能通过UDP读写网络中的数据
- SocketChannel:能通过TCP读写网络中的数据
- ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
这如你所看到的,这些通道涵盖了UDP和TCP网络IO,以及文件IO。
2.3 FileChannel介绍和示例
FileChannel类可以实现常用的read、write以及scatter/gather操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。
方法 | 描述 |
---|
int read(BytesBuffer 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 p) | 设置此通道的文件位置 | long size() | 返回此通道的文件的当前大小 | FileChannel truncate(long s) | 将此通道的文件截取为给定大小 | void force(boolean metaData) | 强制将所有对此通道的文件更新写入到存储设备中 |
2.3.1 入门用例
读取文件内容
file.txt
this is nio!!!
java代码:
package com.study;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo1 {
public static void main(String[] args) throws Exception {
RandomAccessFile aFile = new RandomAccessFile("/Users/xxx/Desktop/NIO/file.txt", "rw");
FileChannel channel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buf);
while(bytesRead != -1){
System.out.println("读取了:" + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.println((char)buf.get());
}
buf.clear();
bytesRead = channel.read(buf);
}
aFile.close();
System.out.println("结束了");
}
}
控制台输出:
读取了:14
t
h
i
s
i
s
n
i
o
!
!
!
结束了
Process finished with exit code 0
2.4 FileChannel操作详解
2.4.1 打开FileChannel
在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例:
RandomAccessFile aFile = new RandomAccessFile("/Users/xxx/Desktop/NIO/file.txt", "rw");
FileChannel channel = aFile.getChannel();
2.4.2 从FileChannel读取数据
调用多个read()方法之一从FileChannel中读取数据。如:
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buf);
首先,分配一个Buffer。从FileChannel中读取的数据被读到Buffer中。然后调用FileChannel.read()方法。该方法将数据从FIleChannel读取到Buffer中。read()方法返回int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。
2.4.3 向FileChannel写数据
使用FileChannel.write()方向向FileChannel写数据,该方法的参数是一个Buffer。如:
package com.study;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo2 {
public static void main(String[] args) throws Exception {
RandomAccessFile aFile = new RandomAccessFile("/Users/liyabin01/Desktop/计算机基础/面试/NIO/file_w.txt", "rw");
FileChannel channel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
String newData = "data write";
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()){
channel.write(buf);
}
channel.close();
}
}
注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。
2.4.4 关闭FileChannel
用完FileChannel后必须将其关闭。
channel.close();
2.4.5 FileChannel的position方法
有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用postion() 方法获取FileChannel的当前位置。也可以通过调用position(long pos) 方法设置FileChannel的当前位置。
这里有两个例子:
long pos = channel.position();
channel.position(pos + 123);
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1(文件结束标志)。
如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致『文件空洞』,磁盘上物理文件中写入的数据间有空隙。
2.4.6 FileChannel的size方法
FileChannel实例的size()方法讲返回该实例所关联文件的大小。如:
long fileSize = channel.size();
2.4.7 FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将制定长度后面的部分删除。如:
channel.truncate(1024);
这个例子截取文件的前1024个字节。
2.4.8 FileChannel的force方法
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。处于性能方面的考虑,操作系统将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。
force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上
2.4.9 FileChannel的transferTo和transferFrom方法
通道之间的数据传输:
如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另一个channel。
(1)transferFrom()方法
FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中。下面是一个FileChannel完成文件间的复制的例子:
示例:
01.txt:
this is from txt 01;
Java源码:
package com.study;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo3 {
public static void main(String[] args) throws Exception {
RandomAccessFile aFile = new RandomAccessFile("/Users/xxx/NIO/01.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("/Users/xxx/NIO/02.txt", "rw");
FileChannel toChannel = bFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
aFile.close();
bFile.close();
System.out.println("over!!");
}
}
方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于count个字节,则传输的字节数要小于请求的字节数。此处要注意,在SocketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有(Count个字节)全部传输到FileChannel中。
(2)transferTo()方法
transferTo()方法将数据从FileChannel传输到其他的channel中。
下面是一个tansferTo()方法的例子:
package com.study;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
public class FileChannelDemo4 {
public static void main(String[] args) throws Exception {
RandomAccessFile aFile = new RandomAccessFile("/Users/xxx/NIO/01.txt", "rw");
FileChannel fromChannel = aFile.getChannel();
RandomAccessFile bFile = new RandomAccessFile("/Users/xxx/NIO/03.txt", "rw");
FileChannel toChannel = bFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
aFile.close();
bFile.close();
System.out.println("over!!");
}
}
2.5 Socket通信
(1)新的Socket通信类可以运行非阻塞模式并且是可选择的,可以激活打程序(如网络服务器和中间件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个Socket连接使用一个线程的必要了,也避免了管理大量线程所需要的上下文切换开销。借助新的NIO类,一个或几个线程就可以管理成千上百的活动Socket连接了,并且只有很少甚至可能没有性能损失。所有的Socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi 包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择。
(2)请注意DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
(3)在我们具体讨论每一种socket通信前,您应该了解socket和socket通道之间的关系 。通道是一个连接I/O服务导管并提供与该服务交互的方法。就某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API ,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用 。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel )在被实例化时都会创建一个对等socket对象。这些是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现在都有getChannel()方法。
(4)要把一个socket通道置于非阻塞模式,我们要依靠所有的socket通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相联的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
设置或者重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用isBlocking()方法来判断某个socket通道当前处于哪种模式。
AbstractSelectableChannel.java 中实现的configureBlocking()方法如下:
public final SelectableChannel configureBlocking(boolean block)
throws IOException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if (blocking == block)
return this;
if (block && haveValidKeys())
throw new IllegalBlockingModeException();
implConfigureBlocking(block);
blocking = block;
}
return this;
}
|