Java NIO:transferFrom 直到流结束

19

我正在尝试使用NIO库。我想监听8888端口的连接,一旦接受到一个连接,就将该通道中的所有内容转储到somefile

我知道如何使用ByteBuffers实现,但我想尝试使用传闻中效率超高的FileChannel.transferFrom来实现。

这是我的代码:

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.socket().bind(new InetSocketAddress(8888));

SocketChannel sChannel = ssChannel.accept();
FileChannel out = new FileOutputStream("somefile").getChannel();

while (... sChannel has not reached the end of the stream ...)     <-- what to put here?
    out.transferFrom(sChannel, out.position(), BUF_SIZE);

out.close();

那么,我的问题是:如何表达“transferFrom直到到达流的末尾”?


编辑:将1024更改为BUF_SIZE,因为所使用的缓冲区的大小与问题无关。

7个回答

15

有几种处理这种情况的方法。首先,您应该知道要传输多少字节,即使用 FileChannel.size() 来确定最大可用空间并求和结果。此情况指的是 FileChannel.trasnferTo(socketChanel)

  • 该方法不返回-1
  • 该方法在Windows上进行模拟。 Windows没有API函数将文件描述符从文件传输到套接字,但它确实有一个(两个)将名称指定的文件传输的函数 - 但与Java API不兼容。
  • 在Linux上,使用标准的 sendfile (或sendfile64),在Solaris上称为 sendfilev64

简而言之, for(long xferBytes = 0; startPos + xferBytes<fchannel.size();)doXfer()将适用于文件->套接字的传输。没有将套接字数据传输到文件的操作系统功能(OP感兴趣)。由于套接字数据不在操作系统缓存中,因此无法有效地完成,因此需要进行模拟。实现复制的最佳方法是使用标准循环,使用大小与套接字读取缓冲区相同的轮询直接ByteBuffer。由于我只使用非阻塞IO,因此还涉及选择器。

话虽如此:我希望能够使其与声称的超级高效率 "? - 它在所有操作系统上都是模拟的,因此当套接字优雅地关闭或不关闭时,它将结束传输。如果套接字是可读的并且打开的,则该函数甚至不会抛出继承的IOException(如果有任何传输)。

我希望回答清楚:只有当源文件是一个文件时,File.transferFrom 才有趣。最有效率(也是最有趣的情况)的是文件->套接字,而文件->文件则通过 filechanel.map/unmap(!!) 实现。


1
一旦连接被接受,将该通道中的所有内容转储到某个文件中。因此,输入是一个套接字通道,所以他不可能知道其中有多少数据,除非发送方首先发送一个长度字。我不知道"use FileChannel.size()来确定最大可用空间并求和结果"是什么意思。 - user207421
@EJP - 继续阅读,它确实说明了套接字->文件是徒劳无功和模拟的。使用专用的直接ByteBuffer大小为套接字读取缓冲区是完成任务的方法。1024是非常糟糕的读取大小(分配的内存总是> pageSize [4k],因此1024只会浪费内存并涉及更多的内核访问)。下面的项目显示了transfer方法的内部情况。 - bestsss
1
当然,但你回答的第一部分关于文件->套接字仍与OP的问题无关。 - user207421
@EJP,file->socket是唯一真正有用的传输方式,所以我把它放在第一位。file->file几乎可以,但涉及到mmap/munmap,后者是一项昂贵的操作,会刷新所有CPU的TLB(对于多CPU来说确实很糟糕),因此调用它时必须使用最大可能的长度,即它甚至可能产生负面的整体性能影响。好吧,我会编辑以显示项目符号提供xfer方法的背景。 :) - bestsss
2
这都是离题了。用户已经指定了他的要求。 - user207421

4

我不确定,但JavaDoc说:

尝试从源通道中读取最多count字节,并将它们写入此通道的文件,从给定位置开始。调用此方法可能会或可能不会传输所有请求的字节;是否这样做取决于通道的性质和状态。如果源通道剩余的字节数小于count字节,或者源通道是非阻塞的并且其输入缓冲区中立即可用的字节数少于count字节,则传输的字节数将少于请求的字节数。

我认为您可以告诉它复制无限字节(当然不是在循环中)就可以完成任务了:

out.transferFrom(sChannel, out.position(), Integer.MAX_VALUE);

所以,我猜当套接字连接关闭时,状态会发生改变,这将停止transferFrom方法的执行。
但是,正如我之前所说的:我不确定。

你比我快了。不过,你可以使用 Long.MAX_VALUE 代替 Integer.MAX_VALUE - Kohányi Róbert
对,但是如果在第一次调用transferFrom时只获取到了10个字节中的3个字节呢? - aioobe
也许可以测试一下你的源通道:它有一个 int read(ByteBuffer) 方法。如果返回 -1,那么里面没有任何内容,因此你已经将所有东西传输到了 FileChannel 中。如果你想要更强大的功能,那么最好还是使用 ByteBuffer重复使用它们非常高效)。 - Kohányi Róbert
1
那么如果它没有返回“-1”呢?那我就浪费了几个字节...这会完全破坏代码。 - aioobe
ReadableByteChannel的文档说明方法int read(ByteBuffer)返回读取的字节数,可能为零,或者如果通道已到达流的末尾,则为-1。因此,如果它确实返回了-1,那么你已经到达了流的末尾。这应该表明...流的末尾?如果您不能依赖此功能,那么您打算如何将通道中的所有内容传输到其他地方?除此之外,您写道您知道如何使用ByteBuffers。如果您知道那个,那么在这种情况下,您如何决定是否可以停止从通道读取? - Kohányi Róbert
显示剩余2条评论

4

直接回答您的问题:

while( (count = socketChannel.read(this.readBuffer) )  >= 0) {
   /// do something
}

但如果您这样做,您就不使用非阻塞IO的任何优势,因为您实际上将其用作阻塞IO。非阻塞IO的重点在于一个网络线程可以同时为多个客户端提供服务:如果从一个通道没有读取任何内容(即count == 0),则可以切换到其他通道(属于其他客户端连接)。
因此,循环应该实际迭代不同的通道,而不是一直从一个通道读取,直到结束为止。
请查看此教程:http://rox-xmlrpc.sourceforge.net/niotut/,我相信它会帮助您理解该问题。

2
但是调用可能会返回0字节而没有到达流的末尾,对吧? - aioobe
@aioobe 只有在非阻塞模式下,当您应该使用选择器而不是循环时,或者如果缓冲区长度为零(这是编程错误),才需要使用它。 - user207421

1
在其他人所写的基础上,这是一个简单的辅助方法,可以实现目标:
public static void transferFully(FileChannel fileChannel, ReadableByteChannel sourceChannel, long totalSize) {
    for (long bytesWritten = 0; bytesWritten < totalSize;) {
        bytesWritten += fileChannel.transferFrom(sourceChannel, bytesWritten, totalSize - bytesWritten);
    }
}

1
这样做:
URLConnection connection = new URL("target").openConnection();
File file = new File(connection.getURL().getPath().substring(1));
FileChannel download = new FileOutputStream(file).getChannel();

while(download.transferFrom(Channels.newChannel(connection.getInputStream()),
        file.length(), 1024) > 0) {
    //Some calculs to get current speed ;)
}

1
据称超级高效的FileChannel.transferFrom。
如果您想同时获得DMA访问和非阻塞IO的好处,最好的方法是将文件映射到内存中,然后只需从套接字读取到内存映射缓冲区即可。
但这需要预先分配文件。

1
transferFrom() 返回一个计数。只需不断调用它,使位置/偏移量前进,直到返回零为止。但是,一开始时的计数要比1024大得多,更像是一兆字节或两兆字节,否则你无法从这个方法中获得太多好处。
编辑:为了回应下面的所有评论,文档中指出:“如果源通道剩余的字节数少于请求的数量,或者如果源通道是非阻塞的并且其输入缓冲区中立即可用的字节数少于请求的数量,则传输的字节数将少于请求的数量。”因此,只要你处于阻塞模式,它就不会返回零,直到源中没有剩余内容。所以循环直到返回零是有效的。
编辑2:
传输方法的设计确实存在问题。它们应该被设计成在流结束时返回-1,就像所有的read()方法一样。

2
该方法的文档说明指出它返回“实际传输的字节数,可能为零”。这并不意味着它不能在第一次调用后立即返回0而没有传输任何内容。(另一个问题是:什么时候以及为什么会这样做?)然而,原始发布者不能依赖于此。我开始认为,确保该方法将通道中的所有内容传输到文件的唯一方法是事先知道将传输多少字节。 - Kohányi Róbert
@KohányiRóbert 如果您不介意省略大括号,您可以用两行代码完成此操作。无论OP想要什么,都不能在一行中使用transferTo()或transferFrom()。 - user207421
也许downvote是因为在打算耗尽流时,调用transferFrom直到它返回0才是正确的(正如@KohányiRóbert所指出的)。除非阅读评论,“只需不断调用它,推进位置/偏移量,直到它返回零。”实际上是相当误导人的。 - aioobe
@aioobe 那样做一点也不错,看看我的编辑。上面大部分的评论都是误导性的。 - user207421
1
我昨天也做了同样的事情,相信这是正确的做法。此外,以这种方式实现还可以报告传输的状态,这可能会很有用。 - rpvilao
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接