安卓:使用HttpURLConnection上传带有进度回调的照片至Cloudinary

8
我试图修改 Cloudinary 的开源库以便监听我的照片上传进度。该库的类包括一个 MultipartUtility Java 类,我修改了它以监听上传进度。
在修改前,原始代码可以在 Github 上找到:https://github.com/cloudinary/cloudinary_java/blob/master/cloudinary-android/src/main/java/com/cloudinary/android/MultipartUtility.java 我实际上将它修改成了另一个云服务 CloudFS 的代码,CloudFS 支持上传文件/图像等时显示上传进度: https://github.com/bitcasa/CloudFS-Android/blob/master/app/src/main/java/com/bitcasa/cloudfs/api/MultipartUpload.java
package com.cloudinary.android;

import com.cloudinary.Cloudinary;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;

/**
 * This utility class provides an abstraction layer for sending multipart HTTP
 * POST requests to a web server.
 *
 * @author www.codejava.net
 * @author Cloudinary
 */
public class MultipartUtility {
    private final String boundary;
    private static final String LINE_FEED = "\r\n";
    private static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
    private HttpURLConnection httpConn;
    private String charset;
    private OutputStream outputStream;
    private PrintWriter writer;
    UploadingCallback uploadingCallback;
    public final static String USER_AGENT = "CloudinaryAndroid/" + Cloudinary.VERSION;
    Long filesize;

    public void setUploadingCallback(UploadingCallback uploadingCallback) {
        this.uploadingCallback = uploadingCallback;
    }

    /**
     * This constructor initializes a new HTTP POST request with content type is
     * set to multipart/form-data
     *
     * @param requestURL
     * @param charset
     * @throws IOException
     */
    public MultipartUtility(String requestURL, String charset, String boundary, Map<String, String> headers, Long filesize) throws IOException {
        this.charset = charset;
        this.boundary = boundary;
        this.filesize = filesize;
        URL url = new URL(requestURL);
        httpConn = (HttpURLConnection) url.openConnection();
        httpConn.setDoOutput(true); // indicates POST method
        httpConn.setDoInput(true);
        httpConn.setFixedLengthStreamingMode(filesize); //added this in

        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpConn.setRequestProperty(header.getKey(), header.getValue());
            }
        }
        httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
        httpConn.setRequestProperty("User-Agent", USER_AGENT);
        outputStream = httpConn.getOutputStream();
        writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true);
    }

    public MultipartUtility(String requestURL, String charset, String boundary) throws IOException {
        this(requestURL, charset, boundary, null, 0L);
    }

    /**
     * Adds a form field to the request
     *
     * @param name  field name
     * @param value field value
     */
    public void addFormField(String name, String value) {
        writer.append("--" + boundary).append(LINE_FEED);
        writer.append("Content-Disposition: form-data; name=\"" + name + "\"").append(LINE_FEED);
        writer.append("Content-Type: text/plain; charset=" + charset).append(LINE_FEED);
        writer.append(LINE_FEED);
        writer.append(value).append(LINE_FEED);
        writer.flush();
    }

    /**
     * Adds a upload file section to the request
     *
     * @param fieldName  name attribute in {@code <input type="file" name="..." />}
     * @param uploadFile a File to be uploaded
     * @throws IOException
     */
    public void addFilePart(String fieldName, File uploadFile, String fileName) throws IOException {
        if (fileName == null) fileName = uploadFile.getName();
        FileInputStream inputStream = new FileInputStream(uploadFile);
        addFilePart(fieldName, inputStream, fileName);
    }

    public void addFilePart(String fieldName, File uploadFile) throws IOException {
        addFilePart(fieldName, uploadFile, "file");
    }

    public void addFilePart(String fieldName, InputStream inputStream, String fileName) throws IOException {
        if (fileName == null) fileName = "file";
        writer.append("--" + boundary).append(LINE_FEED);
        writer.append("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"").append(LINE_FEED);
        writer.append("Content-Type: ").append(APPLICATION_OCTET_STREAM).append(LINE_FEED);
        writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
        writer.append(LINE_FEED);
        writer.flush();

        int progress = 0;
        byte[] buffer = new byte[4096];
        int bytesRead = -1;

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            progress += bytesRead;
/*            int percentage = ((progress / filesize.intValue()) * 100);*/
            if (uploadingCallback != null) {
                uploadingCallback.uploadListener(progress);
            }

        }
        outputStream.flush();
        writer.flush();
        uploadingCallback = null;
        inputStream.close();
        writer.append(LINE_FEED);
        writer.flush();
    }

    public void addFilePart(String fieldName, InputStream inputStream) throws IOException {
        addFilePart(fieldName, inputStream, "file");
    }

    /**
     * Completes the request and receives response from the server.
     *
     * @return a list of Strings as response in case the server returned status
     * OK, otherwise an exception is thrown.
     * @throws IOException
     */
    public HttpURLConnection execute() throws IOException {
        writer.append("--" + boundary + "--").append(LINE_FEED);
        writer.close();

        return httpConn;
    }

}

我所做的更改是按照这个帖子的推荐添加以下内容到httpURLConnection:如何在Android中实现文件上传进度条httpConn.setFixedLengthStreamingMode(filesize); 接着,我创建了一个简单的接口以监听上传进度:
public interface UploadingCallback {

    void uploadListener(int progress);

}

然后,当 HttpURLConnection 写入照片时,我将其附加:

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            progress += bytesRead;
/*            int percentage = ((progress / filesize.intValue()) * 100);*/
            if (uploadingCallback != null) {
                uploadingCallback.uploadListener(progress);
            }

        }

代码运行了,但上传进度似乎没有被正确地测量。照片大约有365kb,上传大约花费了0.1秒(我在17:56:55.481开始上传,在17:56:55.554时完成,这超过了0.7秒)。我不认为我的互联网连接速度那么快,预计至少需要5秒钟才能完成。我有一种感觉它正在测量将照片写入缓冲区的时间,而不是发送到Cloudinary服务器所需的时间。
如何让它测量上传照片所需的时间,以便我可以使用数据来显示进度条?
04-24 17:56:55.481 28306-28725/com.a  upload  4096
04-24 17:56:55.486 28306-28725/com.a  upload  8192
04-24 17:56:55.486 28306-28725/com.a  upload  12288
04-24 17:56:55.486 28306-28725/com.a  upload  16384
04-24 17:56:55.487 28306-28725/com.a  upload  20480
04-24 17:56:55.487 28306-28725/com.a  upload  24576
04-24 17:56:55.487 28306-28725/com.a  upload  28672
04-24 17:56:55.487 28306-28725/com.a  upload  32768
04-24 17:56:55.491 28306-28725/com.a  upload  36864
04-24 17:56:55.492 28306-28725/com.a  upload  40960
04-24 17:56:55.493 28306-28725/com.a  upload  45056
04-24 17:56:55.493 28306-28725/com.a  upload  49152
04-24 17:56:55.493 28306-28725/com.a  upload  53248
04-24 17:56:55.493 28306-28725/com.a  upload  57344
04-24 17:56:55.494 28306-28725/com.a  upload  61440
04-24 17:56:55.494 28306-28725/com.a  upload  65536
04-24 17:56:55.494 28306-28725/com.a  upload  69632
04-24 17:56:55.494 28306-28725/com.a  upload  73728
04-24 17:56:55.494 28306-28725/com.a  upload  77824
04-24 17:56:55.495 28306-28725/com.a  upload  81920
04-24 17:56:55.495 28306-28725/com.a  upload  86016
04-24 17:56:55.495 28306-28725/com.a  upload  90112
04-24 17:56:55.495 28306-28725/com.a  upload  94208
04-24 17:56:55.495 28306-28725/com.a  upload  98304
04-24 17:56:55.495 28306-28725/com.a  upload  102400
04-24 17:56:55.495 28306-28725/com.a  upload  106496
04-24 17:56:55.496 28306-28725/com.a  upload  110592
04-24 17:56:55.496 28306-28725/com.a  upload  114688
04-24 17:56:55.496 28306-28725/com.a  upload  118784
04-24 17:56:55.497 28306-28725/com.a  upload  122880
04-24 17:56:55.498 28306-28725/com.a  upload  126976
04-24 17:56:55.498 28306-28725/com.a  upload  131072
04-24 17:56:55.498 28306-28725/com.a  upload  135168
04-24 17:56:55.498 28306-28725/com.a  upload  139264
04-24 17:56:55.499 28306-28725/com.a  upload  143360
04-24 17:56:55.506 28306-28725/com.a  upload  147456
04-24 17:56:55.510 28306-28725/com.a  upload  151552
04-24 17:56:55.510 28306-28725/com.a  upload  155648
04-24 17:56:55.514 28306-28725/com.a  upload  159744
04-24 17:56:55.515 28306-28725/com.a  upload  163840
04-24 17:56:55.517 28306-28725/com.a  upload  167936
04-24 17:56:55.517 28306-28725/com.a  upload  172032
04-24 17:56:55.518 28306-28725/com.a  upload  176128
04-24 17:56:55.518 28306-28725/com.a  upload  180224
04-24 17:56:55.518 28306-28725/com.a  upload  184320
04-24 17:56:55.519 28306-28725/com.a  upload  188416
04-24 17:56:55.519 28306-28725/com.a  upload  192512
04-24 17:56:55.519 28306-28725/com.a  upload  196608
04-24 17:56:55.519 28306-28725/com.a  upload  200704
04-24 17:56:55.520 28306-28725/com.a  upload  204800
04-24 17:56:55.525 28306-28725/com.a  upload  208896
04-24 17:56:55.526 28306-28725/com.a  upload  212992
04-24 17:56:55.527 28306-28725/com.a  upload  217088
04-24 17:56:55.530 28306-28725/com.a  upload  221184
04-24 17:56:55.530 28306-28725/com.a  upload  225280
04-24 17:56:55.530 28306-28725/com.a  upload  229376
04-24 17:56:55.530 28306-28725/com.a  upload  233472
04-24 17:56:55.530 28306-28725/com.a  upload  237568
04-24 17:56:55.531 28306-28725/com.a  upload  241664
04-24 17:56:55.532 28306-28725/com.a  upload  245760
04-24 17:56:55.532 28306-28725/com.a  upload  249856
04-24 17:56:55.532 28306-28725/com.a  upload  253952
04-24 17:56:55.533 28306-28725/com.a  upload  258048
04-24 17:56:55.533 28306-28725/com.a  upload  262144
04-24 17:56:55.535 28306-28725/com.a  upload  266240
04-24 17:56:55.540 28306-28725/com.a  upload  270336
04-24 17:56:55.540 28306-28725/com.a  upload  274432
04-24 17:56:55.541 28306-28725/com.a  upload  278528
04-24 17:56:55.541 28306-28725/com.a  upload  282624
04-24 17:56:55.543 28306-28725/com.a  upload  286720
04-24 17:56:55.545 28306-28725/com.a  upload  290816
04-24 17:56:55.545 28306-28725/com.a  upload  294912
04-24 17:56:55.547 28306-28725/com.a  upload  299008
04-24 17:56:55.547 28306-28725/com.a  upload  303104
04-24 17:56:55.547 28306-28725/com.a  upload  307200
04-24 17:56:55.547 28306-28725/com.a  upload  311296
04-24 17:56:55.547 28306-28725/com.a  upload  315392
04-24 17:56:55.548 28306-28725/com.a  upload  319488
04-24 17:56:55.548 28306-28725/com.a  upload  323584
04-24 17:56:55.548 28306-28725/com.a  upload  327680
04-24 17:56:55.548 28306-28725/com.a  upload  331776
04-24 17:56:55.549 28306-28725/com.a  upload  335872
04-24 17:56:55.549 28306-28725/com.a  upload  339968
04-24 17:56:55.549 28306-28725/com.a  upload  344064
04-24 17:56:55.550 28306-28725/com.a  upload  348160
04-24 17:56:55.550 28306-28725/com.a  upload  352256
04-24 17:56:55.551 28306-28725/com.a  upload  356352
04-24 17:56:55.551 28306-28725/com.a  upload  360448
04-24 17:56:55.552 28306-28725/com.a  upload  364544
04-24 17:56:55.554 28306-28725/com.a  upload  365790

为了让您自己测试这个功能,您需要在Cloudinary网站上创建一个免费账户,以便获取您的“cloudname”,这样您就可以将Android SDK连接到他们的服务,从而实现直接从Android上传未签名的文件到他们的服务器。
编辑:
这是我尝试过的方法,但实际上上传完成需要7秒钟,进度条却在0.7秒内跳至100%。
    while ((bytesRead = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, bytesRead);
        progress += bytesRead;
        Log.d("MultiPart", "file transferred so far: "
                + progress);
        if (uploadingCallback != null) {
            uploadingCallback.uploadListener(progress);
        }
        Log.d("Flushing", "flush the writer");
        outputStream.flush();
        writer.flush();
    }
2个回答

3
使用flush()方法和调用update callback()的时间存在问题。
从您的代码中可以看出,每次读取图片的一部分时,都会将其写入输出缓冲区,但这并不意味着它已经发送到服务器,它可能被缓存,然后稍后才写入服务器。
您有两个选择,要么在每个outputStream.write()之后调用outputStream.flush(),但这会降低上传的性能,因为您将失去缓冲的好处。
要么在方法末尾的outputStream.flush()之后调用updateCallback()。因为在outputStream.flush()之后,您可以确定数据已经在服务器上,并且进度已经完成。
有关flush的更多信息,请参见此线程 What is the purpose of flush() in Java streams?

你的第二个选择几乎没有意义,因为那也会达到100%,而我尝试做的整个目的是为了获得上传进度指示器。我尝试了你的第一个选择,但似乎不起作用,我已经进行了编辑以展示我所尝试的内容。 - Simon
那么你需要做类似于ug__提议的事情。你应该偶尔调用flush() :), 但是从你的更改中我可以看出,即使这样也不起作用。我需要测试一下并让你知道,但我99%确定调用flush()是正确的方法。也许你的代码的其他部分存在问题。我需要测试一下,我会立即去做。 - aleksamarkoni
从技术上讲,在输出流上调用flush不会有任何作用,因为文档中说它什么也不做:https://docs.oracle.com/javase/7/docs/api/java/io/OutputStream.html#flush() 我将其转换为DataOutputStream以查看现在是否会有所不同。实际上没有任何区别。 - Simon
是的。完全没有问题。要自己测试,请尝试将Cloudinary库集成到测试应用程序中-您还可以按照此处的说明来集成和自定义库:http://stackoverflow.com/questions/29103479/how-to-show-progress-while-upload-file-to-cloudinary-in-android/36811892#36811892 - Simon
可能是使用httpConn.setFixedLengthStreamingMode(filesize)时,操作系统会在0.7秒内缓冲整个文件,然后才进行真正的上传。如果你设置为4096,就是告诉它在缓冲区达到4096大小时开始发送。 - aleksamarkoni
显示剩余2条评论

1
这是一次尝试,因为我没有在Android环境下测试过,但我建议尝试以下方法。

不要使用固定长度,而是使用setChunkedStreamingMode
//httpConn.setFixedLengthStreamingMode(filesize);
httpConn.setChunkedStreamingMode(4096); // or whatever size you see fit

这样做应该会触发请求的部分,每当您发送4096字节的数据时,从而清空内部缓冲区。


你也可以尝试在每次写入后手动刷新缓冲区,但这可能会减慢文件上传速度,特别是如果你刷新得太频繁,然而这可能会解决你的问题。你可能需要尝试不同的缓冲区大小来找到最佳效果。
while ((bytesRead = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, bytesRead);
    progress += bytesRead;
    /* int percentage = ((progress / filesize.intValue()) * 100);*/
    if (uploadingCallback != null) {
        uploadingCallback.uploadListener(progress);
    }
    // trigger the stream to write its data
    outputStream.flush();
 }

如果进行上述任何一种更改,您很可能希望让用户选择设置自己的缓冲区大小,而不是传入总文件大小。例如,将您的构造函数更改为以下内容:

MultipartUtility(String requestURL, String charset, 
                 String boundary, Map<String, String> headers, int chunkSize)

我有点担心使用setChunkedStreamingMode,因为文档中写道:旧的HTTP/1.0服务器可能不支持此模式。我不知道cloudinary正在使用什么类型的服务器,所以它可能无法正常工作。fixedlengthstreamingmode似乎没有这个限制。无论如何,我也尝试了您的刷新建议,但似乎不起作用。请参见我的问题编辑以查看我尝试了什么。 - Simon
@Simon,文档说明URLConnection实现是为RFC 2616设计的,这是HTTP/1.1规范,如果您要连接的服务器不支持该规范,我会感到非常惊讶,因为这是当今Web上的标准。我正在查看http://developer.android.com/reference/java/net/HttpURLConnection.html的第一句话。 - ug_

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