当前位置: 首页 > news >正文

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

一、问题背景与核心思路

1.1 场景痛点

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

1.2 核心思路

分块上传 + 服务端合并

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

二、Android 客户端实现细节

2.1 分块处理与上传流程

完整代码实现(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)) continueval chunkData = readChunk(fis, chunkNumber)val isLastChunk = chunkNumber == totalChunks - 1try {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_SIZEfis.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)确保唯一性

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

    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)

// 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 接收分块接口

@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();}@Asyncpublic void asyncMergeFile(String fileId, int totalChunks) {// 实现合并逻辑}
}

3.2 合并文件实现

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. 服务端优化

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

分块校验示例(服务端)

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

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

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

  1. 阿里云OSS分片上传

    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

    TransferUtility transferUtility = TransferUtility.builder().s3Client(s3Client).context(context).build();MultipleFileUpload upload = transferUtility.uploadDirectory(bucketName, remoteDir, localDir, new ObjectMetadataProvider() {@Overridepublic 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校验最佳实践

相关文章:

  • 用 AI 开发 AI:原汤化原食的 MCP 桌面客户端
  • 【评测】Qwen3-Embedding模型初体验
  • MSYS2 环境配置与 Python 项目依赖管理笔记
  • android计算器代码
  • typeof运算符 +unll和undefined的区别
  • 树状数组学习笔记
  • 人工智能学习07-函数
  • MATLAB遍历生成20到1000个节点的无线通信网络拓扑推理数据
  • 动态模块加载的响应式架构:从零到一的企业级实战指南
  • 量化面试绿皮书:7. 100的阶乘中有多少个尾随零
  • 《PyTorch深度学习入门》
  • 05.查询表
  • 探索双曲函数:从定义到MATLAB可视化
  • 【CATIA的二次开发23】抽象对象Document涉及文档激活控制的方法
  • 深入​剖析网络IO复用
  • 一文掌握 Tombola 抽象基类的自动化子类测试策略
  • 工作邮箱收到钓鱼邮件,点了链接进去无法访问,会有什么问题吗?
  • github开源协议选择
  • ESP32 在Arduino开发环境中,如果程序运行报错如何定位程序报错是哪行代码
  • Python爬虫实战:研究demiurge框架相关技术
  • 南昌做网站要多少钱/百度搜索使用方法
  • 奉化云优化seo/seo公司的选上海百首网络
  • 建设网站banner/百度seo关键词排名优化工具
  • 迷你主机做网站/广州做seo的公司
  • 阿里巴巴怎么做企业网站/市场推广方案模板
  • 有主体新增网站/谷歌账号注册入口官网