通道是NIO的一个主要创新,用于在Buffer与通道另一端之间进行有效的数据传输
I/O可以分为文件IO和流IO,那么通道对应的就可以分为文件通道(FileChannel)和流通道(流通道就是套接字通道,SocketChannel),所以NIO中有四种通道实现类:
- FileChannel:文件通道,用于操作文件I/O
- ServerSocketChannel:服务器套接字通道,用于TCP连接响应客户端连接
- SocketChannel:套接字通道,用于TCP协议,客户端连接服务器后,服务器和客户端都会有一个SocketChannel,就可以互相发送数据了
- DatagramChannel:数据报通道,用于UDP协议
一、打开一个通道的方法如下:
//打开一个文件通道,指定为可读写
RandomAccessFile raf = new RandomAccessFile("d:/test.txt", "rw");
FileChannel fc = raf.getChannel();
// 打开一个服务器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 打开一个套接字通道,
SocketChannel sc = SocketChannel.open();
// 打开一个数据报通道
DatagramChannel dc = DatagramChannel.open();
文件通道还可以通过底层文件句柄的的方式获得,但是这样有可能导致不能读写文件
//不要使用这种方式获取通道实例
FileInputStream fis = new FileInputStream("d:/test.txt");
FileChannel fileChannel = fis.getChannel();
ByteBuffer buff = ByteBuffer.allocate(8192);
fileChannel.write(buff);
还可以通过java.nio.channels.Channels这个工具类获取通道实例,下面是一个例子:
// 创建一个可读通道
ReadableByteChannel rbc = Channels.newChannel(System.in);
// 创建一个可写通道
WritableByteChannel wbc = Channels.newChannel(System.out);
// 创建一个大小为8192字节的字节缓冲区
ByteBuffer buff = ByteBuffer.allocate(8192);
// 轮询将可读通道的数据读到缓冲区
while (rbc.read(buff) != -1) {
// 翻转缓冲区
buff.flip()
String str = new String(buff.array()).trim();
// 若输入"bye"则关闭通道
if (str.equals("bye")) {
rbc.close();
wbc.close();
break;
}
// 将缓冲区的数据写入到可写通道
wbc.write(buff);
// 轮询缓冲区是否还有剩余数据
while (buff.hasRemaining()) {
wbc.write(buff);
}
// 清空缓冲区
buff.clear();
}
通道可以以阻塞(blocking)或非阻塞(non-blocking)模式运行,阻塞模式会一直等待某个操作直到返回结果;非阻塞不会一直等待,要么返回null,要么返回执行完的结果。只有流通道才能已non-blocking模式运行,如Socket和Pipe。
Socket通道类继承SelectableChannel,只有SelectableChannel类才能与选择器(Selector)一起使用。
关闭通道使用close()方法,调用close()方法根据操作系统的网络实现不同可能会出现阻塞,可以在任何时候多次调用close();若出现阻塞,第一次调用close()后会一直等待;若第一次调用close()成功关闭后,之后再调用close()会立即返回,不会执行任何操作。
在一个已关闭的通道上进行I/O操作会抛出ClosedChannelException,可以通道isOpen()方法来检查通道时候为打开状态。
NIO中的通道都实现了InterruptibleChannel,若某个线程上有一个处于阻塞状态的通道,线程被中断会抛出ClosedByInterruptException,并会关闭通道。可以调用isInterrupted()方法检查某个线程的interrupt状态。
二、文件通道(FileChannel)
FileChannel不能直接创建,只能通过创建一个文件对象(RandAccessFile、FileInputStream、FileOutputStream)后调用其getChannel()方法获得。FileChannel是线程安全,多个进程并发操作同一文件不会引起任何问题;兵法行为受低层操作系统或文件系统影响。
FileChannel类保证同一个JVM上的所有FileChannel实例看到的文件内容是一致的,但不能保证外部的非Java进程看到的该文件视图一致,也可能一致,这取决于低层操作系统的实现。
打开一个文件通道可以使用下面方式:
//RandomAccessFile有2中构造方法,下面的构造方法等同于:
//RandomAccessFile raf = new RandomAccessFile(new File("test.txt"), "rw");
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
RandomAccessFile构造方法的第二个参数含义如下:
- "rw":对文件可读可写,若文件不存在则会创建该文件
- "r":只读
- "rws":对文件可读写,并且文件中数据和元信息(若更新时间等)的每个更新都会写入到磁盘
- "rwd":对文件可读写,并且文件数据的每个更新会同步写入到磁盘
文件锁在Jdk1.4时才被提供,当多个程序并发操作同一个文件时,可以使用文件锁来锁定文件同一时刻只能接受一个程序的IO操作。文件锁分为独占锁和共享锁,FileChannel提供了文件锁的API,在FileChannel的文件锁方式中,锁的对象是文件而不是通道或线程,也就是说文件锁不适用于同一个JVM上多个线程并发访问文件的情况。同一个JVM中,一个线程获得了某个文件的独占锁,第二个线程也可以获得这个文件的独占锁;但是,在不同的JVM中,第一个JVM的线程获得的某个文件的独占所,第二个JVM的线程会被阻塞。导致这样的情况的原因是文件锁是由操作系统在进程级上来判优的,而不是在线程级上。
文件锁可以通FileChannel的lock()或tryLock()方法获取,两者的区别如下:
- lock():阻塞,第一个线程获取文件锁后,第二个线程必须等待
- tryLock():非阻塞,若不能立即获得文件锁则返回null
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel fc = raf.getChannel();
//--------------------------------------------
//阻塞获得文件独占锁,并锁定文件所有数据
FileLock lock0 = fc.lock();
//阻塞获得文件独占锁,并锁定文件指定数据
FileLock lock1 = fc.lock(0, 8192, false);
//阻塞获得文件共享锁,并锁定文件0 ~ 8192字节的数据
FileLock lock2 = fc.lock(0, 8192, true);
//--------------------------------------------
//非阻塞获取文件独占所,等同于lock()
FileLock lock3 = fc.tryLock();
//非阻塞获取文件独占所,等同于fc.lock(0, 8192, false);
FileLock lock4 = fc.tryLock(0, 8192, false);
//非阻塞获取文件共享锁,等同于fc.lock(0, 8192, true);
FileLock lock5 = fc.tryLock(0, 8192, true);
FileLock对象关联FileChannel,FileLock API如下:
- channel():获取关联的FileChannel
- isShared():判断是共享锁还是独占锁,返回ture是共享锁,返回false是独占所
- overlaps(long position, long size):判断当前文件锁锁定的区域是否有交叉,也就是是否也被别线程锁定了
- isValid():判断当前文件锁是否有效
- release():释放文件锁,通道被关闭或JVM关闭时也会释放文件锁。
实际应用中,一般使用共享受读文件,使用独占所写文件
三 、Socket通道(SocketChannel)
新的socket通道类可以运行非阻塞模式并且是可选择的。这两个性能可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。NIO模式下再也没有为每个socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换总开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择(readiness selection)。
ServerSocketChannel
public abstract class ServerSocketChannel extends AbstractSelectableChannel
{
public static ServerSocketChannel open() throws IOException;
public abstract ServerSocket socket();
public abstract ServerSocket accept()throws IOException;
public final int validOps();
}
ServerSocketChannel是一个基于通道的socket监听器。它同我们所熟悉的java.net.ServerSocket执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
socketChannel.close();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
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()) {
channel.write(buf);
}
注意SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。
DatagramChannel
正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)。
DatagramChannel是无连接的。每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)。