Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。
Java NIO的通道可以更加细化。例如,不同的网络传输协议类型,在Java中都有不同的NIO Channel实现。
其中最为重要的四种Channel实现:
- FileChannel:文件通道,用于文件的数据读写。
- SocketChannel:套接字通道,用于套接字TCP连接的数据读写。
- ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求创建一个SocketChannel通道。
- DatagramChannel:数据报通道,用于UDP的数据读写。
1 FileChannel
1.1 获取FileChannel
- 可以通过文件的输入流、输出流获取FileChannel
@Test
public void getChannel(){
try {
// 创建一个文件输出流
FileOutputStream fos = new FileOutputStream("D:\\h.txt");
FileChannel channel1 = fos.getChannel();
// 创建一个文件输入流
FileInputStream fins = new FileInputStream("D:\\h.txt");
FileChannel channel2 = fins.getChannel();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
复制代码
- 也可以通过RandomAccessFile(文件随机访问)类来获取FileChannel实例,代码如下:
//创建RandomAccessFile随机访问对象
RandomAccessFile rFile = new RandomAccessFile("filename.txt","rw");
//获取文件流的通道(可读可写)
FileChannel channel = rFile.getChannel();
复制代码
1.2 读取FileChannel
在大部分应用场景中,从通道读取数据都会调用通道的int read(ByteBuffer buf)
方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量。
public class FileChannelReadDemo01 {
public static void main(String[] args) {
testReadDataFromChannel();
}
public static void testReadDataFromChannel() {
// 创建一个文件输出流
RandomAccessFile fos = null;
FileChannel channel = null;
try {
fos = new RandomAccessFile ("D:\\h.txt","rw");
channel = fos.getChannel();
// 定义一个字节缓冲区域
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = -1;
while ((length = channel.read(buffer)) != -1) {
// 调用flip,buffer由写入变为读
buffer.flip();
byte[] array = buffer.array();
System.out.print(new String(array, 0, length,"utf-8"));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
closeQuietly(fos);
closeQuietly(channel);
}
}
public static void closeQuietly(java.io.Closeable o) {
if (null == o) {
return;
}
try {
o.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
1.3 写入FileChannel
把数据写入通道,在大部分应用场景中都会调用通道的write(ByteBuffer)
方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。
write(ByteBuffer)
方法的作用是从ByteBuffer缓冲区中读取数据,然后写入通道自身,而返回值是写入成功的字节数。
对于入参buffer实例来说,需要从其中读取数据写入channel通道中,所以入参buffer必须处于读模式,不能处于写模式。
//如果buf处于写模式(如刚写完数据),需要翻转buf,使其变成读模式
buf.flip();
int outlength = 0;
//调用write()方法,将buf的数据写入通道
while ((outlength = outchannel.write(buf)) != 0) {
System.out.println("写入的字节数:" + outlength);
}
复制代码
1.4 关闭channel
当通道使用完成后,必须将其关闭。关闭非常简单,调用close()方法即可。
1.5 强制刷新到磁盘
在将缓冲区写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据落地(或刷新)到磁盘,完成最终的数据保存。
在将缓冲区数据写入通道时,要保证数据能写入磁盘,可以在写入后调用一下FileChannel的force()方法。
1.6 【案例】 文件复制
public class FileNIOCopyDemo {
public static void main(String[] args) throws IOException {
// 源文件
File srcFile = new File("D:\\h.txt");
// 目标文件
File destFile = new File("D:\\hd.txt");
// 校验源文件是否存在
fileIsExists(srcFile);
// 创建目标文件
createFile(destFile);
long startTime = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outchannel = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
inChannel = fis.getChannel();
outchannel = fos.getChannel();
int length = -1;
// 创建一个ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(1024);
// 读取文件
while ((length = inChannel.read(buf)) != -1) {
//翻转buf,变成成读模式
buf.flip();
int outlength = 0;
// 从buffer中读取数据,然后写入到channe中
while ((outlength = outchannel.write(buf)) != 0) {
System.out.println("写入字节数:" + outlength);
}
// 清除buf,变成写入模式
buf.clear();
}
//强制刷新磁盘
outchannel.force(true);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeQuietly(fis);
closeQuietly(fos);
closeQuietly(inChannel);
closeQuietly(outchannel);
}
long endTime = System.currentTimeMillis();
System.out.println("一共耗时:【" + (endTime - startTime) + "】 ms");
}
private static void createFile(File destFile) throws IOException {
if (!destFile.exists()) {
destFile.createNewFile();
}
}
private static void fileIsExists(File srcFile) throws FileNotFoundException {
if (!srcFile.exists()) {
throw new FileNotFoundException("source.file.not.exists");
}
}
private static void closeQuietly(java.io.Closeable o) {
if (null == o) {
return;
}
try {
o.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
一共耗时:【14】 ms
作为文件复制的程序来说,以上代码的效率不是最高的。更高效的文件复制可以调用文件通道的transferFrom()方法。
public class FileNIOFastCopyDemo {
public static void main(String[] args) throws IOException {
// 源文件
File srcFile = new File("D:\\h.txt");
// 目标文件
File destFile = new File("D:\\hd.txt");
// 校验源文件是否存在
fileIsExists(srcFile);
// 创建目标文件
createFile(destFile);
long startTime = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outchannel = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
inChannel = fis.getChannel();
outchannel = fos.getChannel();
long size = inChannel.size();
long pos = 0;
long count = 0;
while (pos < size) {
//每次复制最多1024个字节,没有就复制剩余的
count = size - pos > 1024 ? 1024 : size - pos;
//复制内存,偏移量pos + count长度
pos += outchannel.transferFrom(inChannel, pos, count);
}
//强制刷新磁盘
outchannel.force(true);
} catch (Exception e) {
e.printStackTrace();
} finally {
closeQuietly(fis);
closeQuietly(fos);
closeQuietly(inChannel);
closeQuietly(outchannel);
}
long endTime = System.currentTimeMillis();
System.out.println("一共耗时:【" + (endTime - startTime) + "】 ms");
}
private static void createFile(File destFile) throws IOException {
if (!destFile.exists()) {
destFile.createNewFile();
}
}
private static void fileIsExists(File srcFile) throws FileNotFoundException {
if (!srcFile.exists()) {
throw new FileNotFoundException("source.file.not.exists");
}
}
private static void closeQuietly(Closeable o) {
if (null == o) {
return;
}
try {
o.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
2 SocketChannel
在NIO中,涉及网络连接的通道有两个:
- 一个是SocketChannel,负责连接的数据传输;
- 另一个是ServerSocketChannel,负责连接的监听。
其中,NIO中的SocketChannel传输通道与OIO中的Socket类对应,NIO中的ServerSocketChannel监听通道对应于OIO中的ServerSocket类。
ServerSocketChannel仅应用于服务端,而SocketChannel同时处于服务端和客户端。所以,对于一个连接,两端都有一个负责传输的SocketChannel。
无论是ServerSocketChannel还是SocketChannel,都支持阻塞和非阻塞两种模式。如何进行模式的设置呢?调用configureBlocking()方法,具体如下:
// 设置为非阻塞模式。
socketChannel.configureBlocking(false)
// 设置为阻塞模式。
socketChannel.configureBlocking(true)
复制代码
在阻塞模式下,SocketChannel的连接、读、写操作都是同步阻塞式的,,在效率上与Java OIO面向流的阻塞式读写操作相同。
2.1 获取SocketChannel传输通道
- 通过SocketChannel静态方法open()获得一个套接字传输通道
- 然后将socket设置为非阻塞模式
- 最后通过connect()实例方法对服务器的IP和端口发起连接。