URLConnection的getContentLength()方法返回了一个负数值

10

这是我的代码:

url = paths[0];
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
int length = connection.getContentLength(); // i get negetive length
InputStream is = (InputStream) url.getContent();
byte[] imageData = new byte[length]; 
int buffersize = (int) Math.ceil(length / (double) 100);
int downloaded = 0;
int read;
while (downloaded < length) {
    if (length < buffersize) {
        read = is.read(imageData, downloaded, length);
    } else if ((length - downloaded) <= buffersize) {
        read = is.read(imageData, downloaded, length - downloaded);
    } else {
        read = is.read(imageData, downloaded, buffersize);
    }
    downloaded += read;
    publishProgress((downloaded * 100) / length);
}
Bitmap bitmap = BitmapFactory.decodeByteArray(imageData, 0,
        length);
if (bitmap != null) {
    Log.i(TAG, "Bitmap created");
} else {
    Log.i(TAG, "Bitmap not created");
}
is.close();
return bitmap;

我查看了Java文档,长度是负数是因为以下原因:

"内容的字节数,如果无法确定则为负数。如果内容长度已知但超过Long.MAX_VALUE,则返回负数。"

这可能是什么原因?我正在尝试下载一张图片。值得注意的是,这是我尝试下载图像的第四种方法。其他三种方法在这里提到。

编辑:

如所请求,这是我使用的完整方法。

protected Bitmap getImage(String imgurl) {

    try {
        URL url = new URL(imgurl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        int length = connection.getContentLength();
        InputStream is = (InputStream) url.getContent();
        byte[] imageData = new byte[length];
        int buffersize = (int) Math.ceil(length / (double) 100);
        int downloaded = 0;
        int read;
        while (downloaded < length) {
            if (length < buffersize) {
                read = is.read(imageData, downloaded, length);
            } else if ((length - downloaded) <= buffersize) {
                read = is.read(imageData, downloaded, length
                        - downloaded);
            } else {
                read = is.read(imageData, downloaded, buffersize);
            }
            downloaded += read;
        //  publishProgress((downloaded * 100) / length);
        }
        Bitmap bitmap = BitmapFactory.decodeByteArray(imageData, 0,length);
        if (bitmap != null) {
             System.out.println("Bitmap created");
        } else {
            System.out.println("Bitmap not created");
        }
        is.close();
        return bitmap;
    } catch (MalformedURLException e) {
        System.out.println(e);
    } catch (IOException e) {
        System.out.println(e);
    } catch (Exception e) {
        System.out.println(e);
    }
    return null;
}
6个回答

35

默认情况下,HttpURLConnection的实现请求服务器使用gzip压缩。
由于getContentLength()返回传输的字节数,因此您不能使用该方法预测可以从getInputStream()读取多少字节。
相反,读取该流直到它耗尽:当read()返回-1时。
可以通过在请求标头中设置可接受的编码来禁用Gzip压缩:

 urlConnection.setRequestProperty("Accept-Encoding", "identity");

那么试试这个:

HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Accept-Encoding", "identity"); // <--- Add this line
int length = connection.getContentLength(); // i get negetive length

来源(性能段落):http://developer.android.com/reference/java/net/HttpURLConnection.html


谢谢,非常完美的答案 :) - Kalpesh
哇!这个可行。我差点就要把我的笔记本扔了 :P 非常感谢! - Qadir Hussain

14

这可能有两个常见的解释:

  1. 未知内容长度,或者更具体地说,服务器没有在响应消息中设置“Content-Length”标头。

  2. 内容长度大于Integer.MAX_VALUE。如果发生这种情况,getContentLength()返回-1。(Javadoc建议使用getContentLengthLong()而不是getContentLength()来避免这个问题。)

无论哪种方式,最好不要预先分配固定大小的字节数组来保存图像。

  • 一种替代方案是创建一个本地的ByteArrayOutputStream并将从套接字读取的字节复制到其中。然后调用toByteArray获取完整的字节数组。

  • 另一种替代方案是将数据保存在文件系统中的临时文件中。

显然,造成这种情况的一个常见根本原因是某些实现默认请求对响应数据进行“gzip”编码。这会强制服务器将内容长度设置为-1。可以通过以下方式防止这种情况:

connection.setRequestProperty("Accept-Encoding", "identity");

...但这意味着响应不会被压缩。所以这(在我看来)是一个次标准的解决方案。


您现有的客户端代码在另一方面也存在问题。如果发生IOException或其他异常,代码块将“异常退出”而不关闭URLConnection。这将导致文件描述符泄漏。这样做太多次,您的应用程序将因文件描述符或本地端口号耗尽而失败。

最佳实践是使用try/finally确保始终关闭链接到外部资源的URLConnections、Sockets、Streams等。


基于(所谓的)内容长度预分配缓冲区将使您面临拒绝服务攻击的风险。想象一下,如果坏人向您发送很多带有危险性极高的“Content-Length”头的请求,然后缓慢发送数据会发生什么。可能出现OOME或更糟糕的情况。


您建议在应用程序或服务器端进行的任何更改。这里需要添加的任何代码。 - pradeep
我确实捕获了异常,我并没有只是在上面发布了那段代码,所有上面的代码都位于try catch语句中。 - pradeep
不仅仅是捕获异常的问题。你还必须在finally块中关闭连接。 - Stephen C
嗨,我已经添加了我在编辑中使用的方法的完整代码。 - pradeep
是的。我明白了。正如我之前解释的那样,如果在打开连接和关闭输入流之间抛出异常,你的代码很有可能泄漏套接字文件描述符。 - Stephen C
当服务器使用分块传输编码(通常用于流式传输的大型有效载荷)时,内容长度是未知的,不能依赖它。 试图为整个响应预分配缓冲区是非常糟糕的形式,你能说“拒绝服务”吗? 有关传输编码的答案,请参见下面的回答... - escape-llc

3
似乎该服务器在其响应头中没有提供Content-Length,你是否从响应头中获取了Transfer-Encoding=chunked头?
我的情况是:我执行了HttpURLConnection并考虑服务器会对我响应“Content-Length”为正值,但它没有,然后我转向AndroidHttpClient,这是android HttpClient实现,再次执行相同的请求并获得正确的Content-Length。
我使用Wireshark分析了两个请求,发现请求头有一点不同。
使用AndroidHttpClient的标头列表:
---------------------------------- request headers
Range : bytpe=0-
Host : download.game.yy.com
Connection : Keep-Alive
User-Agent : com.duowan.mobile.netroid

---------------------------------- response headers
Server : nginx
Content-Type : text/plain; charset=utf-8
ETag : "535e2578-84e350"
Cache-Control : max-age=86400
Accept-Ranges : bytes
Content-Range : bytes 0-8708943/8708944
Content-Length : 8708944

使用HttpURLConnection的请求头列表:

---------------------------------- request headers
Range : bytpe=0-
Host : download.game.yy.com
Connection : Keep-Alive
User-Agent : com.duowan.mobile.netroid
Accept-Encoding : gzip    // difference here

---------------------------------- response headers
Server : nginx
Content-Type : text/plain; charset=utf-8
Cache-Control : max-age=86400
Transfer-Encoding : chunked
X-Android-Received-Millis : 1398861612782
X-Android-Sent-Millis : 1398861608538

请求头的唯一区别是Accept-Encoding,这不是我添加的,而是Android默认设置为HttpURLConnection,之后,我将其设置为identity,然后再次执行请求,以下是完整的头部堆栈:

---------------------------------- request headers
Range : bytpe=0-
Host : download.game.yy.com
Connection : Keep-Alive
User-Agent : com.duowan.mobile.netroid
Accept-Encoding : identity

---------------------------------- response headers
Server : nginx
Content-Type : text/plain; charset=utf-8
ETag : "535e2578-84e350"
Cache-Control : max-age=86400
Accept-Ranges : bytes
Content-Range : bytes 0-8708943/8708944
Content-Length : 8708944
X-Android-Received-Millis : 1398862186902
X-Android-Sent-Millis : 1398862186619

正如您所看到的,当我将Accept-Encoding设置为“identity”替换系统默认值“gzip”后,服务器提供了“Content-Length”正值,这就是AndroidHttpClient能够获得正确的Content-Length值而HttpURLConnection不能的原因。

gzip压缩编码可能会导致分块响应,由服务器端考虑。如果服务器认为您可以接收分块编码响应,则可能不提供Content-Length头部信息。尝试禁用gzip可接受的行为,然后查看与之有何不同。


请注意:关闭传输编码可能会在服务器上引起性能问题,因为很可能无法再“流式”您请求的内容,并且必须在发送之前“计算”整个响应。 最好的方案是不依赖该标头,而是直接进行流式传输。这与压缩等无关。 - escape-llc

0

虽然我来的有点晚,但这可能会对某些人有所帮助。我遇到了同样的问题,每当我尝试获取内容长度时,我总是得到-1的值。

之前我使用以下方法来获取内容长度。

long totalByte=connection.getContentLength();

我使用了以下方法解决了我的问题。

long totalByte=Long.parseLong(connection.getHeaderField("Content-Length"));

0

我在我的壁纸应用程序中也遇到了这个问题。问题是因为您的服务器在其HTTP标头中没有提供Content-Length。以下是一个正常的HTTP标头快照,其中包含Content-length

enter image description here

我正在使用共享主机,因此无法更改服务器配置。在我的应用程序中,我使用一个近似值(比我的实际文件大小大)来设置进度对话框的最大值,如下所示:

int filesize = connection.getContentLength();
if(filesize < 0) {
    progressDialog.setMax(1000000);
} else {
    progressDialog.setMax(filesize);
}

您也可以在此处查看我的完整示例源代码:

Android进度对话框示例


0

返回此连接的URL引用的资源内容长度,如果内容长度未知,则返回-1。 - Scott.N

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