CatShare原理实战篇(3):在Android里内嵌Ktor+Netty HTTPS服务器,WebSocket协议与ZIP流式传输
【免费下载链接】CatShare 类原生 & 海外设备,现已加入互传联盟。 项目地址: https://gitcode.com/gh_mirrors/ca/CatShare
CatShare 是一款开源的 Android 互传应用,模仿各大品牌手机间的"互传"体验:用蓝牙发现附近设备,通过 Wi-Fi Direct 点对点直连,发送和接收文件与文本。本文是"原理实战"系列第 3 篇,带你拆解它的传输层核心:如何在 Android App 内部嵌入一个 Ktor + Netty 的 HTTPS 服务器,用 WebSocket 协议完成信令握手,并借助 ZIP 流式传输实现满速文件互传。
🏗️ 为什么要在 App 里跑一个"服务器"?
CatShare 的互传链路由三个通道分工协作:
| 蓝牙 LE(BLE) | 发现设备、交换公钥、传递 Wi-Fi Direct 连接信息 | 低 |
| Wi-Fi Direct | 免路由器组网,相当于两台设备间"拉网线" | 高 |
| HTTPS + WebSocket | 真正的数据通道,文件从这里流过 | 高 |
它的设计哲学是:发送方临时充当服务器,接收方临时充当客户端。整个过程中,只有发送方运行 Ktor 服务器,接收方只运行一个 Ktor HttpClient,传输结束后服务器立即关闭。
- 发送方(服务器端):P2pSenderService.kt
- 接收方(客户端):P2pReceiverService.kt
🔐 搭建内嵌 HTTPS 服务器:三个关键点
关键点 1:一行 embeddedServer 启动 Netty 引擎
Ktor 的 embeddedServer(Netty) 把一个完整的 HTTP 服务器直接嵌入 App 进程,无需独立部署:
val httpServer = embeddedServer(Netty, configure = {
val keyStore = buildKeyStore {
certificate("sampleAlias") {
password = "foobar"
domains = listOf("127.0.0.1", "0.0.0.0", "localhost")
}
}
sslConnector(keyStore = keyStore, keyAlias = "sampleAlias", …) {
port = 0
}
enableHttp2 = false
}) {
install(WebSockets)
// 路由:/websocket 与 /download
}
关键点 2:自签名证书 + 随机端口,用完即弃
- buildKeyStore 由 ktor-network-tls-certificates 在运行时自动生成自签名证书,无需在打包时携带任何证书文件,每次传输都是一份全新证书;
- sslConnector 设置 port = 0,让系统分配一个空闲端口,启动后通过 resolvedConnectors().first().port 取出实际端口(P2pSenderService.kt),这个端口会随连接信息一起交给接收方。
关键点 3:接收方为什么敢"跳过证书校验"?
因为证书是发送方随机生成的自签名证书,接收方无法提前预装。接收端通过 FakeTrustManager.kt 信任一切:
class FakeTrustManager : X509TrustManager {
override fun checkClientTrusted(…) { } // 空实现
override fun checkServerTrusted(…) { }
override fun getAcceptedIssuers() = emptyArray()
}
这并非裸奔:整个 Wi-Fi Direct 网络是隔离的临时组网,接入凭证(SSID/PSK/端口)只能通过加密的 BLE 通道获得,外部设备根本无法路由到该服务器,"安全"由链路隔离而非 CA 体系保证。
📡 WebSocket 信令:一条连接上跑 4 类消息
文件走 HTTP 下载,控制指令则全部复用同一条 WSS 连接(wss://<组网IP>:<端口>/websocket)。消息格式极简,一行文本:
type:id:name?payload
// 例如:action:1:sendRequest?{"taskId":"42","fileName":"cat.png","fileCount":1,"totalSize":1024}
解析正则定义在 WebSocketMessage.kt。完整握手时序:
发送方还设置了层层超时:等待 WS 连接 10 秒、握手 5 秒、开始传输 30 秒(P2pSenderService.kt),任何一步卡住都会给出明确报错,不会无声挂起。
📦 ZIP 流式传输:为什么不用多个 HTTP 请求逐文件发?
发送端:边读边压,不落盘
/download 路由用 respondOutputStream 把响应体直接当流用,ZipOutputStream 逐个文件写入:
call.respondOutputStream(ContentType.Application.Zip) {
ZipOutputStream(this).use { zo ->
for ((i, rf) in task.files.withIndex()) {
cr.openInputStream(rf.uri)!!.use { ist ->
zo.putNextEntry(ZipEntry("$i/${rf.name}"))
// 4MB 缓冲,读完写入,累计进度
}
}
}
}
两个工程细节值得借鉴(P2pSenderService.kt):
- 4MB 缓冲区:大块读写摊薄系统调用开销;
- 进度节流:累计 1 秒才刷新一次通知,避免通知栏被高频更新卡死。
接收端:边收边解,直写 MediaStore
接收方发起 GET /download?taskId=xxx,把响应字节通道直接包成 ZipInputStream(P2pReceiverService.kt),saveArchive 逐个 entry 读文件、直写 Downloads/CatShare/,全程不需要把 ZIP 整体落盘——内存占用恒定,进度条天然连续。
在 Android 14+ 上还注册了 ZipPathValidatorCallback 来绕过对"无意义相对路径"的严格校验(P2pReceiverService.kt、ZipPathValidatorCallback.kt)。
多文件 = 1 个 HTTP 请求,配合协程取消即可随时中断传输,这是逐文件方案做不到的。
🔄 一次传输的完整生命周期(8 步)
📚 源码阅读清单
| Ktor 服务器搭建与 WebSocket 路由 | P2pSenderService.kt#L200-L294 |
| ZIP 流式写出 + 进度节流 | P2pSenderService.kt#L296-L359 |
| 客户端信任配置(FakeTrustManager) | P2pReceiverService.kt#L383-L399 |
| 流式解压直写 MediaStore | P2pReceiverService.kt#L606-L674 |
| 信令消息格式与解析 | WebSocketMessage.kt#L27-L48 |
| 依赖版本(Ktor 3.0.3) | libs.versions.toml#L8-L31 |
💡 一个容易踩的坑:release 包混淆时,记得在 proguard-rules.pro 保留 io.ktor.**,否则 Ktor 依赖反射的类被裁剪后服务器会起不来。
下一篇,我们将聊聊 BLE 侧的公钥加密握手(BleSecurity),看看 CatShare 如何在不依赖证书体系的前提下完成"设备间信任"。
【免费下载链接】CatShare 类原生 & 海外设备,现已加入互传联盟。 项目地址: https://gitcode.com/gh_mirrors/ca/CatShare
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考






