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

news2024/9/23 16:21:01

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2068280.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“购物也能赚钱?‘随机返利‘模式颠覆你的消费体验!“

近期&#xff0c;关于“消费即享随机返利”的话题在张三与李四之间频繁提及&#xff0c;这一新颖的消费机制究竟是何方神圣&#xff1f; 实质上&#xff0c;它并非某种实体物品&#xff0c;而是一种创新的营销策略&#xff0c;旨在促进商品销售。去年&#xff0c;一位精明的商家…

【25届秋招】饿了么0817算法岗笔试

目录 1. 第一题2. 第二题3. 第三题 ⏰ 时间&#xff1a;2024/08/17 &#x1f504; 输入输出&#xff1a;ACM格式 ⏳ 时长&#xff1a;100min 本试卷还有单选和多选部分&#xff0c;但这部分比较简单就不再展示。 最近终于有时间继续整理之前的笔试题了&#xff0c;因为时间仓促…

Gartner发布2024年终端和工作空间安全成熟度曲线:24项相关技术发展和应用状况及趋势

由于攻击者使用人工智能来增强网络钓鱼和终端攻击&#xff0c;企业需要高级安全措施来阻止入侵行为。此技术成熟度曲线可帮助安全和风险管理领导者识别可增强终端和工作空间保护的技术。 需要知道什么 网络安全创新层出不穷&#xff0c;但区分真正的进步与短暂的趋势却很困难。…

如何在Python中使用IP代理

在网络爬虫、数据抓取等应用场景中&#xff0c;使用IP代理可以有效避免IP被封禁&#xff0c;提高爬取效率。本文将详细介绍如何在Python中使用IP代理&#xff0c;帮助你在实际项目中灵活应用。 准备工作 在开始之前&#xff0c;你需要准备以下工具和资源&#xff1a; Python环…

Go Convey测试框架入门(go convey gomonkey)

Go Convey测试框架入门 介绍 GoConvey是一款针对Golang的测试框架&#xff0c;可以管理和运行测试用例&#xff0c;同时提供了丰富的断言函数&#xff0c;并支持很多 Web 界面特性。 Golang虽然自带了单元测试功能&#xff0c;并且在GoConvey框架诞生之前也出现了许多第三方测…

JAVA后端程序拉取私人仓库的npm包并将该程序打包成jar包

当前有一个系统用于导出项目&#xff0c;而每次导出的项目并不可以直接使用&#xff0c;需要手动从npm私人仓库中获取一个npm包然后将他们整合到一起它才是一个完整的项目&#xff0c;所以目前我的任务就是编写一个java程序可以自动地从npm私人仓库中拉取下来那个模板代码到指定…

虚拟机网络的三种模式,NAT模式,桥接模式,仅主机模式

一、首先说最简单的也就是桥接模式 使用桥接模式会在虚拟机生成一个虚拟交换机&#xff0c;连接到主机的网卡&#xff0c;所以他们是能互相ping通的。 二、NAT模式&#xff0c;我感觉是最复杂的一个模式 使用nat模式&#xff0c;主机会多出一个网卡&#xff0c;这个网卡vmnet…

微信小程序获取当前位置并自定义浮窗

1、在腾讯地图api申请key&#xff08;添加微信小程序的appid&#xff09;。 每个Key每日可以免费使用100次&#xff0c;超过次数后会导致地图不显示。可以多申请几个Key解决。WebService API | 腾讯位置服务腾讯地图开放平台为各类应用厂商和开发者提供基于腾讯地图的地理位置…

推荐一个国内Midjourney镜像站,限时充值享5折优惠 结尾附实测图片

作为一名绘画爱好者&#xff0c;你是否曾梦想过将脑海中的画面转化为现实&#xff1f;现在&#xff0c;有了群嘉智创平台&#xff08;ai.qunzjia.cn&#xff09;&#xff0c;这一切都将成为可能。群嘉智创是国内领先的AI对话与Midjourney绘画服务平台&#xff0c;通过接入国内多…

如何使用ssm实现校园美食交流系统+vue

TOC ssm026校园美食交流系统vue 第1章 概述 1.1 研究背景 随着现代网络技术发展&#xff0c;对于校园美食交流系统现在正处于网络发展的阶段&#xff0c;所以对它的要求也是比较严格的&#xff0c;要从这个系统的功能和用户实际需求来进行对系统制定开发的发展方式&#xf…

【MySQL】 黑马 MySQL进阶 笔记

文章目录 存储引擎MySQL的体系结构存储引擎概念存储引擎特点InnoDBMyISAMMemory 存储引擎选择 索引概述结构B Tree(多路平衡查找树)B TreeHash为什么InnoDB存储引擎选择使用Btree索引结构? 分类思考题 语法SQL性能分析&#xff08;索引相关&#xff09;SQL执行频率慢查询日志p…

VMware Workstation Pro 下载

文章目录 VMware Workstation ProVMware下载与安装 VMware Workstation Pro VMware Workstation Pro 对个人用户已经完全免费&#xff01; VMware下载与安装 第一步&#xff1a;进入vmware的官网 VMWare已被收购&#xff0c;因此它会跳到&#xff0c; Broadcom 注册页面&…

[Meachines] [Easy] granny IIS 6.0+CVE-2017-7269+进程迁移+MS15-051权限提升

信息收集 IP AddressOpening Ports10.10.10.15TCP:80 $ nmap -p- 10.10.10.15 --min-rate 1000 -sC -sV -Pn PORT STATE SERVICE VERSION 80/tcp open http Microsoft IIS httpd 6.0 |_http-server-header: Microsoft-IIS/6.0 | http-methods: |_ Potentially risky…

移动式气象站:科技赋能,监测天气

在自然灾害频发、气候变化日益显著的今天&#xff0c;准确、及时地获取气象信息对于农业生产、城市规划、交通运输以及灾害预警等领域至关重要。传统固定气象站虽能提供稳定的观测数据&#xff0c;但在偏远地区、灾害现场或快速变化的环境中&#xff0c;其局限性逐渐显现。为此…

怎么都在劝我用通义灵码

听朋友说最近通义灵码有个活动&#xff0c;分享体验心得就有机会抽 iPhone 15。而且通过活动第一次使用通义灵码的新用户&#xff0c;还人均送一个“显眼包”。 有点儿心动了。点开活动页面一看&#xff0c;好家伙&#xff0c;好几百人都在劝我用通义灵码。 来看看他们是怎么说…

Winxvideo AI(AI视频编辑软件) v3.5 中文免安装版

Winxvideo AI是一款基于人工智能技术开发的视频编辑软件。 软件截图&#xff1a; 使用说明&#xff1a; 解压后&#xff0c;双击start_xvideo.bat来运行软件 下载地址&#xff1a;压缩包 解压密码&#xff1a;helloh 下载时可能会有广告&#xff0c;忽略&#xff0c;等下载…

深入学习SQL优化的第三天

目录 聚合函数 排序和分组 聚合函数 1251. 平均售价 表&#xff1a;Prices------------------------ | Column Name | Type | ------------------------ | product_id | int | | start_date | date | | end_date | date | | price | int …

【题解】【排序】—— [NOIP1998 提高组] 拼数

【题解】【排序】—— [NOIP1998 提高组] 拼数 [NOIP1998 提高组] 拼数题目描述输入格式输出格式输入输出样例输入 #1输出 #1输入 #2输出 #2 提示 1.题意解析2.AC代码 [NOIP1998 提高组] 拼数 题目描述 设有 n n n 个正整数 a 1 … a n a_1 \dots a_n a1​…an​&#xff0…

第41篇 使用数码管实现计数器<二>

Q&#xff1a;如何设计汇编语言程序实现手动控制计数器&#xff1f; A&#xff1a;在本实验程序中&#xff0c;使用轮询法读取Data寄存器获取KEY的状态&#xff0c;当未按下任何KEY时&#xff0c;Data寄存器中的值为0&#xff0c;当按下按键KEY[i]时&#xff0c;Data寄存器中…

Circuitjs 创建自定义逻辑(Custom Logic)器件

您可以使用 自定义逻辑芯片 来实现自己的简单逻辑器件. 位于“菜单–绘制–数字芯片–添加自定义逻辑”下, 或者是"右键–数字芯片–添加自定义逻辑". 视频简介: Circuitjs 自定义逻辑电路(custom logic)功能简介 一个具体示例 来看一个具体的示例, 通过它来讲述 自…