【问题标题】:Android: Uploading a photo in Cloudinary with progress callback in HttpURLConnectionAndroid:在 Cloudinary 中上传照片,并在 HttpURLConnection 中使用进度回调
【发布时间】:2016-08-18 00:04:30
【问题描述】:

我正在尝试修改 cloudinary 的开源库,以便我可以收听我的照片上传进度。库类包含一个 MultipartUtility java 类,我对其进行了修改以监听上传进度。

修改前的原代码可以在github上找到:https://github.com/cloudinary/cloudinary_java/blob/master/cloudinary-android/src/main/java/com/cloudinary/android/MultipartUtility.java

我确实将其修改为类似于另一个云服务 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 中添加以下内容:How to implement file upload progress bar in 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,上传时间大约是十分之一秒(我在 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 秒内从 0% 跳跃到 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();
    }

【问题讨论】:

    标签: java android httpurlconnection image-uploading cloudinary


    【解决方案1】:

    flush()方法的使用和调用update callback()的时间有问题。

    从代码中可以看出,每次读取部分图片时,都会将其写入输出缓冲区,但这并不意味着它已发送到服务器,它可能会被缓冲,然后稍后再写入到服务器。

    你有两个选择,要么在每个 outputStream.write() 之后调用 outputStream.flush(),但这会降低上传的性能,因为你会失去缓冲的好处。

    或者您可以在方法末尾的 outputStream.flush() 之后调用 updateCallback()。因为在 outputStream.flush() 之后,您确定数据在服务器上,并且该进度已经结束。

    有关刷新的更多信息,请参阅此线程What is the purpose of flush() in Java streams?

    【讨论】:

    • 您的第二种选择几乎没有意义,因为这也将是 100%,而我试图做的全部目的是获取上传的进度指示器。我尝试了您的第一个替代方案,但它似乎不起作用,我进行了编辑以表明您希望我尝试过。
    • 好吧,你需要做类似 ug__ 提议的事情。您应该偶尔调用一次 flush() :),但正如我从您的更改中看到的那样,即使这对您不起作用。我需要对此进行测试并让你知道,但我 99% 确信调用 flush() 是正确的方法。也许您的代码的其他部分存在问题。需要对此进行测试,我将立即进行。
    • 从技术上讲,在 outputstream 上调用 flush 不会做任何事情,因为文档说它什么都不做:docs.oracle.com/javase/7/docs/api/java/io/… 我将它投射到 DataOutputStream 看看它现在是否会有所作为。其实没什么区别。
    • 你试过用这个吗:而不是 httpConn.setFixedLengthStreamingMode(filesize);使用类似 httpConn.setFixedLengthStreamingMode(4096); .在这种情况下,您不应该偶尔使用刷新,因为系统应该每隔 4096 刷新一次。
    • 是的。它根本没有区别。要自己进行测试,请尝试将 cloudinary 库集成到测试应用程序中 - 您还可以按照此处的说明了解如何在集成之前集成和自定义库:stackoverflow.com/questions/29103479/…
    【解决方案2】:

    这是在黑暗中拍摄的,因为我没有在 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();
     }
    

    通过这些更改中的任何一个,您可能希望让用户选择设置自己的缓冲区大小,而不是传递总文件大小。 EG 将您的构造函数更改为以下内容:

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

    【讨论】:

    • 我对使用 setChunkedStreamingMode 有点紧张,因为我在文档中读到:仅旧 HTTP/1.0 的服务器可能不支持此模式。我不知道 cloudinary 使用的是什么类型的服务器,所以它可能无法正常工作。 fixedlengthstreamingmode 似乎没有这个限制。无论如何,我也尝试了您的冲洗建议,但似乎不起作用。请参阅我的问题中的编辑以查看我尝试过的内容。
    • @Simon 文档指出 URLConnection 实现是为 RFC 2616 设计的,它是 HTTP/1.1 规范,如果您尝试连接的服务器不支持该规范,我会感到非常惊讶这些天是网络上的标准。我在看developer.android.com/reference/java/net/HttpURLConnection.html的第一句话
    猜你喜欢
    • 1970-01-01
    • 2016-09-26
    • 2016-09-10
    • 2015-05-06
    • 2019-01-05
    • 1970-01-01
    • 2023-03-07
    • 2020-09-06
    • 2012-07-04
    相关资源
    最近更新 更多