Zrobiłem prosty przykład serwera używając Netty w Kotlinie (wspiera zero-copy przez wrapowanie pliku w typ FileRegion
) - w requeście podajesz nazwę pliku i offset od którego ma być wysyłany, np.: ?file=test.txt&from=10
Obsługę większości błędów i bezpieczeństwa ominąłem, być może trzeba też manualnie zwalniać jakieś resource. Na podstawie materiałów w sieci można sobie dopisać klienta do tego, który w razie potrzeby robi retry od pewnego offsetu:
package server
import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelInitializer
import io.netty.channel.DefaultFileRegion
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.codec.http.*
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.file.Files
fun main() {
ServerBootstrap()
.group(NioEventLoopGroup(1), NioEventLoopGroup())
.channel(NioServerSocketChannel::class.java)
.childHandler(object : ChannelInitializer<SocketChannel>() {
override fun initChannel(connection: SocketChannel) {
connection.pipeline()
.addLast(HttpServerCodec())
.addLast(HttpObjectAggregator(Short.MAX_VALUE.toInt()))
.addLast(
object : ChannelInboundHandlerAdapter() {
override fun channelRead(context: ChannelHandlerContext, request: Any) {
request as FullHttpRequest
try {
handleFileRequest(request, context)
} catch (ex: Exception) {
handleError(request, ex, context)
}
}
}
)
}
})
.bind(8080).sync().channel().closeFuture().sync()
}
fun handleFileRequest(request: HttpRequest, context: ChannelHandlerContext) {
val params = QueryStringDecoder(request.uri()).parameters()
val fileName = params["file"]?.firstOrNull() ?: error("File name not provided")
val fromOffset = params["from"]?.run { firstOrNull()?.toLong() } ?: 0L
val file = File(fileName)
if (file.exists().not()) error("File does not exist: $file")
val fileSize = file.length()
val fileRegion = DefaultFileRegion(file, fromOffset, fileSize - fromOffset)
val headers = DefaultHttpHeaders()
.add(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"${file.name}\"")
.add(HttpHeaderNames.CONTENT_TYPE, Files.probeContentType(file.toPath()))
.add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
val response = DefaultHttpResponse(
request.protocolVersion(),
HttpResponseStatus.OK,
headers
)
with(context) {
writeAndFlush(response)
writeAndFlush(fileRegion)
writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
}
println(fileRegion)
}
fun handleError(request: HttpRequest, error: Exception, context: ChannelHandlerContext) {
val body =
Unpooled.wrappedBuffer(
StringWriter()
.also { error.printStackTrace(PrintWriter(it)) }
.toString()
.toByteArray()
)
val headers = DefaultHttpHeaders()
.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
.add(HttpHeaderNames.CONTENT_LENGTH, body.capacity())
val response = DefaultFullHttpResponse(
request.protocolVersion(),
HttpResponseStatus.BAD_REQUEST,
body,
headers,
EmptyHttpHeaders.INSTANCE
)
context.channel()
.writeAndFlush(response)
}