在写socket传输文件过程中想着通过零拷贝api加快传输速度,但是发现文件总是只能传过去8M。代码如下:

//服务端代码
public static void server() {
        Selector selector = null;
        ServerSocketChannel server = null;
        RandomAccessFile randomAccessFile = null;
        try {
            //创建一个server端,监听8080端口,设置非阻塞
             server = ServerSocketChannel.open();
            server.bind(new InetSocketAddress(8080));
            server.configureBlocking(false);
            //注册selector,事件为接受连接
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
            //打开一个文件流通道
            randomAccessFile = new RandomAccessFile("D:\\Program Files\\Java\\jre1.8.0_60.zip","r");
            FileChannel fileChannel = randomAccessFile.getChannel();


            while(selector.select() > 0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    //处理连接事件
                    if(key.isAcceptable()){
                        SocketChannel accept = server.accept();
                        System.out.println("服务端开始写出数据");
                        long start = System.currentTimeMillis();
                        //将文件发送到客户端
                        fileChannel.transferTo(0,randomAccessFile.length(),accept);
                        System.out.println("服务端写出完成,耗时: "
                                + (System.currentTimeMillis() - start)
                                + " ms,写出文件大小:" + randomAccessFile.length()/1024/1024 + "MB");
                    }
                    //移除当前事件
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                randomAccessFile.close();
                selector.close();
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

目标文件大小为61M

//客户端代码
public static void client(){
        SocketChannel client = null;
        Selector selector = null;
        RandomAccessFile randomAccessFile = null;
        try {
            client = SocketChannel.open(new InetSocketAddress(8080));
            client.configureBlocking(false);

            selector = Selector.open();
            client.register(selector,SelectionKey.OP_READ);

            randomAccessFile = new RandomAccessFile("D:\\Program Files\\Java\\test.zip","rw");
            FileChannel channel = randomAccessFile.getChannel();

            while(selector.select() > 0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if(key.isReadable()){
                        ByteBuffer buf = ByteBuffer.allocate(1024);
                        System.out.println("客户端开始写入数据");
                        long length = 0;
                        long start = System.currentTimeMillis();
                        while(client.read(buf) > 0){
                            buf.flip();
                            length += buf.limit();
                            channel.write(buf);
                            buf.clear();
                        }
                        System.out.println("客户端写入完成,耗时:"
                                + (System.currentTimeMillis() - start)
                                + " ms,写入文件大小:" + length/1024/1024 + "MB");
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                client.close();
                selector.close();
                randomAccessFile.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

运行结果
在这里插入图片描述
可以看到只写出了8M文件。这里可以设置服务端transferTo每次写8M循环直到文件传输完毕为止,但是问题并没有解决。经过对源码debug后发现问题如下:

FileChannelImpl

这里调用的transferTo方法是有FileChannelImpl实现的,FileChannelImpl在初始化的时候会创建一个FileDispatcherImpl对象
在这里插入图片描述
而在FileDispatcherImpl对象的静态代码块中有一个isFastFileTransferRequested方法

 static {
        IOUtil.load();
        fastFileTransfer = isFastFileTransferRequested();
    }
 static boolean isFastFileTransferRequested() {
        String fileTransferProp = java.security.AccessController.doPrivileged(
            new PrivilegedAction<String>() {
                @Override
                public String run() {
                     return System.getProperty("jdk.nio.enableFastFileTransfer");
                }
            });
        boolean enable;
        //从上面可以看到,如果没有设置jdk.nio.enableFastFileTransfer这个启动参数
        //那么这个enable就是false的
        if ("".equals(fileTransferProp)) {
            enable = true;
        } else {
            enable = Boolean.parseBoolean(fileTransferProp);
        }
        return enable;
    }

然后再看到FileChannelImpl的transferTo方法
在这里插入图片描述
这里的target是一个SocketChannel,所以这个方法进入,最终会走到这个判断

SelectableChannel sc = (SelectableChannel)target;
if (!nd.canTransferToDirectly(sc))
	//返回的是-6
    return IOStatus.UNSUPPORTED_CASE;

boolean canTransferToDirectly(java.nio.channels.SelectableChannel sc) {
	//这个就是之前被设置为false的变量,所以最终会返回false。
   return fastFileTransfer && sc.isBlocking();
}

也就是说返回的是一个小于0的数,所以进入下一个判断。这个方法我们将看到结果了

private long transferToTrustedChannel(long position, long count,
                                          WritableByteChannel target)
        throws IOException
    {
    	//这里返回的true
        boolean isSelChImpl = (target instanceof SelChImpl);
        //false
        if (!((target instanceof FileChannelImpl) || isSelChImpl))
            return IOStatus.UNSUPPORTED;

        // Trusted target: Use a mapped buffer
        long remaining = count;
        while (remaining > 0L) {
        	//关键在这, MAPPED_TRANSFER_SIZE是8M大小,也就是这个size每次传输8M
        	//通过循环完成全部文件的传输
            long size = Math.min(remaining, MAPPED_TRANSFER_SIZE);
            try {
                MappedByteBuffer dbb = map(MapMode.READ_ONLY, position, size);
                try {
                    // ## Bug: Closing this channel will not terminate the write
                    int n = target.write(dbb);
                    assert n >= 0;
                    remaining -= n;
                    //这里是true,会进入。也就是说传输了8M就break了!
                    if (isSelChImpl) {
                        // one attempt to write to selectable channel
                        break;
                    }
                    assert n > 0;
                    position += n;
                } finally {
                    unmap(dbb);
                }
            } catch (ClosedByInterruptException e) {
                // target closed by interrupt as ClosedByInterruptException needs
                // to be thrown after closing this channel.
                assert !target.isOpen();
                try {
                    close();
                } catch (Throwable suppressed) {
                    e.addSuppressed(suppressed);
                }
                throw e;
            } catch (IOException ioe) {
                // Only throw exception if no bytes have been written
                if (remaining == count)
                    throw ioe;
                break;
            }
        }
        return count - remaining;
    }

所以通过源码可以看到,因为transferTo的目标对象是一个socketChannel,而又没有设置启动参数jdk.nio.enableFastFileTransfer,所以传输8M文件后就直接返回了。
那么解决办法就很简单了,启动参数带上jdk.nio.enableFastFileTransfer就可以了。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐