Channel-to-channel传输是可以极其快速的,特别是在底层操作系统提供本地支持的时候。某些操作系统可以不必通过用户空间传递数据而进行直接的数据传输。对于大量的数据传输,这会是一个巨大的帮助。——摘抄至《Java NIO》
今天学习了NIO的channel-to-channel的数据传输方式,为了对其有一个较清晰的认识,特意进行测试,并与java-io的传统传输方式进行比较。以下将通过对我测试实例的分析来阐述其用法,并挖掘其隐藏问题点。请往下看。
一、先上代码
package nio; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.util.Date; /** * 通道数据迁移 * * @author wh-simm * */ public class ChannelTransfer { public static void main(String[] args) throws Exception { String srcFile = "E:\videos\奇门盾甲.2017.TC720P.国语中字.mp4";// "E:\spring-mvc.xml"; // catFiles (Channels.newChannel (System.out), srcFile); nioTest(srcFile, "E:\videos\奇门盾甲.2017.TC720P.国语中字-copy-nio.mp4"); nio2Test(srcFile, "E:\videos\奇门盾甲.2017.TC720P.国语中字-copy-nio2.mp4"); ioTest(srcFile, "E:\videos\奇门盾甲.2017.TC720P.国语中字-copy-io.mp4"); } public static void ioTest(String src, String dest) { long time = new Date().getTime(); // 传统io处理方式 try { Files.copy(new File(src).toPath(), new FileOutputStream(new File(dest))); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } long run = new Date().getTime() - time; System.out.println("io运行时长:" + run); } public static void nioTest(String src, String dest) { long time = new Date().getTime(); try { catFiles(src, dest); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } long run = new Date().getTime() - time; System.out.println("nio运行时长:" + run); } public static void nio2Test(String src, String dest) { long time = new Date().getTime(); try { catFiles2(src, dest); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } long run = new Date().getTime() - time; System.out.println("nio分段传输 运行时长:" + run); } /** * nio channel-channel * * @param target * @param src * @throws Exception */ private static void catFiles(String src, String dest) throws Exception { FileInputStream fis = new FileInputStream(src); FileChannel channel = fis.getChannel(); FileOutputStream fos = new FileOutputStream(dest); // channel-to-channel 快速传递数据 超过2G的传递会被截断 int.MaxValue channel.transferTo(0, channel.size(), fos.getChannel()); channel.close(); fos.close(); fis.close(); } /** * 定义2G的buffer长度 */ private static final long bufLength = 1024 * 1024 * 1024 * 2L;// 每次拷贝2GB的数据(上限) /** * nio channel-channel * * @param target * @param src * @throws Exception */ private static void catFiles2(String src, String dest) throws Exception { FileInputStream fis = new FileInputStream(src); FileChannel input = fis.getChannel(); // channel-to-channel 快速传递数据 超过2G的传递会被截断 int.MaxValue FileOutputStream fos = new FileOutputStream(dest); FileChannel output = fos.getChannel(); long size = input.size(); while (input.position() < size) { long count = input.transferTo(input.position(), bufLength, output); input.position(input.position() + count); } input.close(); output.close(); fos.close(); fis.close(); } }
二、测试结果展示
简要分析:从迁移日志看, 使用nio 不分段迁移方式耗时101095ms,但是有个问题 物理文件只迁移成功了2GB的数据。nio分段迁移,耗时 170428毫秒,物理文件完整迁移。传统io迁移方式耗时206286ms,物理文件完整迁移。针对迁移结果,我们首先要接收到一个信息,channel-to-channel传输时,一次只能最多只能发送2GB的数据。再者就是,nio的channel-to-channel传输大文件的速度确实比传统io方式要快。当然,这个时间偏差,性能比只能反应出在我电脑上的水准,实际上迁移过程中我的磁盘io开销已经爆满了。如果机器性能够优越,这个性能差可能会体现的更明显。
三、源码解析
3.1、ioTest方法
传统io,使用stream的一边读一边写的方式来做。这里我调用的是java.nio.file.Files.copy(Path source, OutputStream out)方法,该方法对InputStream往OutputStream中的数据写入过程进行了封装,其定义的缓存尺寸buffer-size,也是比较艺术的,8KB,即系统内存页最小分配单位。假如你还在自己写这个过程,可以考虑直接使用该方法。下面是方法内部源码,仅做参考。
public static long copy(Path source, OutputStream out) throws IOException { // ensure not null before opening file Objects.requireNonNull(out); try (InputStream in = newInputStream(source)) { return copy(in, out); } } // buffer size used for reading and writing private static final int BUFFER_SIZE = 8192; /** * Reads all bytes from an input stream and writes them to an output stream. */ private static long copy(InputStream source, OutputStream sink) throws IOException { long nread = 0L; byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = source.read(buf)) > 0) { sink.write(buf, 0, n); nread += n; } return nread; }
3.2、catFiles方法,不分段直接调用transferTo方法
transferTo( )和transferFrom( )方法允许将一个通道交叉连接到另一个通道,而不需要通过一个中间缓冲区来传递数据。只有FileChannel类有这两个方法,因此channel-to-channel传输中通道之一必须是FileChannel。您不能在socket通道之间直接传输数据,不过socket通道实现WritableByteChannel和ReadableByteChannel接口,因此文件的内容可以用transferTo( )方法传输给一个socket通道,或者也可以用transferFrom( )方法将数据从一个socket通道直接读取到一个文件中。
直接的通道传输不会更新与某个FileChannel关联的position值。请求的数据传输将从position参数指定的位置开始,传输的字节数不超过count参数的值。实际传输的字节数会由方法返回,可能少于您请求的字节数。
对于传输数据来源是一个文件的transferTo( )方法,如果position + count的值大于文件的size值,传输会在文件尾的位置终止。假如传输的目的地是一个非阻塞模式的socket通道,那么当发送队列(send queue)满了之后传输就可能终止,并且如果输出队列(output queue)已满的话可能不会发送任何数据。类似地,对于transferFrom( )方法:如果来源src是另外一个FileChannel并且已经到达文件尾,那么传输将提早终止;如果来源src是一个非阻塞socket通道,只有当前处于队列中的数据才会被传输(可能没有数据)。由于网络数据传输的非确定性,阻塞模式的socket也可能会执行部分传输,这取决于操作系统。许多通道实现都是提供它们当前队列中已有的数据而不是等待您请求的全部数据都准备好。
上述内容摘抄自《Java NIO》,其中提到了输出队列已满的话可能不会发送任何数据。但是没有提及输出队列的上限到底是多少。根据我的测试结果来看,这个上限是2GB,因此超过2GB的文件 不分段是无法完整传输的。这个点很重要,需要记下。
3.3、catFiles2方法,分段调用transferTo方法
这个方法是对catFiles的优化,为了方便对比,写成两个方法。主要就是定义了一个2GB的分段长度,每传输一段数据后,就标记输入通道的position,直到position到达通道的末尾。核心代码:input.position(input.position() + count)
至此,本次测试分析结束。如果再遇到大文件的拷贝等处理,你可以试试channel-to-channel方式。