Android 大文件分块上传实战:突破表单数据限制的完整方案

一、问题背景与核心思路

1.1 场景痛点

当 Android 客户端需要上传 500MB 的大文件到服务器,而服务器表单限制为 2MB 时,传统的直接上传方案将完全失效。此时需要设计一套分块上传机制,将大文件拆分为多个小块,突破服务器限制。

1.2 核心思路

分块上传 + 服务端合并

  1. 将文件切割为 ≤2MB 的块
  2. 逐块上传至服务器
  3. 服务端接收后按顺序合并

二、Android 客户端实现细节

2.1 分块处理与上传流程

完整代码实现(Kotlin)

kotlin 复制代码
// FileUploader.kt
object FileUploader {
    // 分块大小(1.9MB 预留安全空间)
    private const val CHUNK_SIZE = 1.9 * 1024 * 1024 

    suspend fun uploadLargeFile(context: Context, file: File) {
        val fileId = generateFileId(file) // 生成唯一文件标识
        val totalChunks = calculateTotalChunks(file)
        val uploadedChunks = loadProgress(context, fileId) // 加载已上传分块记录

        FileInputStream(file).use { fis ->
            for (chunkNumber in 0 until totalChunks) {
                if (uploadedChunks.contains(chunkNumber)) continue

                val chunkData = readChunk(fis, chunkNumber)
                val isLastChunk = chunkNumber == totalChunks - 1

                try {
                    uploadChunk(fileId, chunkNumber, totalChunks, chunkData, isLastChunk)
                    saveProgress(context, fileId, chunkNumber) // 记录成功上传的分块
                } catch (e: Exception) {
                    handleRetry(fileId, chunkNumber) // 重试逻辑
                }
            }
        }
    }

    private fun readChunk(fis: FileInputStream, chunkNumber: Int): ByteArray {
        val skipBytes = chunkNumber * CHUNK_SIZE
        fis.channel().position(skipBytes.toLong())

        val buffer = ByteArray(CHUNK_SIZE)
        val bytesRead = fis.read(buffer)
        return if (bytesRead < buffer.size) buffer.copyOf(bytesRead) else buffer
    }
}

关键技术点解析

  1. 唯一文件标识生成:通过文件内容哈希(如 SHA-256)确保唯一性

    kotlin 复制代码
    fun generateFileId(file: File): String {
        val digest = MessageDigest.getInstance("SHA-256")
        file.inputStream().use { is ->
            val buffer = ByteArray(8192)
            var read: Int
            while (is.read(buffer).also { read = it } > 0) {
                digest.update(buffer, 0, read)
            }
        }
        return digest.digest().toHex()
    }
  2. 进度持久化存储:使用 SharedPreferences 记录上传进度

    kotlin 复制代码
    private fun saveProgress(context: Context, fileId: String, chunk: Int) {
        val prefs = context.getSharedPreferences("upload_progress", MODE_PRIVATE)
        val key = "${fileId}_chunks"
        val existing = prefs.getStringSet(key, mutableSetOf()) ?: mutableSetOf()
        prefs.edit().putStringSet(key, existing + chunk.toString()).apply()
    }

2.2 网络请求实现(Retrofit + Kotlin Coroutine)

kotlin 复制代码
// UploadService.kt
interface UploadService {
    @Multipart
    @POST("api/upload/chunk")
    suspend fun uploadChunk(
        @Part("fileId") fileId: RequestBody,
        @Part("chunkNumber") chunkNumber: RequestBody,
        @Part("totalChunks") totalChunks: RequestBody,
        @Part("isLast") isLast: RequestBody,
        @Part chunk: MultipartBody.Part
    ): Response<UploadResponse>
}

// 上传请求封装
private suspend fun uploadChunk(
    fileId: String,
    chunkNumber: Int,
    totalChunks: Int,
    chunkData: ByteArray,
    isLast: Boolean
) {
    val service = RetrofitClient.create(UploadService::class.java)
    
    val requestFile = chunkData.toRequestBody("application/octet-stream".toMediaType())
    val chunkPart = MultipartBody.Part.createFormData(
        "chunk", 
        "chunk_${chunkNumber}", 
        requestFile
    )

    val response = service.uploadChunk(
        fileId = fileId.toRequestBody(),
        chunkNumber = chunkNumber.toString().toRequestBody(),
        totalChunks = totalChunks.toString().toRequestBody(),
        isLast = isLast.toString().toRequestBody(),
        chunk = chunkPart
    )

    if (!response.isSuccessful) {
        throw IOException("Upload failed: ${response.errorBody()?.string()}")
    }
}

三、服务端实现(Spring Boot 示例)

3.1 接收分块接口

java 复制代码
@RestController
@RequestMapping("/api/upload")
public class UploadController {
    
    @Value("${upload.temp-dir:/tmp/uploads}")
    private String tempDir;
    
    @PostMapping("/chunk")
    public ResponseEntity<?> uploadChunk(
        @RequestParam String fileId,
        @RequestParam int chunkNumber,
        @RequestParam int totalChunks,
        @RequestParam boolean isLast,
        @RequestPart("chunk") MultipartFile chunk) {
        
        // 创建临时目录
        Path tempDirPath = Paths.get(tempDir, fileId);
        if (!Files.exists(tempDirPath)) {
            try {
                Files.createDirectories(tempDirPath);
            } catch (IOException e) {
                return ResponseEntity.status(500).body("Create dir failed");
            }
        }
        
        // 保存分块
        Path chunkFile = tempDirPath.resolve("chunk_" + chunkNumber);
        try {
            chunk.transferTo(chunkFile);
        } catch (IOException e) {
            return ResponseEntity.status(500).body("Save chunk failed");
        }
        
        // 如果是最后一块则触发合并
        if (isLast) {
            asyncMergeFile(fileId, totalChunks);
        }
        
        return ResponseEntity.ok().build();
    }
    
    @Async
    public void asyncMergeFile(String fileId, int totalChunks) {
        // 实现合并逻辑
    }
}

3.2 合并文件实现

java 复制代码
private void mergeFile(String fileId, int totalChunks) throws IOException {
    Path tempDir = Paths.get(this.tempDir, fileId);
    Path outputFile = Paths.get("/data/final", fileId + ".dat");
    
    try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(outputFile))) {
        for (int i = 0; i < totalChunks; i++) {
            Path chunk = tempDir.resolve("chunk_" + i);
            Files.copy(chunk, out);
        }
        out.flush();
    }
    
    // 清理临时文件
    FileUtils.deleteDirectory(tempDir.toFile());
}

四、技术对比与方案选择

方案 优点 缺点 适用场景
传统表单上传 实现简单 受限于服务器大小限制 小文件上传(<2MB)
分块上传 突破大小限制,支持断点续传 实现复杂度较高 大文件上传(>100MB)
第三方云存储SDK 无需自行实现,功能完善 依赖第三方服务,可能有费用产生 需要快速集成云存储的场景

五、关键实现步骤总结

  1. 客户端分块切割

    • 确定分块大小(建议略小于限制值)
    • 生成唯一文件ID(基于文件内容哈希)
    • 实现可恢复的上传进度记录
  2. 分块上传

    • 使用多部分表单上传每个分块
    • 携带分块元数据(序号/总数/文件ID)
    • 实现超时重试机制
  3. 服务端处理

    • 按文件ID创建临时存储目录
    • 验证分块完整性(可选MD5校验)
    • 原子性合并操作
  4. 可靠性增强

    • 断点续传支持
    • 网络异常自动重试
    • 上传完整性校验

六、注意事项与优化建议

  1. 分块大小优化

    • 建议设置为 服务器限制值 * 0.95(如 1.9MB)
    • 测试不同分块大小对传输效率的影响
  2. 并发控制

    • 可并行上传多个分块(需服务端支持)
    • 合理控制并发数(建议 3-5 个并行)
  3. 安全防护

    • 添加身份验证(JWT Token)
    • 限制单个文件的最大分块数
    • 使用 HTTPS 加密传输
  4. 服务端优化

    • 设置合理的临时文件清理策略
    • 使用异步合并操作避免阻塞请求线程
    • 实现分块哈希校验(示例代码见下方)

分块校验示例(服务端)

java 复制代码
// 计算分块MD5
String receivedHash = DigestUtils.md5Hex(chunk.getInputStream());
if (!receivedHash.equals(clientProvidedHash)) {
    throw new InvalidChunkException("Chunk hash mismatch");
}

七、扩展方案:第三方云存储集成

对于不想自行实现分块上传的场景,可考虑以下方案:

  1. 阿里云OSS分片上传

    kotlin 复制代码
    val oss = OSSClient(context, endpoint, credentialProvider)
    val request = InitiateMultipartUploadRequest(bucketName, objectKey)
    val uploadId = oss.initMultipartUpload(request).uploadId
    
    // 上传分片
    val partETags = mutableListOf<PartETag>()
    for (i in chunks.indices) {
        val uploadPartRequest = UploadPartRequest(
            bucketName, objectKey, uploadId, i+1).apply {
            partContent = chunks[i]
        }
        partETags.add(oss.uploadPart(uploadPartRequest).partETag)
    }
    
    // 完成上传
    val completeRequest = CompleteMultipartUploadRequest(
        bucketName, objectKey, uploadId, partETags)
    oss.completeMultipartUpload(completeRequest)
  2. AWS S3 TransferUtility

    java 复制代码
    TransferUtility transferUtility = TransferUtility.builder()
        .s3Client(s3Client)
        .context(context)
        .build();
    
    MultipleFileUpload upload = transferUtility.uploadDirectory(
        bucketName, 
        remoteDir, 
        localDir, 
        new ObjectMetadataProvider() {
            @Override
            public void provideObjectMetadata(File file, ObjectMetadata metadata) {
                metadata.setContentType("application/octet-stream");
            }
        });
    
    upload.setTransferListener(new UploadListener());

八、关键点总结

  1. 分块策略:合理设置分块大小,生成唯一文件标识
  2. 断点续传:本地持久化上传进度,支持网络恢复
  3. 完整性校验:客户端与服务端双端校验分块数据
  4. 并发控制:平衡并行上传数量与服务器压力
  5. 错误处理:实现自动重试与异常上报机制
  6. 安全防护:身份验证 + 传输加密 + 大小限制

九、参考资源

  1. Retrofit 官方文档
  2. Spring Boot 文件上传指南
  3. AWS Android SDK 文档
  4. MD5校验最佳实践
相关推荐
网安INF5 分钟前
CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
java·web安全·网络安全·flink·漏洞
一叶知秋哈5 分钟前
Java应用Flink CDC监听MySQL数据变动内容输出到控制台
java·mysql·flink
jackson凌11 分钟前
【Java学习笔记】SringBuffer类(重点)
java·笔记·学习
sclibingqing17 分钟前
SpringBoot项目接口集中测试方法及实现
java·spring boot·后端
程序员JerrySUN21 分钟前
全面理解 Linux 内核性能问题:分类、实战与调优策略
java·linux·运维·服务器·单片机
糯米导航25 分钟前
Java毕业设计:办公自动化系统的设计与实现
java·开发语言·课程设计
糯米导航28 分钟前
Java毕业设计:WML信息查询与后端信息发布系统开发
java·开发语言·课程设计
米粉03051 小时前
深入剖析Nginx:从入门到高并发架构实战
java·运维·nginx·架构
简诚1 小时前
HttpURLConnection实现
java
androidwork1 小时前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·java·kotlin·androidx