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

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, 点击公众号回复的链接即可下载。如:

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

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin Multiplatform 是一种由 JetBrains 开发的跨平台开发框架。它允许开发人员使用 Kotlin 语言编写代码,然后在多个平台上运行,包括 Android、iOS、Web 等。与传统的跨平台解决方案相比,Kotlin Multiplatform 提供了更高的灵活性和性能。 Kotlin Multiplatform 的核心思想是共享代码。开发人员可以编写一个通用的 Kotlin 模块,其中包含与平台无关的业务逻辑和算法。然后,他们可以根据不同的目标平台,编写平台特定的代码。这样,开发人员可以在不同平台之间共享核心逻辑,减少了重复代码的编写,并且保持了一致性。 Kotlin Multiplatform 目前已经应用于许多项目中。对于 Android 开发人员来说,它提供了更好的性能和开发体验。它允许开发人员在 Android 和 iOS 上使用相同的 Kotlin 代码库,从而加快了开发速度和代码复用。对于 iOS 开发人员来说,Kotlin Multiplatform 可以通过共享核心业务逻辑来简化跨平台开发,并且可以与现有的 Objective-C 或 Swift 代码无缝集成。 总之,Kotlin Multiplatform 是一个强大的跨平台开发框架,可以大大简化和提高开发人员的工作效率。它同时适用于 Android 和 iOS 开发,并且允许开发人员在不同平台之间共享核心逻辑。在未来,我们可以预见 Kotlin Multiplatform 将会在跨平台开发领域发挥更大的作用,并且有望成为开发人员的首选解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值