本博客介绍如何进行文件的分块上传。本文侧重介绍客户端,服务器端请参考博客《Java 文件分块上传服务器端源代码》。建议读者朋友在阅读本文代码前先了解一下 MIME 协议。
所谓分块上传并非把大文件进行物理分块,然后挨个上传,而是依次读取大文件的一部分文件流进行上传。分块,倒不如说分流比较切实。本文通过一个项目中的示例,说明使用 Apache 的 HttpComponents/HttpClient 对大文件进行分块上传的过程。示例使用的版本是
HttpComponents Client 4.2.1。
本文仅以一小 demo 功能性地解释 HttpComponents/HttpClient 分块上传,没有考虑 I/O 关闭、多线程等资源因素,读者可以根据自己的项目酌情处理。
本文核心思想及流程:以 100 MB 大小为例,大于 100 MB 的进行分块上传,否则整块上传。对于大于 100 MB 的文件,又以 100 MB 为单位进行分割,保证每次以不大于 100 MB 的大小进行上传。比如 304 MB 的一个文件会分为 100 MB、100
MB、100 MB、4 MB 等四块依次上传。第一次读取 0 字节开始的 100 MB 个字节,上传;第二次读取第 100 MB 字节开始的 100 MB 个字节,上传;第三次读取第 200 MB 字节开始的 100 MB 个字节,上传;第四次读取最后剩下的 4 MB 个字节进行上传。
自定义的 ContentBody 源码如下,其中定义了流的读取和输出:
package com.defonds.rtupload.common.util.block;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import org.apache.http.entity.mime.content.AbstractContentBody;
import com.defonds.rtupload.GlobalConstant;
public class BlockStreamBody extends AbstractContentBody {
//给MultipartEntity看的2个参数
private long blockSize=0;//本次分块上传的大小
private String fileName=null;//上传文件名
//writeTo需要的3个参数
private int blockNumber=0,blockIndex=0;//blockNumber分块数;blockIndex当前第几块
private File targetFile=null;//要上传的文件
private BlockStreamBody(String mimeType) {
super(mimeType);
// TODO Auto-generated constructor stub
}
/**
* 自定义的ContentBody构造子
* @param blockNumber分块数
* @param blockIndex当前第几块
* @param targetFile要上传的文件
*/
public BlockStreamBody(int blockNumber, int blockIndex, File targetFile) {
this("application/octet-stream");
this.blockNumber=blockNumber;//blockNumber初始化
this.blockIndex=blockIndex;//blockIndex初始化
this.targetFile=targetFile;//targetFile初始化
this.fileName=targetFile.getName();//fileName初始化
//blockSize初始化
if (blockIndex
this.blockSize=GlobalConstant.CLOUD_API_LOGON_SIZE;
} else {//最后一块
this.blockSize=targetFile.length() - GlobalConstant.CLOUD_API_LOGON_SIZE * (blockNumber - 1);
}
}
@Override
public void writeTo(OutputStream out) throws IOException {
byte b[] = new byte[1024];//暂存容器
RandomAccessFile raf=newRandomAccessFile(targetFile, "r");//负责读取数据
if (blockIndex== 1) {//第一块
int n=0;
long readLength=0;//记录已读字节数
while (readLength <= blockSize - 1024) {//大部分字节在这里读取
n=raf.read(b, 0, 1024);
readLength += 1024;
out.write(b, 0, n);
}
if (readLength <= blockSize) {//余下的不足 1024 个字节在这里读取
n=raf.read(b, 0, (int)(blockSize - readLength));
out.write(b, 0, n);
}
} else if (blockIndex
raf.seek(GlobalConstant.CLOUD_API_LOGON_SIZE * (blockIndex - 1));//跳过前[块数*固定大小 ]个字节
int n=0;
long readLength=0;//记录已读字节数
while (readLength <= blockSize - 1024) {//大部分字节在这里读取
n=raf.read(b, 0, 1024);
readLength += 1024;
out.write(b, 0, n);
}
if (readLength <= blockSize) {//余下的不足 1024 个字节在这里读取
n=raf.read(b, 0, (int)(blockSize - readLength));
out.write(b, 0, n);
}
} else {//最后一块
raf.seek(GlobalConstant.CLOUD_API_LOGON_SIZE * (blockIndex - 1));//跳过前[块数*固定大小 ]个字节
int n=0;
while ((n=raf.read(b, 0, 1024)) != -1) {
out.write(b, 0, n);
}
}
//TODO 最后不要忘掉关闭out/raf
}
@Override
public String getCharset() {
// TODO Auto-generated method stub
return null;
}
@Override
public String getTransferEncoding() {
// TODO Auto-generated method stub
return "binary";
}
@Override
public String getFilename() {