您的位置:首页 > 移动开发 > Android开发

android上实现multi-part上传

2015-09-08 00:11 465 查看
最近做http的图片上传,约定好用http的multipart实现。遇到的一些坑,网上查不到不到解决方案,自己解决了,在这儿记录下来。

android自带的http实现有两种,一个是java的HttpUrlConnection,一个是apache的HttpClient,在项目里用的是后者,因为感觉它的封装程度使用起来更方便些。

之前使用的是一个国人写的基于HttpClient的框架,用着各种坑。最近接入七牛的sdk时发现人家用的是android-async-http,于是去github找到了这个,看起来挺不错的,决定换之。幸好之前觉得国人那个框架不靠谱,自己封装了个中间层,换库代价没那么大。

大坑仍然是multi-part上传图片,因为上传前有时需要压缩图片,所以传的参数是InputStream而不是File。虽然也可以把InputStream转为File或bytes,但感觉那样太蛋疼了,还是想直接传,于是问题来了。

发现,这个库上传图片的InputStream时,进度马上走到100%,像是被直接读进缓存了。这个当然不行,进度条完全就一摆设,上网查到了说法:
https://github.com/loopj/android-async-http/issues/118
原来是这个库的缺陷,有个家伙把文件上传进度失效的问题搞了,但是InputStream的没搞,虽然自己搞完后感觉人家那种搞法并没什么卵用。求人不如求己,决定自己改下这个库。

multipart的实现主要在SimpleMultipartEntity里面,isChunked()直接返回的false,十分怀疑这个能否实现文件的上传进度。学人家解决文件上传的方式处理了一下InputStream,结果进度还是直接到100%,所以这种方法并没用。七牛能实现进度,是因为他们自己把文件分成了多块,用多个请求分别上传,对于我并不适用。

RequestParams:
public HttpEntity getEntity(ResponseHandlerInterface progressHandler) throws IOException {
if (useJsonStreamer) {
return createJsonStreamerEntity(progressHandler);
} else if (!forceMultipartEntity && streamParams.isEmpty() && fileParams.isEmpty() && fileArrayParams.isEmpty()) {
return createFormEntity();
} else {
return createMultipartEntity(progressHandler);
}
}
找到RequestParams,发现其实他的成员变量都是protected,不过createMultipartEntity是private的,只能改为protected再重写。

public class IhpRequestParams extends RequestParams {

private static final Charset UTF_8 = Charset.forName("UTF-8");

@Override
protected HttpEntity createMultipartEntity(final ResponseHandlerInterface progressHandler) throws IOException {
MultipartEntity multipartEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null, UTF_8) {
@Override
public void writeTo(OutputStream outstream) throws IOException {
super.writeTo(new CountingOutputStream(outstream, getTotalLength(), progressHandler));
}

};
for (BasicNameValuePair nameValuePair : getParamsList()) {
multipartEntity.addPart(new FormBodyPart(nameValuePair.getName(), new StringBody(nameValuePair.getValue(), UTF_8)));
}

for (ConcurrentHashMap.Entry<String, StreamWrapper> entry : streamParams.entrySet()) {
multipartEntity.addPart(entry.getKey(), new InputStreamBody(entry.getValue().inputStream, entry.getValue().contentType, entry.getValue().name));
}
return multipartEntity;
}

private int getTotalLength() {
int length = 0;
for (ConcurrentHashMap.Entry<String, StreamWrapper> entry : streamParams.entrySet()) {
try {
length += entry.getValue().inputStream.available();
} catch (IOException e) {
e.printStackTrace();
}
}
return length;
}

public class CountingOutputStream extends FilterOutputStream {

private long transferred;
private long totalLength;
private long progress;
private ResponseHandlerInterface responseHandler;

public CountingOutputStream(final OutputStream out, long totalLength, ResponseHandlerInterface responseHandler) {
super(out);
this.totalLength = totalLength;
this.transferred = 0;
this.responseHandler = responseHandler;
}

@Override
public void write(int oneByte) throws IOException {
super.write(oneByte);
if (null == responseHandler) {
return;
}
this.transferred ++;
long progressNew = transferred * 100 / totalLength;
if (progressNew > progress || progressNew >= 100) {
progress = progressNew;
responseHandler.sendProgressMessage(transferred, totalLength);
}
}

}
}
引用了httpmime-4.1.3的MultipartEntity, 其实比较新的httpmime库已经推荐用MultipartEntityBuilder了,但没怎么去了解。反正用这个有问题还可能通过继承解决,用builder如果有问题的话继承都没得搞了。进度条的问题通过包装OutputStream解决。

本以为这样万事大吉了,没想到9个上传请求发出时进度条都很快走完,然后卡在100%,最后成功几个,挂掉几个。

查log发现,挂掉的都是SocetTimeout。联想进度条瞬间走完的表现,可以推测出,这些请求并没有按预期实现chunk上传。而是都先读进缓存,最后9个几百k的数据抢着上传,抢慢的就超时了。android-async-http默认的socket超时是10秒,改成30秒后果然好了很多。

在grepcode上找到了4.1.3的MultipartEntity的源码,发现其实只要传进了InputStreamPart,他是会使用chunk上传的。带着疑惑用fiddler抓包看了下,这个multipart上传确实是chunk编码的。

难道chunk编码的chunk size太大了?上网查了下,默认是2k,排除这个因素。

看AsynHttpClient代码,HttpConnectionsParams已经setTcpNoDelay为true,setSocketBufferSize为8192,应该也不存在数据留在缓冲区不发出去的问题。

决定看下writeTo的那个OutputStream的情况,断点调试发现是一个ChunkedOutputStream,去grepcode看源码,发现包了个SocketOutputBuffer,层层递进,最后能找到SocketImpl!

有了这个,就能直接确定socket的状况了。于是,通过这个OutputStream层层反射拿到了SocketImpl实例。断点调试一查看,send buffer的大小竟然有1m多。

妹的,HttpConnectionsParams的setSocketBufferSize方法根本不是我以为的效果啊。不过也没那么多精力再去细究了,反正httpclient的库应该是比较固定的,决定hack一下。

public static void setChunkedSocketSendBufferSize(ChunkedOutputStream out, int size) {
SocketOutputBuffer socketOutputBuffer = getOutputBuffer(out);
if (null == socketOutputBuffer) {
return;
}

SocketImpl socketImpl = getSocket(socketOutputBuffer);
if (null != socketImpl) {
try {
socketImpl.setOption(SocketOptions.SO_SNDBUF, size);
} catch (SocketException e) {
e.printStackTrace();
}

4000
}
}

public static SocketOutputBuffer getOutputBuffer(ChunkedOutputStream os) {
Class c = ChunkedOutputStream.class;
try {
Field field = c.getDeclaredField("out");
field.setAccessible(true);
return (SocketOutputBuffer) field.get(os);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static SocketImpl getSocket(SocketOutputBuffer socketOutputBuffer) {
try {
OutputStream outputStream = getSocketOutputStream(socketOutputBuffer);
Class c = outputStream.getClass();
Field field = c.getDeclaredField("socketImpl");
field.setAccessible(true);
return (SocketImpl) field.get(outputStream);
}
catch (Exception e) {
return null;
}
}

public static OutputStream getSocketOutputStream(SocketOutputBuffer socketOutputBuffer) {
try {
Class c = AbstractSessionOutputBuffer.class;
Field field = c.getDeclaredField("outstream");
field.setAccessible(true);
return (OutputStream) field.get(socketOutputBuffer);
}
catch (Exception e) {
return null;
}
}
通过各种反射把socket的send buffer size改为8k, 终于成功实现了正常的上传进度,把自己都感动了。。。。。 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息