欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

(转)Java 文件分块上传客户端源代码

程序员文章站 2022-07-12 16:48:17
...

 本博客介绍如何进行文件的分块上传。本文侧重介绍客户端,服务器端请参考博客《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 < blockNumber) {//不是最后一块,那就是固定大小了
			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  = new RandomAccessFile(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 < blockNumber) {//既不是第一块,也不是最后一块
			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() {
		// TODO Auto-generated method stub
		return fileName;
	}

	@Override
	public long getContentLength() {
		// TODO Auto-generated method stub
		return blockSize;
	}

}

  在自定义的 HttpComponents/HttpClient 工具类 HttpClient4Util 里进行分块上传的封装:

	public static String restPost(String serverURL, File targetFile,Map<String, String> mediaInfoMap){

		String content ="";
		try {
			DefaultHttpClient httpClient = new DefaultHttpClient();
			HttpPost post = new HttpPost(serverURL +"?");
			httpClient.getParams().setParameter("http.socket.timeout",60*60*1000);
			MultipartEntity mpEntity = new MultipartEntity();
			List<String> keys = new ArrayList<String>(mediaInfoMap.keySet());
			Collections.sort(keys, String.CASE_INSENSITIVE_ORDER);
			for (Iterator<String> iterator = keys.iterator(); iterator.hasNext();) {
				String key = iterator.next();
				if (StringUtils.isNotBlank(mediaInfoMap.get(key))) {
					mpEntity.addPart(key, new StringBody(mediaInfoMap.get(key)));
				}
			}
			
			if(targetFile!=null&&targetFile.exists()){
				ContentBody contentBody = new FileBody(targetFile);
				mpEntity.addPart("file", contentBody);
			}
			post.setEntity(mpEntity);

			
			HttpResponse response = httpClient.execute(post);
			content = EntityUtils.toString(response.getEntity());
			httpClient.getConnectionManager().shutdown();
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("=====RequestUrl==========================\n"
				+getRequestUrlStrRest(serverURL, mediaInfoMap).replaceAll("&fmt=json", ""));
		System.out.println("=====content==========================\n"+content);
		return content.trim();
	}

  其中 "file" 是分块上传服务器对分块文件参数定义的名字。细心的读者会发现,整块文件上传直接使用 Apache 官方的 InputStreamBody,而分块才使用自定义的 BlockStreamBody。

        最后调用 HttpClient4Util 进行上传:

 

	public static Map<String, String> uploadToDrive(
			Map<String, String> params, String domain) {

		File targetFile = new File(params.get("filePath"));
		long targetFileSize = targetFile.length();
		int mBlockNumber = 0;
		if (targetFileSize < GlobalConstant.CLOUD_API_LOGON_SIZE) {
			mBlockNumber = 1;
		} else {
			mBlockNumber = (int) (targetFileSize / GlobalConstant.CLOUD_API_LOGON_SIZE);
			long someExtra = targetFileSize
					% GlobalConstant.CLOUD_API_LOGON_SIZE;
			if (someExtra > 0) {
				mBlockNumber++;
			}
		}
		params.put("blockNumber", Integer.toString(mBlockNumber));

		if (domain != null) {
			LOG.debug("Drive---domain=" + domain);
			LOG.debug("drive---url=" + "http://" + domain + "/sync"
					+ GlobalConstant.CLOUD_API_PRE_UPLOAD_PATH);
		} else {
			LOG.debug("Drive---domain=null");
		}
		String responseBodyStr = HttpClient4Util.getRest("http://" + domain
				+ "/sync" + GlobalConstant.CLOUD_API_PRE_UPLOAD_PATH, params);

		ObjectMapper mapper = new ObjectMapper();
		DrivePreInfo result;
		try {
			result = mapper.readValue(responseBodyStr, ArcDrivePreInfo.class);
		} catch (IOException e) {
			LOG.error("Drive.preUploadToArcDrive error.", e);
			throw new RtuploadException(GlobalConstant.ERROR_CODE_13001);// TODO
		}
		// JSONObject jsonObject = JSONObject.fromObject(responseBodyStr);
		if (Integer.valueOf(result.getRc()) == 0) {
			int uuid = result.getUuid();
			String upsServerUrl = result.getUploadServerUrl().replace("https",
					"http");
			if (uuid != -1) {
				upsServerUrl = upsServerUrl
						+ GlobalConstant.CLOUD_API_UPLOAD_PATH;
				params.put("uuid", String.valueOf(uuid));

				for (int i = 1; i <= mBlockNumber; i++) {
					params.put("blockIndex", "" + i);
					HttpClient4Util.restPostBlock(upsServerUrl, targetFile,
							params);//
				}
			}
		} else {
			throw new RtuploadException(GlobalConstant.ERROR_CODE_13001);// TODO
		}
		return null;
	}

 其中 params 这个 Map 里封装的是服务器分块上传所需要的一些参数,而上传块数也在这里进行确定。
        本文中的示例经本人测试能够上传大文件成功,诸如 *.mp4 的文件上传成功没有出现任何问题。如果读者朋友测试时遇到问题无法上传成功,请在博客后跟帖留言,大家共同交流下。本文示例肯定还存在很多不足之处,如果读者朋友发现还请留言指出,笔者先行谢过了。

相关标签: 上传下载