【问题标题】:Android HttpUrlConnection: Post MultipartAndroid HttpUrlConnection:发布多部分
【发布时间】:2014-02-21 11:06:46
【问题描述】:

我正在尝试将测试文件发布到使用 Android 部署在 tomcat 上的 spring rest servlet。我在 Android 4.1.2 上开发,但我在 4.0.3 上验证了同样的问题。

问题是文件上传需要很长时间(4MB 文件大约需要 70 秒),也在本地网络中。时间与使用 3g 连接相当。我已经排除它可能是服务器问题:使用 curl 执行相同的调用需要 1 / 2 秒,并且使用 apache 作为后端结果是相同的。

使用HttpClient 可以正常工作。

我正在使用 Spring Android RestClient 1.0.1.RELEASE,并且考虑到 Android 版本以及我没有覆盖默认行为的事实,它使用 HttpUrlConnection 而不是 HttpClient 来发出 http 请求。

我还实现了我的自定义 ClientHttpRequestFactory 以操纵 SSL 连接的一些细节,并且我定义了自己的 ClientHttpRequestInterceptor 实现以修改身份验证标头。

我还设置了setBufferRequestBody(false),以避免在大文件上使用OutOfMemoryException。但此属性对所需时间没有影响。

MyClientHttpRequestFactory

public class MyClientHttpRequestFactory extends SimpleClientHttpRequestFactory{

    @Override
    protected void prepareConnection(HttpURLConnection connection,  String httpMethod) throws IOException {
        super.prepareConnection(connection, httpMethod);
        connection.setConnectTimeout(240 * 1000);
        connection.setReadTimeout(240 * 1000);


        if ("post".equals(httpMethod.toLowerCase())) {
            setBufferRequestBody(false);
        }else {
            setBufferRequestBody(true);
        }
    }

@Override
protected HttpURLConnection openConnection(URL url, Proxy proxy) throws IOException {
    final HttpURLConnection httpUrlConnection = super.openConnection(url, proxy);

    if (url.getProtocol().toLowerCase().equals("https")
        &&
        settings.selfSignedCert().get())
    {
        try {
            ((HttpsURLConnection)httpUrlConnection).setSSLSocketFactory(getSSLSocketFactory());
            ((HttpsURLConnection)httpUrlConnection).setHostnameVerifier(new NullHostnameVerifier());
        } catch (Exception e) {
            MyLog.e(LOG_TAG, "OpenConnection", e);
        } 
    } 

    return httpUrlConnection;
}

MyClientHttpRequestInterceptor

public class MyClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        final HttpHeaders headers = request.getHeaders();

        headers.setAuthorization(new HttpBasicAuthentication( settings.username().get(), settings.password().get()));

        if (settings.enable_gzip().get()) {
            headers.setAcceptEncoding(ContentCodingType.GZIP);
        }

        return execution.execute(request, body);
    }
}

这里是我的休息电话:

List<ClientHttpRequestInterceptor> interceptors = Arrays.asList((ClientHttpRequestInterceptor)myClientHttpRequestInterceptor);

RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
restTemplate.setInterceptors(interceptors);

MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("file", new FileSystemResource("/sdcard/test/4MB_file"));

HttpEntity<MultiValueMap> requestEntity = new HttpEntity<MultiValueMap>(parts);
restTemplate.exchange(myUrl, HttpMethod.POST, requestEntity, Integer.class).getBody();

}

查看 Spring Android 源代码,我的请求通过的下一行代码是:

public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());
    if (this.bufferRequestBody) {
        return new SimpleBufferingClientHttpRequest(connection);
    } else {
        return new SimpleStreamingClientHttpRequest(connection, this.chunkSize);
    }
}

因为this.bufferRequestBodyfalse,所以return new SimpleStreamingClientHttpRequest(connection, this.chunkSize); 被执行(with chunkSize = 0)

SimpleStreamingClientHttpRequest(HttpURLConnection connection, int chunkSize) {
    this.connection = connection;
    this.chunkSize = chunkSize;

    // Bugs with reusing connections in Android versions older than Froyo (2.2)
    if (olderThanFroyo) {
        System.setProperty("http.keepAlive", "false");
    }
}

然后:

ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());

delegate.getHeaders().putAll(request.getHeaders());

if (body.length > 0) {
    FileCopyUtils.copy(body, delegate.getBody());
}
return delegate.execute();

我认为这里是所有 android 子系统..

我已经转储tcp流量并分析了它:

POST /urlWherePost HTTP/1.1
Content-Type: multipart/form-data;boundary=nKwsP85ZyyzSDuAqozCTuZOSxwF1jLAtd0FECUPF
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxxx=
Accept-Encoding: gzip
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.1.2; sdk Build/MASTER)
Host: 192.168.168.225:8080
Connection: Keep-Alive
Content-Length: 4096225

--nKwsP85ZyyzSDuAqozCTuZOSxwF1jLAtd0FECUPF
Content-Disposition: form-data; name="file"; filename="4MB_file"
Content-Type: application/octet-stream
Content-Length: 4096000

我尝试使用 curl 重新创建类似的请求:

curl --verbose 
    -H "Connection: Keep-Alive" 
    -H "Content-Type: multipart/form-data" 
    -H "Accept-Encoding: gzip" 
    -H "Content-Disposition: form-data; name=\"file\"; filename=\"4MB_file\"" 
    -H "Content-Type: application/octet-stream" 
    --user xxx:xxx 
    -X POST 
    --form file=@4MB_file 
    http://192.168.168.225:8080/urlWherePost

但是使用 curl 后就可以了。

发布 json 数据不成问题(可能体型较小)。但是当我尝试发送“大”文件时,时间会增加。

查看 DDMS shell,在网络统计信息中,我还发现 TX 中的网络吞吐量从未超过 250kb。似乎有一个bootleneck,但如何调查呢?在哪里可以查看,可以更改哪个参数?

感谢您的任何建议!

【问题讨论】:

  • 你想试试我写的多部分库还是需要使用 Spring?
  • @fabian 这个时候我会避免替换 Spring。但是请给我更多关于这种多部分库的详细信息!
  • 你能给我一个邮件地址什么的吗?我会把代码和一些使用说明发给你
  • irc:irc.abjects.net 房间:#spring_android
  • 你真的需要将所有的messageconverters添加到resttemplate吗?越少越快....?

标签: android performance spring httpurlconnection android-networking


【解决方案1】:

您是否尝试过使用 MultipartEntity 方法?我在从服务器下载大量 JSON 数据时遇到了同样的问题,但我切换到这种方法并捕获了服务器提供给我的所有数据。

HttpClient httpclient = new DefaultHttpClient();
HttpPost httppost = new HttpPost("http://myurl.com");

try {
    MultipartEntity entity = new MultipartEntity();

    entity.addPart("type", new StringBody("json"));
    entity.addPart("data", new JSONObject(data));
    httppost.setEntity(entity);
    HttpResponse response = httpclient.execute(httppost);
} catch (ClientProtocolException e) {
} catch (IOException e) {
} catch (JSONException e){
}

【讨论】:

  • 感谢您的回复,但我有 HttpUrlConnection 的问题,而不是 HttpClient 的问题。如我的问题所述, HttpClient 有效!无论如何,谢谢!
【解决方案2】:

要上传大文件,你可以使用这个库android-async-http

【讨论】:

    【解决方案3】:

    为了简单易用,我推荐这个库https://github.com/koush/ion。 我在我的项目中使用它并且效果很好。

    【讨论】:

      猜你喜欢
      • 2018-10-15
      • 1970-01-01
      • 2014-12-20
      • 2015-11-12
      • 1970-01-01
      • 2011-01-02
      • 2012-04-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多