注册

现在的位置是: 小猿圈 > 基于Kotlin Multiplatform实现静态文件服务器(五)_kotlin multiplatform h5

基于Kotlin Multiplatform实现静态文件服务器(五)_kotlin multiplatform h5

Vicky2024-11-23 22:25:39.065人围观
版权声明 本文转自https://blog.csdn.net/luckyion/article/details/141475632,版权归原作者所有。如有侵权,请联系删除,谢谢。

Netty简介

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。

文件服务

文件服务基于Netty框架实现,关于Netty,可以了解:https://netty.io/

class BootStrapServer {
    private lateinit var bossGroup: EventLoopGroup
    private lateinit var workerGroup: EventLoopGroup

    fun startServer(httpFileConfig: HttpFileServerConfig) {
        LogTool.i("BootStrapServer->startServer")
        bossGroup = NioEventLoopGroup()
        workerGroup = NioEventLoopGroup()
        try {
            val b = ServerBootstrap()
            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel::class.java)
                .handler(LoggingHandler(LogLevel.INFO))
                .childHandler(HttpServerInitializer(httpFileConfig))
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true)

            val ch: Channel = b.bind(httpFileConfig.serverPort).sync().channel()
            LogTool.i("服务成功启动,请打开http://127.0.0.1:${httpFileConfig.serverPort}")
            ch.closeFuture().sync()
            serverStarted = true
        } catch (e: InterruptedException) {
            e.printStackTrace()
        } finally {
            LogTool.i("BootStrapServer->finally")
            stopServer()
        }
    }

    fun stopServer() {
        if (!serverStarted) {
            LogTool.e("服务未启动")
            return
        }
        bossGroup.shutdownGracefully()
        workerGroup.shutdownGracefully()
        serverStarted = false
    }

    companion object {
        val bootStrapServer = BootStrapServer()
        var serverStarted = false
    }
}

在Netty中,不同的请求使用不同的Handler进行处理。在这里,我们通过HttpServerInitializer进行Handler绑定。

.childHandler(HttpServerInitializer(httpFileConfig))
class HttpServerInitializer(private val httpFileConfig: HttpFileServerConfig) :
    ChannelInitializer<SocketChannel>() {
    override fun initChannel(socketChannel: SocketChannel?) {
        // 将请求和应答消息编码或解码为HTTP消息
        socketChannel?.apply {
            pipeline().addLast(HttpServerCodec())
            pipeline()
                .addLast(HttpObjectAggregator(65536)) 
            pipeline().addLast(ChunkedWriteHandler()) 
            pipeline().addLast("httpAggregator", HttpObjectAggregator(512 * 1024)); 
            pipeline().addLast("explore-file-static-handler", HttpStaticFileServerHandler(httpFileConfig))
        }
    }
}

除基本设置信息外,在pipeline中添加的HttpStaticFileServerHandler用来处理文件请求。

class HttpStaticFileServerHandler internal constructor(config: HttpFileServerConfig) :
    SimpleChannelInboundHandler<FullHttpRequest?>() {
    private val httpConfig: HttpFileServerConfig = config

    override fun channelRead0(ctx: ChannelHandlerContext?, request: FullHttpRequest?) {
        if (ctx == null || request == null) {
            LogTool.e("ctx or request is null.")
            return
        }
        if (!request.decoderResult().isSuccess) {
            sendError(ctx, BAD_REQUEST)
            return
        }

        if (request.method() !== GET) {
            sendError(ctx, METHOD_NOT_ALLOWED)
            return
        }

        val uri = request.uri()
        val path = sanitizeUri(uri)

        val file = File(path)
        if (!file.exists()) {
            sendError(ctx, NOT_FOUND)
            return
        }

        if (file.isDirectory) {
            if (uri.endsWith("/")) {
                sendFileListing(ctx, file, uri)
            } else {
                sendRedirect(ctx, "$uri/")
            }
            return
        }

        if (!file.isFile) {
            sendError(ctx, FORBIDDEN)
            return
        }

        val raf: RandomAccessFile
        try {
            raf = RandomAccessFile(file, "r")
        } catch (ignore: FileNotFoundException) {
            sendError(ctx, NOT_FOUND)
            return
        }
        val fileLength = raf.length()

        val response: HttpResponse = DefaultHttpResponse(HTTP_1_1, OK)
        HttpUtil.setContentLength(response, fileLength)
        setContentTypeHeader(response, file)
        response.headers().set(
            HttpHeaderNames.CONTENT_DISPOSITION,
            String.format("filename=%s", URLEncoder.encode(file.name, "UTF-8"))
        )

        if (HttpUtil.isKeepAlive(request)) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
        }
        ctx.write(response)

        val sendFileFuture =
            ctx.write(DefaultFileRegion(raf.channel, 0, fileLength), ctx.newProgressivePromise())
        val lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)

        sendFileFuture.addListener(object : ChannelProgressiveFutureListener {
            override fun operationProgressed(
                future: ChannelProgressiveFuture,
                progress: Long,
                total: Long
            ) {
                // Handle process.
            }

            override fun operationComplete(future: ChannelProgressiveFuture) {
                LogTool.i(future.channel().toString() + " 传输完成.")
            }
        })

        if (!HttpUtil.isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE)
        }
    }

    @Deprecated("Deprecated in Java")
    override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
        cause.printStackTrace()
        if (ctx.channel().isActive) {
            sendError(ctx, INTERNAL_SERVER_ERROR)
        }
    }

    private fun sanitizeUri(uri: String): String {
        var fileUri = uri
        try {
            fileUri = URLDecoder.decode(fileUri, "UTF-8")
        } catch (e: UnsupportedEncodingException) {
            throw Error(e)
        }

        if (fileUri.isEmpty() || fileUri[0] != '/') {
            return httpConfig.fileDirectory
        }

        // Convert to absolute path.
        return getPlatform().getPlatformDefaultRoot() + fileUri
    }

    private fun sendFileListing(ctx: ChannelHandlerContext, dir: File, dirPath: String) {
        val response: FullHttpResponse = DefaultFullHttpResponse(HTTP_1_1, OK)
        response.headers()[HttpHeaderNames.CONTENT_TYPE] = "text/html; charset=UTF-8"

        val buf = StringBuilder()
            .append("<!DOCTYPE html>\r\n")
            .append("<html><head><meta charset='utf-8' /><title>")
            .append("Listing of: ")
            .append(dirPath)
            .append("</title></head><body>\r\n")

            .append("<h3>Listing of: ")
            .append(dirPath)
            .append("</h3>\r\n")

            .append("<ul>")
            .append("<li><a href=\"../\">..</a></li>\r\n")

        for (f in dir.listFiles()!!) {
            if (f.isHidden || !f.canRead()) {
                continue
            }

            val name = f.name

            buf.append("<li><a href=\"")
                .append(name)
                .append("\">")
                .append(name)
                .append("</a></li>\r\n")
        }

        buf.append("</ul></body></html>\r\n")
        val buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8)
        response.content().writeBytes(buffer)
        buffer.release()

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
    }

    private fun sendRedirect(ctx: ChannelHandlerContext, newUri: String) {
        val response: FullHttpResponse = DefaultFullHttpResponse(HTTP_1_1, FOUND)
        response.headers()[HttpHeaderNames.LOCATION] = newUri

        // Close the connection as soon as the error message is sent.
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
    }

    private fun sendError(ctx: ChannelHandlerContext, status: HttpResponseStatus) {
        val response: FullHttpResponse = DefaultFullHttpResponse(
            HTTP_1_1, status, Unpooled.copiedBuffer("Failure: $status\r\n", CharsetUtil.UTF_8)
        )
        response.headers()[HttpHeaderNames.CONTENT_TYPE] = "text/plain; charset=UTF-8"

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
    }

    companion object {
        private fun setContentTypeHeader(response: HttpResponse, file: File) {
            val mimeTypesMap = MimetypesFileTypeMap()
            response.headers()
                .set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.path))
        }
    }
}

基于KMP的静态文件服务器就基本完成,看看Windows上访问Android的效果。

 在Windows或Linux上运行效果也一样,点击目录可以进入下一级,点击文件可以下载。

源码下载

如果不想一步一步实现,也可以关注公众号”梦想周游世界的猿同学“,或扫码关注后直接获取本示例源码。

 关注公众号后,在消息中输入 source:FileServer.zip, 点击公众号回复的链接即可下载。如:

 感谢阅读和关注,祝大家:有钱、有梦、有远方。