You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从自建Rust服务器向Android应用下载APK时出现流结束错误

问题分析与解决思路

核心问题定位

目前APK下载到95%左右中断,结合代码来看,大概率是Rust服务器未确保缓冲区数据全部发送就关闭连接,或是Android端读取逻辑的边界处理存在问题。


Rust 服务器端排查与修复

1. 强制刷新发送缓冲区

你当前用write_all(&stream)发送响应,但write_all仅保证字节写入内核缓冲区,不确保数据已发送到网络。必须在write_all后调用flush(),把缓冲区数据强制推送到网络:

let http_stream = HttpResponseBuilder { 
    status : "200 OK".to_string(),
    headers : vec![],
    body : String::new()
}.build_with_bytes(match fs::read(apk::get_apk_path()) {
    Ok(k_) => k_,
    Err(e_) => {
        error_response(stream, hkslib::error::format_error("handle_get_apk", e_.to_string()));
        return;
    }
});

// 写入数据后强制刷新缓冲区
if let Err(e) = http_stream.write_all(&stream) {
    error!("Failed to write response: {}", e);
}
// 关键:确保所有数据发送到客户端
if let Err(e) = stream.flush() {
    error!("Failed to flush stream: {}", e);
}

2. 改用流式文件发送(避免内存过载)

现在用fs::read把整个APK读进内存,大文件会占用大量内存,还可能导致发送时缓冲区压力过大。改成流式读取发送更稳定:

use std::io::{BufReader, Read, Write};

fn handle_get_apk(mut stream: TcpStream) {
    let apk_path = apk::get_apk_path();
    let file = match fs::File::open(&apk_path) {
        Ok(f) => f,
        Err(e) => {
            error_response(stream, hkslib::error::format_error("handle_get_apk", e.to_string()));
            return;
        }
    };
    let file_size = file.metadata().unwrap().len();

    // 构建并发送HTTP响应头
    let headers = format!(
        "HTTP/1.1 200 OK\r\nAccept-Ranges: bytes\r\nContent-Length: {}\r\n\r\n",
        file_size
    );
    if let Err(e) = stream.write_all(headers.as_bytes()) {
        error!("Failed to write headers: {}", e);
        return;
    }
    stream.flush().unwrap();

    // 分块读取并发送文件
    let mut reader = BufReader::new(file);
    let mut buffer = [0; 4096];
    loop {
        match reader.read(&mut buffer) {
            Ok(0) => break, // 文件读取完毕
            Ok(n) => {
                if let Err(e) = stream.write_all(&buffer[0..n]) {
                    error!("Failed to write file chunk: {}", e);
                    break;
                }
                stream.flush().unwrap(); // 每块发送后立即刷新
            }
            Err(e) => {
                error!("Failed to read file: {}", e);
                break;
            }
        }
    }
}

3. 检查连接生命周期

确认服务器线程在处理完请求后,不要立即关闭TcpStream。TCP本身会有FIN握手,但必须确保数据全部发送完成后再释放连接。


Android 客户端排查与修复

1. 修正读取逻辑的边界判断

当前while(!channel.isClosedForRead)可能在最后一块数据未读完时提前退出,改成按文件总长度校验读取:

val totalLength = response.contentLength() ?: -1
var receivedLength = 0L
val channel = response.bodyAsChannel()

// 按总长度循环读取,直到接收完整
while (receivedLength < totalLength || totalLength == -1) {
    try {
        val bytes = channel.readByteArray(4096)
        if (bytes.isEmpty()) break
        localFile.appendBytes(bytes)
        receivedLength += bytes.size
        _log("Received $receivedLength bytes from $totalLength")
    } catch (e: Exception) {
        _log("Failed to read data... $e")
        e.printStackTrace()
        break
    }
}

// 校验文件完整性,不匹配则删除
if (totalLength != -1 && receivedLength != totalLength) {
    _log("Download incomplete: expected $totalLength, got $receivedLength")
    localFile.delete()
}

2. 移除不必要的runBlocking嵌套

GlobalScope.launch(Dispatchers.IO)里再套runBlocking会阻塞线程,改用async/await逻辑更合理:

fun getFile(destination: String, client: HttpClient, headers: ArrayList<Pair<String, String>>?) {
    GlobalScope.launch(Dispatchers.IO) {
        log("sending request to $destination")
        val localFile = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "app.apk")

        try {
            client.prepareGet(destination) {
                headers?.forEach { (key, value) ->
                    header(key, value)
                }
            }.execute { response ->
                if (response.status.value >= 400) {
                    println(response.toString())
                    return@execute
                }
                // 上面修正后的读取逻辑放在这里
                val totalLength = response.contentLength() ?: -1
                var receivedLength = 0L
                val channel = response.bodyAsChannel()

                while (receivedLength < totalLength || totalLength == -1) {
                    val bytes = channel.readByteArray(4096)
                    if (bytes.isEmpty()) break
                    localFile.appendBytes(bytes)
                    receivedLength += bytes.size
                    _log("Received $receivedLength bytes from $totalLength")
                }

                if (totalLength != -1 && receivedLength != totalLength) {
                    _log("Download failed: incomplete file")
                    localFile.delete()
                } else {
                    _log("Download completed successfully")
                }
            }
        } catch (e: Exception) {
            _log("Request failed: $e")
            e.printStackTrace()
        }
    }
}

额外验证步骤

  1. 用curl直接从服务器下载APK:curl http://你的服务器IP/apk路径 -o test.apk,对比原文件大小和MD5值,确认服务器是否能发送完整文件。
  2. 在Rust服务器打印发送的总字节数,和原APK大小对比,确认是否发送了全部数据。
  3. 检查Android端是否有后台进程(如内存清理)中断下载,或存储权限是否正常。

内容的提问来源于stack exchange,提问作者HomeyKrogerSage

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 01:50:22