使用HttpUrlConnection进行multipart文件上传并显示进度条

9

我想通过 HttpUrlConnection 检查上传文件的进度。我该如何做?我尝试在写入 OutputStream 数据时计算字节数,但是它是错误的,因为真正的上传发生在我调用 conn.getInputStream() 时,所以我需要以某种方式检查 inputStream。这是我的代码:

public static void uploadMovie(final HashMap<String, String> dataSource, final OnLoadFinishedListener finishedListener, final ProgressListener progressListener) {
  if (finishedListener != null) {
    new Thread(new Runnable() {
       public void run() {
         try {

              String boundary = getMD5(dataSource.size()+String.valueOf(System.currentTimeMillis()));
              MultipartEntityBuilder multipartEntity = MultipartEntityBuilder.create();
              multipartEntity.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);    
              multipartEntity.setCharset(Charset.forName("UTF-8"));

              for (String key : dataSource.keySet()) {
                 if (key.equals(MoviesFragmentAdd.USERFILE)) {
                    FileBody  userFile = new FileBody(new File(dataSource.get(key)));
                    multipartEntity.addPart(key, userFile);
                    continue;
                 }
                 multipartEntity.addPart(key, new StringBody(dataSource.get(key),ContentType.APPLICATION_JSON));
              }

              HttpEntity entity = multipartEntity.build();
              HttpURLConnection conn = (HttpsURLConnection) new URL(URL_API + "/video/addForm/").openConnection();
              conn.setUseCaches(false);
              conn.setDoOutput(true);
              conn.setDoInput(true);
              conn.setRequestMethod("POST");
              conn.setRequestProperty("Accept-Charset", "UTF-8");
              conn.setRequestProperty("Connection", "Keep-Alive");
              conn.setRequestProperty("Cache-Control", "no-cache");
              conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
              conn.setRequestProperty("Content-length", entity.getContentLength() + "");
              conn.setRequestProperty(entity.getContentType().getName(),entity.getContentType().getValue());

              OutputStream os = conn.getOutputStream();
              entity.writeTo(os);
              os.close();

              //Real upload starting here -->>

              BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));

              //<<--

              JsonObject request = (JsonObject) gparser.parse(in.readLine());
              if (!request.get("error").getAsBoolean()) {
              //do something
              }
              conn.disconnect(); 

           } catch (Exception e) {
            e.printStackTrace();
           }
         }
    }).start();

  }
}

嘿,小建议,尝试使用asynchttp库,它简单而实用。 - Ashwin S Ashok
@AshwinSAshok 有没有关于我的问题的教程? - whizzzkey
http://loopj.com/android-async-http/ - Ashwin S Ashok
@AshwinSAshok 伙计,感谢链接,但我找不到任何好的教程关于这个库,如果你正在使用这个库,请你能否给我展示一个例子? - whizzzkey
你有没有想过如何使用HttpURLConnection上传文件到服务器,实现相同的功能呢? - Sirop4ik
2个回答

14

由于你需要处理上传,我想大部分时间都会花在 entity.writeTo(os);上。也许第一次连接服务器时也需要一些时间(DNS解析、SSL握手等)。我认为你设置的“真正上传”的标志不正确。

现在取决于你的Multipart库是否可以拦截writeTo。如果它很聪明和资源高效,它会遍历各个部分并逐一将内容流式传输到输出流中。如果不是这样的话,而且 .build()操作创建了一个庞大的byte[],那么你可以取出这个数组,将其分块流式传输到服务器,并告诉用户已经完成了多少上传百分比。

从资源角度来看,我更喜欢不太了解发生了什么。但如果反馈非常重要,而且电影尺寸只有几兆字节,那么你可以先将Multipart-Entity流式传输到ByteArrayOutputStream,然后将创建的小字节数组的小块写入服务器,同时通知用户进度。以下代码未经验证或测试(你可以将其视为伪代码):

ByteArrayOutputStream baos = new ByteArrayOutputStream();
entity.writeTo(baos);
baos.close();
byte[] payload = baos.toByteArray();
baos = null;

OutputStream os = conn.getOutputStream();

int totalSize = payload.length;
int bytesTransferred = 0;
int chunkSize = 2000;

while (bytesTransferred < totalSize) {
    int nextChunkSize = totalSize - bytesTransferred;
    if (nextChunkSize > chunkSize) {
        nextChunkSize = chunkSize;
    }
    os.write(payload, bytesTransferred, nextChunkSize); // TODO check outcome!
    bytesTransferred += nextChunkSize;

    // Here you can call the method which updates progress
    // be sure to wrap it so UI-updates are done on the main thread!
    updateProgressInfo(100 * bytesTransferred / totalSize);
}
os.close();
更加优雅的方法是编写一个拦截OutputStream,它会记录进度并将真实的写操作委托给底层的“真实”OutputStream。 编辑 @whizzzkey 写道:
我已经多次检查过了 - entity.writeTo(os)不会进行真正的上传,它会执行conn.getResponseCode()或conn.getInputStream()
现在清楚了。HttpURLConnection正在缓冲您的上传数据,因为它不知道内容长度。您已经设置了'Content-length'标头,但显然这被HUC忽略了。您必须调用
conn.setFixedLengthStreamingMode(entity.getContentLength());

那么你最好移除对conn.setRequestProperty("Content-length", entity.getContentLength() + "");的调用。

在这种情况下,HUC可以编写头文件和entity.writeTo(os)可以真正向服务器传输数据。否则,当HUC知道将要传输多少字节时,缓冲的数据会被发送。所以实际上,getInputStream()告诉HUC你已经完成了,但在真正读取响应之前,所有收集的数据都必须发送到服务器。

我不建议更改您的代码,但是对于那些不知道传输数据的确切大小(以字节为单位,而不是字符!!)的人,可以告诉HUC应该以块的方式传输数据,而无需设置确切的内容长度:

conn.setChunkedStreamingMode(-1); // use default chunk size

谢谢您的回答,但是您关于这个问题的看法是错误的:大部分时间都花在了entity.writeTo(os)上。我已经启动了调试模式,并且发现大部分时间都花费在了这一行代码“BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));”,所以问题在于我应该在获取InputStream时检查进度。 - whizzzkey
你的文件有多大?你的网络连接速度是多少?你使用的后端服务器是什么(RoR、ASPX、Java、node.js、PHP等)?你能否在服务器端跟踪流量,例如使用wireshark或tcpdump? - hgoebl
我正在发送一个3MB的文件,我的网络速度是2Mb/s,后端服务器是PHP。很抱歉,我无法跟踪服务器端的流量,因为我没有访问权限。你的意思是问题出在服务器响应时间过长,而entity.writeTo(os)是真正的上传吗? - whizzzkey
我相信:entity.writeTo(os) 是真正的上传过程。如果你增加文件大小和/或限制网络速度,你应该也会意识到这一点。如果你观察到上传时有短暂的延迟,但是在服务器响应(getInputStream)之前有长时间的延迟,那么问题可能是服务器端的(PHP脚本需要时间来处理你的文件)。 - hgoebl
2
我已经多次重新检查过了 - entity.writeTo(os)并不会进行真正的上传,它只是执行conn.getResponseCode()或conn.getInputStream()。 - whizzzkey
显示剩余4条评论

-2

在你的活动中加入这段代码...

public class PublishPostToServer extends AsyncTask implements ProgressListenerForPost {

    public Context pContext;
    public long totalSize;
    private String response;

    public PublishPostToServer(Context context) {
        pContext = context;

    }

    protected void onPreExecute() {
        showProgressDialog();
    }

    @Override
    protected Boolean doInBackground(Void... params) {
        boolean success = true;
        try {
            response = NetworkAdaptor.getInstance()
                    .upLoadMultipartImageToServer(
                            "",
                            "",
                            "", this, this); // Add file path, Authkey, caption 

        } catch (Exception e) {
            success = false;
        }
        return success;
    }

    @Override
    protected void onPostExecute(Boolean result) {
        super.onPostExecute(result);
        //validateResponse(result, response);
    }

    @Override
    protected void onProgressUpdate(Integer... values) {

        try {
            if (mProgressDialog != null) {
                mProgressDialog.setProgress(values[0]);
            }
        } catch (Exception exception) {

        }
    }

    @Override
    public void transferred(long num) {
        publishProgress((int) ((num / (float) totalSize) * 100));
    }

}

private void showProgressDialog() {

    try {
        String dialogMsg = "Uploading Image...";
        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setMessage(dialogMsg);
        mProgressDialog.setIndeterminate(false);
        mProgressDialog.setMax(100);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mProgressDialog.setCancelable(false);
        mProgressDialog.show();
    } catch (Exception exception) {

    }
}

现在,创建一个NetworkAdapter类

public String upLoadMultipartImageToServer(String sourceFileUri, String auth_key, String caption, ProgressListenerForPost listiner, PublishPostToServer asyncListiner) { String upLoadServerUri = "" + "upload_image";

    HttpPost httppost = new HttpPost(upLoadServerUri);

    File file = new File(sourceFileUri);

    if (file.exists()) {

        FileBody filebodyVideo = new FileBody(file);
        CustomMultiPartEntity multipartEntity = new CustomMultiPartEntity(
                HttpMultipartMode.BROWSER_COMPATIBLE, listiner);
        try {
            multipartEntity.addPart("auth_key", new StringBody(auth_key));
            multipartEntity.addPart("caption", new StringBody(caption));
            multipartEntity.addPart("image", filebodyVideo);
            asyncListiner.totalSize = multipartEntity.getContentLength();
            httppost.setEntity(multipartEntity);

        }

        catch (UnsupportedEncodingException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }

        DefaultHttpClient mHttpClient = new DefaultHttpClient();
        String response = "";
        try {
            response = mHttpClient.execute(httppost,
                    new MovieUploadResponseHandler());
        } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return response;
    } else {
        return null;
    }

} 

@SuppressWarnings("rawtypes")
private class MovieUploadResponseHandler implements ResponseHandler {

    @Override
    public Object handleResponse(HttpResponse response)
            throws ClientProtocolException, IOException {

        HttpEntity r_entity = response.getEntity();
        String responseString = EntityUtils.toString(r_entity);
        // DebugHelper.printData("UPLOAD", responseString);

        return responseString;
    }

}

public static boolean isValidResponse(String resultData) {
    try {

    } catch (Exception exception) {
        //DebugHelper.printException(exception);
    }
    return true;
}

public String upLoadVideoToServer(String currentFilePath, String string,
        PublishPostToServer publishPostToServer,
        PublishPostToServer publishPostToServer2) {
    // TODO Auto-generated method stub
    return null;
}

他想要使用HttpUrlConnection。 - Clive Jefferies

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接