android上实现multi-part上传

来源:互联网 发布:软件项目实施方案范文 编辑:程序博客网 时间:2024/05/17 02:12

最近做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();            }        }    }    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, 终于成功实现了正常的上传进度,把自己都感动了。。。。。 




0 0
原创粉丝点击