安卓端大文件上传的设计和问题处理

目前正在做一个视频相关的项目,里面有个需求是:安卓手机端需要随时可以录制视频,时间可能是几分钟或者几个小时,然后录制的适配需要传到服务器上。如何录制这里暂时不说,我们主要研究一个如何上传的问题。

按照用户的需求,视频的分辨率要达到720p,最大码率设定为2.5Mbps,这样一分钟的大小大概是20MB左右,一个小时在1200MB。如果直接上传1200M的文件,肯定会存在:

  1. 上传端、接收端需要消耗大量内存处理这个文件,容易导致内存溢出;
  2. 网络带宽会被上传操作占满,导致网络拥堵;
  3. 传输时间过长,容易导致终端,然后必须重新上传。

针对上面的问题,我们提出了响应的解决对策:

  1. 减少单个录制文件的大小:约定录制时,每5分钟重新生成一个新的录制文件。控制单个文件在100M左右。避免超大文件的存储压力和数据准确性。在30M带宽的情况下,大约半分钟可以传输完,不会读网络造成很大的压力。
  2. 传输的时候,终端按照1MB大小分块传输,服务器端每次收到1MB文件就存入本地,传输过程中,对每个分块标记序号和md5值,确保分块传输的完整性和准确性。
  3. 传输过程异常中断时,再次发起传输时,终端先去服务器查询相应文件的最后传输成功位置,然后从这个位置开始继续传输,避免重复传输导致的网络开销。

对于5分钟录制一个文件,在安卓端启动一个定时器即可。每5分钟执行一次录制停止,指定新文件名,然后重新启动录制的操作。

安卓端上传的核心代码:

while (
        chunck <= chuncks
                &&uploadStatus!= UploadStatus.UPLOAD_STATUS_PAUSE
                &&uploadStatus!= UploadStatus.UPLOAD_STATUS_ERROR)
{

    uploadStatus = UploadStatus.UPLOAD_STATUS_UPLOADING;

//分块读取并传输,避免一次性读入的内存开销。
    final byte[] mBlock = FileUtils.getBlock((chunck - 1) * blockLength, file, blockLength);
//对数据做md5校验,如果服务器收到数据的md5和终端的不一致,这个数据需要重新传输。
    String md5 = MD5Utils.getMD5String(mBlock);
    Map params = new HashMap();
    params.put("name", file.getName());//fileName
    params.put("chunks", chuncks + "");
    params.put("chunk", chunck + "");
    params.put("filelength", file.length() + "");//文件的总大小,服务器校验大小是否一致
    params.put("md5str", md5);
    params.put("debugstr", debugstr);
    MultipartBody.Builder builder = new MultipartBody.Builder()
            .setType(MultipartBody.FORM);
    addParams(builder, params);
    RequestBody requestBody = RequestBody.create(MEDIA_TYPE_MARKDOWN, mBlock);
    builder.addFormDataPart("mFile", file.getName(), requestBody);//filename
    Log.i("onUploadSuccessurl",url);
    Request request = new Request.Builder()
            .url(url)
            .post(builder.build())
            .build();
    Response response = null;
    response = mClient.newCall(request).execute();

服务器端收到数据后,先校验md5值是否一致,不一致的话,会给终端反馈失败的标识位。如检测到当前文件以及传输完毕,就会把收到的所有分段合并成一个文件。核心代码如下:

//计算md5是否一致

String server_md5 = MD5Utils.getFileMD5String(savedFile);

Logger.info("MD5校验:终端"+md5str+">>>服务器"+server_md5+" "+(server_md5.equals(md5str)));

if (md5str!=null && md5str.length()>0 && server_md5.equals(md5str)==false) {

throw new Exception("收到数据的md5校验失败");

}

//文件传输完毕

if (schunk != null && schunk.intValue() == schunks.intValue()) {

List toDelete = new ArrayList();

//写临时文件,如果直接写最终的文件,那处理失败的时候,就会导致正常文件也错误了。

String tmpFile = newFileName+"_temp";

outputStream = new BufferedOutputStream(new FileOutputStream(new File(tmp, tmpFile)));

// 遍历文件合并

for (int i = 1; i <= schunks; i++) {

File partFile = new File(tmp, i + "_" + name);

byte[] bytes = FileUtils.readFileToByteArray(partFile);

 

System.out.println("文件合并:" + i + "/" + schunks+"..."+bytes.length);

outputStream.write(bytes);

outputStream.flush();

toDelete.add(partFile);

try {

//确保缓存写入

Thread.sleep(10);

}catch(Exception e) {}

}

outputStream.flush();

try {

outputStream.close();//关闭流

}catch(Exception ee) {}

 

try {

//确保缓存写入

Thread.sleep(50);

}catch(Exception e) {}

 

File _file = new File(tmp, tmpFile);

System.out.println("生成的文件长度"+_file.length()+", 终端反馈的长度:"+fileLength);

 

//判断长度是否一致,不一致的话,需要重新上传

if (fileLength>0 && _file.length()!=fileLength) {

System.out.println("生成的文件长度和终端反馈的不一致,终端"+fileLength+",服务器"+_file.length());

}

 

//判断原来是否有

File dst = new File(tmp, newFileName);

if (dst.exists()==false) {

_file.renameTo(dst);

System.out.println("重命名到最终的文件"+dst.getName()+"---"+dst.length());

}else {

//用更大的文件,存在一边传一边写的情况

if (_file.length()>dst.length()) {

_file.renameTo(dst);

System.out.println("将较大的文件重命名到最终的文件"+dst.getName()+"---"+dst.length());

}else {

//比已有的还小就不用处理了

System.out.println("文件大小一致,不处理"+dst.getName()+"---"+dst.length());

}

}

 

//删除分片文件

for (File file : toDelete) {

//不能因为个别文件删除失败导致所有文件不删除

try {

System.out.println("删除"+file.getName()+">>>"+file.length());

file.delete();

}catch(Exception eee) {}

}

response.getWriter().write("{\"status\":true,\"url\":\"" + getUrl(dst) + "\"}");

}else{

response.getWriter().write("{\"status\":true,\"newName\":\"" + newFileName + "\"}");

}

 

通过上面的机制,基本可以保证终端录制的大文件,能及时、准确的上传到服务器上。当然在实际调试过程中,也遇到了不少问题:

  1. 手机端的数据写入延迟。在开始阶段,总是发现上传到服务器的文件无法播放,将手机本地和服务器的文件做对比后,返现文件的开始和结尾部分都有差异。后来发现录制完成后,手机中的文件并没有立即写入磁盘,需要等待一定时间,文件越大,这个时间越久。
  2. 重复传输导致服务器端的文件损坏。主要是同一个文件传输了两次,第一次的传输任务正常合并视频文件的过程中,第二次传输任务也往同一个目标文件合并视频文件,读写冲突导致最后生成的视频文件损坏。后来改为每次合并视频文件到随机生成的临时文件,合并完成后,再改名为最终的文件,比如对同一个文件并发写导致的错误。

你可能感兴趣的:(java)