在使用AsyncHttpClient做简单的非断点上传功能时,我们要想实时检测任务的开始、结束以及进度,需要实现AsyncHttpResponseHandler,并复写其各种onXXX()方法。代码如下:
@Override
public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) {
}
@Override
public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) {
}
@Override
public void onProgress(long bytesWritten, long totalSize) {
}
@Override
public void onCancel() {
}
@Override
public void onFinish() {
}
@Override
public void onRetry(int retryNo) {
}
@Override
public void onStart() {
}
其中,在onProgress里,我们可以得到上传的进度。以上说的是简单上传,运行一点问题都没有。
可是当我按照服务端提供的接口做断点上传时,onProgress就不能正常的返回进度了。为了找出原因,我对两种情况的http请求都进行了抓包,发现原因出在http请求头里。在简单上传时,请求头里的"Content-Type"字段默认为 "multipart/form-data"的形式,而服务端给我的接口中,请求头里的"Content-Type"必须是"application/octet-stream"。
为什么只是改了个请求头就不能回调进度了呢?我想只有在框架源码中才能找到答案。只有找到onProgress这个回调最原始的地方才能知道原因。我先在AsyncHttpResponseHandler里找到了这个onProgress方法:
/**
* Fired when the request progress, override to handle in your own code
*
* @param bytesWritten offset from start of file
* @param totalSize total size of file
*/
public void onProgress(long bytesWritten, long totalSize) {
AsyncHttpClient.log.v(LOG_TAG, String.format("Progress %d from %d (%2.0f%%)", bytesWritten, totalSize, (totalSize > 0) ? (bytesWritten * 1.0 / totalSize) * 100 : -1));
}
发现它是在一个handleMessage方法的一个等于PROGRESS_MESSAGE 的case分支中被调用的:
case PROGRESS_MESSAGE:
response = (Object[]) message.obj;
if (response != null && response.length >= 2) {
try {
onProgress((Long) response[0], (Long) response[1]);
} catch (Throwable t) {
AsyncHttpClient.log.e(LOG_TAG, "custom onProgress contains an error", t);
}
} else {
AsyncHttpClient.log.e(LOG_TAG, "PROGRESS_MESSAGE didn't got enough params");
}
break;
接着找到这个PROGRESS_MESSAGE 是在sendProgressMessage方法中传递过来的。而这个sendProgressMessage方法是复写的方法。它是在AsyncHttpResponseHandler的父类ResponseHandlerInterface中被定义的。
@Override
final public void sendProgressMessage(long bytesWritten, long bytesTotal) {
sendMessage(obtainMessage(PROGRESS_MESSAGE, new Object[]{bytesWritten, bytesTotal}));
}
接下来只要能找到用ResponseHandlerInterface来调用sendProgressMessage的地方差不多就能找到问题所在了。果然在SimpleMultipartEntity类中,我找到了这个方法被调用的地方,它在updateProgress方法中被调用,从名字也可以看出这是用来更新进度的。
private void updateProgress(long count) {
bytesWritten += count;
progressHandler.sendProgressMessage(bytesWritten, totalSize);
}
再看这个updateProgress在哪几个地方被调用了:首先在这个writeTo方法中被调用了2次
@Override
public void writeTo(final OutputStream outstream) throws IOException {
bytesWritten = 0;
totalSize = (int) getContentLength();
out.writeTo(outstream);
updateProgress(out.size());
for (FilePart filePart : fileParts) {
filePart.writeTo(outstream);
}
outstream.write(boundaryEnd);
updateProgress(boundaryEnd.length);
}
另外在SimpleMultipartEntity的内部类FilePart类的writeTo方法中被调用了三次。同样从名字及代码中的file也可以看出,这一段其实是真正更新文件传输进度的地方!
private class FilePart {
.
- .
.
public void writeTo(OutputStream out) throws IOException {
out.write(header);
updateProgress(header.length);
FileInputStream inputStream = new FileInputStream(file);
final byte[] tmp = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(tmp)) != -1) {
out.write(tmp, 0, bytesRead);
updateProgress(bytesRead);
}
out.write(CR_LF);
updateProgress(CR_LF.length);
out.flush();
AsyncHttpClient.silentCloseInputStream(inputStream);
}
}
但是看这里的代码写的没什么问题,为什么会没有被调用呢?
再来仔细研究一下SimpleMultipartEntity这个类发现:类的开头有段注释:意思是这是个主要用来发送一个或多个文件的简单的分段实体(请求体)。
/**
* Simplified multipart entity mainly used for sending one or more files.
*/
也就是说,在用AsyncHttpClient进行上传文件的时候,是把文件封装成这个http请求体来操作的,那我们再来仔细看看他到底做了些什么。
该类只有一个构造方法,目的是把ResponseHandlerInterface的实例传过来,好进行回调操作。然后他搞了一些boundary相关的东西。后来通过抓包发现,在发出的请求中出现的分割线原来就是在这里写入的。由于服务端给出的断点续传接口要求不要分割线,因此我就把这里的分割线相关的全部注释掉了。
public BreakpointFileEntity(ResponseHandlerInterface progressHandler) {
// final StringBuilder buf = new StringBuilder();
// final Random rand = new Random();
// for (int i = 0; i < 30; i++) {
// buf.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
// }
//
// boundary = buf.toString();
// boundaryLine = ("--" + boundary + STR_CR_LF).getBytes();
// boundaryEnd = ("--" + boundary + "--" + STR_CR_LF).getBytes();
this.progressHandler = progressHandler;
}
再来看除了构造方法外,重载最多的就是addPart方法。可以看到,除了流文件外,其他的都是最终将一个file文件封装成FilePart实例,然后添加到一个叫fileParts的集合中。然后在计算总大小(getContentLength)和写入(writeTo)的时候,再遍历集合来操作。
再来看另外两个方法createContentType和createContentDisposition,从名字可以看出是用来创建请求头里的ContentType和ContentDisposition的。
private byte[] createContentType(String type) {
String result = AsyncHttpClient.HEADER_CONTENT_TYPE + ": " + normalizeContentType(type) + STR_CR_LF;
return result.getBytes();
}
private byte[] createContentDisposition(String key) {
return (
AsyncHttpClient.HEADER_CONTENT_DISPOSITION +
// ": form-data; name=\"" + key + "\"" + STR_CR_LF).getBytes();
": attachment; name=\"" + key + "\"" + STR_CR_LF).getBytes();
//attachment; filename="Folder.jpg"
}
private byte[] createContentDisposition(String key, String fileName) {
return (
AsyncHttpClient.HEADER_CONTENT_DISPOSITION +
// ": form-data; name=\"" + key + "\"" +
// "; filename=\"" + fileName + "\"" + STR_CR_LF).getBytes();
": attachment; name=\"" + key + "\"" +
"; filename=\"" + fileName + "\"" + STR_CR_LF).getBytes();
}
这两个方法主要是在FilePart的createHeader方法里,通过此方法返回一个header对象,供FilePart持有。由于我们的断点上传要求的http头的Content_Type和Content_Disposition分别是:"application/octet-stream"和“attachment; filename="xxxx”的,所以这两个地方我需要改过来。
另外,由于断点之后再续传,应该是从同一个文件的不同字节位置开始上传的,这个位置startPos是服务器那边返回过来的,所以原来的从开头位置读取文件的做法就要做相应的修改了。
// 修改为从指定位置开始读取
public void writeTo(OutputStream out) throws IOException {
out.write(header);
updateProgress(header.length);
FileInputStream inputStream = new FileInputStream(file);
inputStream.skip(startPos); // 从指定位置开始读
final byte[] tmp = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(tmp)) != -1) {
out.write(tmp, 0, bytesRead);
updateProgress(bytesRead);
}
out.write(CR_LF);
updateProgress(CR_LF.length);
out.flush();
AsyncHttpClient.silentCloseInputStream(inputStream);
}
但是这个startPos怎么传到这里来是个问题。我们可以在FilePart中新增一个startPos的成员变量,然后重载一个带startPos参数的构造方法。
// 新增构造方法
public FilePart(String key, File file, int startPos, String type, String customFileName) {
header = createHeader(key, TextUtils.isEmpty(customFileName) ? file.getName() : customFileName, type);
this.file = file;
this.startPos = startPos;
}
然后在新增一个addPart方法,里面的add的FilePart对象的构造方法用我们刚才新增的带startPos的。
// 新增
public void addPart(String key, File file, int startPos, String type, String customFileName) {
fileParts.add(new FilePart(key, file, startPos, normalizeContentType(type), customFileName));
}
由于这个addPart方法是在外部的RequestParams类中Add file params地方调用的。因为RequestParams类是在HttpClint包中不好修改,所以我们可以复制一个RequestParams类命名为BreakpointRequestParams,然后把其中的Add
file params一段,修改为以下部分,因为file文件是通过fileWrapper这个对象带过去的,所以我们同样可以用它把startPos参数带过去。
// Add file params
// for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet()) {
// FileWrapper fileWrapper = entry.getValue();
// entity.addPart(entry.getKey(), fileWrapper.file, fileWrapper.contentType, fileWrapper.customFileName);
// }
// 新增
for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet()) {
FileWrapper fileWrapper = entry.getValue();
entity.addPart(entry.getKey(), fileWrapper.file, fileWrapper.startPos, fileWrapper.contentType, fileWrapper.customFileName);
}
因此,就需要把BreakpointRequestParams中的FileWrapper这个内部类新增一个构造方法:
public static class FileWrapper implements Serializable {
public final File file;
public final String contentType;
public final String customFileName;
public final int startPos;
public FileWrapper(File file, String contentType, String customFileName) {
this.file = file;
this.contentType = contentType;
this.customFileName = customFileName;
this.startPos = 0;
}
// 新增
public FileWrapper(File file,int startPos, String contentType, String customFileName) {
this.file = file;
this.contentType = contentType;
this.customFileName = customFileName;
this.startPos = startPos;
}
}
由于上面的for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet())遍历的是fileParams这个map集合。所以我们再找到这个map装进数据的地方,并将此处的put方法的参数加上 startPos。
/**
* Adds a file to the request with both custom provided file content-type and file name
*
* @param key the key name for the new param.
* @param file the file to add.
* @param contentType the content type of the file, eg. application/json
* @param customFileName file name to use instead of real file name
* @throws FileNotFoundException throws if wrong File argument was passed
*/
// public void put(String key, File file, String contentType, String customFileName) throws FileNotFoundException {
// if (file == null || !file.exists()) {
// throw new FileNotFoundException();
// }
// if (key != null) {
// fileParams.put(key, new FileWrapper(file, contentType, customFileName));
// }
// }
// 转为断点上传准备的
// int startPos 传输的起始位置, int fragmentLength 传输碎片的长度
public void put(String key, File file, int startPos, String contentType, String customFileName) throws FileNotFoundException {
if (file == null || !file.exists()) {
throw new FileNotFoundException();
}
if (key != null) {
fileParams.put(key, new FileWrapper(file, startPos, contentType, customFileName));
}
}
再重载一个简单参数的put方法共客户端调用,这样客户端只用调用param.put(“file”,fileToUpload,startPos),就可以实现断点上传了。
// 新增
public void put(String key, File file, int startPos) throws FileNotFoundException {
put(key, file, startPos, null, null);
}
客户端调用代码:
private void uploadSingleFile(final TranslistFileBean fileBean, final String startPos) {
String token = ExitApplication.getInstance().getToken();
AsyncHttpClient client = new AsyncHttpClient();
String url = ControlEasyHttpUtils.getBaseUrlNoV1() + "upload?_token=" + token + "&folder_id=" + fileBean.getFolderId();
BreakpointRequestParams params = new BreakpointRequestParams();
File file = new File(fileBean.getPath());
try {
params.put("file", file, Integer.parseInt(startPos));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 不加这一段文件名字会变乱码
String encode = null;
try {
encode = URLEncoder.encode(fileBean.getName(), "UTF-8");
encode = encode.replace("+", "%20"); // 把转码后的+好还原为空格
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 添加请求头
client.addHeader("Content-Length", (file.length() - Integer.parseInt(startPos)) + "");
client.addHeader("Content-Disposition", "attachment;filename=\"" + encode + "\"");
client.addHeader("Session-ID", fileBean.getSession_id());
client.addHeader("X-Content-Range", "bytes " + startPos + "-" + (file.length() - 1) + "/" + (file.length()));
client.addHeader("Content-Type", "application/octet-stream");
BreakpointAsyncHttpResponseHandler responseHandler = new BreakpointAsyncHttpResponseHandler(mContext, fileBean);
// 设置 到了该获取加密进度的时候的监听
responseHandler.setTime2GetPackPersentListener(this);
RequestHandle requestHandle = client.post(mContext, url, params, responseHandler);
//把 上传线程 添加到全局map变量中,或者替换原来的
ExitApplication.getInstance().mCancelableMap.put(fileBean, requestHandle);
}
这样如果是第一次上传,startPos 设置为"0",如果是续传,就传服务器返回的字节位置即可。这种情况下是可以上传成功的,但还是有个问题,上传上去的文件发现已坏,后来发现原来是文件头部被写入了http请求头中的信息,也就是FilePart类中的header信息也被写入到文件中去了,于是,我就header的写入注释掉了,发现问题解决了。
// 修改为从指定位置开始读取
public void writeTo(OutputStream out) throws IOException {
// 这里如果不把header去掉的话,就会将头部一些信息写入到文件开头里,造成文件损坏!
// out.write(header);
// updateProgress(header.length);
FileInputStream inputStream = new FileInputStream(file);
inputStream.skip(startPos); // 从指定位置开始读
final byte[] tmp = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(tmp)) != -1) {
out.write(tmp, 0, bytesRead);
updateProgress(bytesRead);
}
out.write(CR_LF);
updateProgress(CR_LF.length);
out.flush();
AsyncHttpClient.silentCloseInputStream(inputStream);
}
需要完整代码的请在留言里注明邮箱,谢谢!
作者:woshiwangbiao 发表于2016/10/19 12:57:50 原文链接
阅读:23 评论:0 查看评论