Closed bridger closed 4 years ago
That's a good question, @weissi would know more about the FileRegion
stuff.
Although I think the main concern here is that you're not loading the entire file into memory first or allowing the file's contents to balloon in memory. If you use Vapor's FileIO helpers (built on top of NIO's non blocking file io type) you can read in a file chunk by chunk with back pressure support. This means that you should theoretically be able to wait until the WebSocket has successfully written the chunk before reading a new one into memory. That should be very efficient without needing to get into the weeds with FileRegion stuff.
@bridger NIO (on sockets) does support sending FileRegion
it'll use the sendfile
system call to send them. That fulfils your requirement of "not being loaded into memory". However, these days, sendfile
is less useful because it doesn't support TLS (in theory possible with Kernel TLS but yeah, not really a thing), it'll literally send a file through a socket.
So what @tanner0101 proposes is the right way to go. NIO has all the tools that are required to only load a fixed sized chunk into memory at once and Vapor 4 seems to make use of them :)
@tanner0101 / @bridger One thing that we do want to eventually do however is to have the SSLHandler
support FileRegion
s by itself making use of NonBlockingFileIO
and sending the file (it will however load that into memory of course). We're tracking the work here: https://github.com/apple/swift-nio-ssl/issues/175
It's important to add that even if we implemented that, it won't be any better than if you implement it exactly the way @tanner0101 proposes :)
Sweet. I’m onboard with the idea that incrementally loaded chunks is a solution that will be more useful than sendfile. It would also allow the messages to go through a compression pipeline.
One tricky thing is making sure that complete messages are still sent in order. For example, if I have two calls - send a file and then send a Data message, I’d like to see that all chunks from the first file are sent before the Data message is sent.
Sent with GitHawk
@bridger Doing this outside a handler is quite straightforward: Use readChunked
and in the chunkHandler
closure just pass channel.writeAndFlush
:).
NonBlockingFileIO will only send you the next chunk if the previous one completed the future (making sure we don't load too much into memory). NIO will also take care of the ordering for you. If you enqueue stuff in NIO in the right order, it'll come out in the right order.
Implementing a handler that takes OutboundIn = FileRegion
and produces OutboundOut = ByteBuffer
by loading chunks of a file is indeed not super easy. You'll need to buffer everything incoming until you fully processed the previous item. I think the Perfect folks were working on something like this though and eventually this should be part of NIO's SSL handlers :).
@bridger here's some rough pseudo-code of what I mentioned in the first comment:
let fileio: NonBlockingFileIO
fileio.readChunked(...) { chunk in
let promise = el.makePromise(...)
ws.send(chunk, promise: promise)
return promise.futureResult
}
There are some circumstances in my app where I'd like to send data directly from a file to a WebSocket. Is it possible to support that, without loading the file contents into memory?
I see that Channel.writeAndFlush can take a FileRegion, which conforms to NIOAny. Using that type can allow SwiftNIO to send the file without loading it into memory. However, the docs for FileRegion say:
Does anyone have a good guess (or know where to look) to see if the ChannelHandlers in Websocket-Kit and SwiftNIO need access directly to a ByteBuffer?