阅读 392

讲透java.net.ProtocolException: unexpected end of stream

1.问题背景

视频下载的过程中,特别是M3U8视频下载,经常会出现如下的异常:

W/System.err: java.net.ProtocolException: unexpected end of stream
W/System.err:     at com.android.okhttp.internal.http.Http1xStream$FixedLengthSource.read(Http1xStream.java:398)
W/System.err:     at com.android.okhttp.okio.RealBufferedSource$1.read(RealBufferedSource.java:372)
W/System.err:     at java.io.InputStream.read(InputStream.java:101)
......
W/System.err:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
W/System.err:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
W/System.err:     at java.lang.Thread.run(Thread.java:919)
复制代码

举一个例子 视频url : www.fztxylgy.com:65/20210218/7E… 网页url : www.smqzj.com/play-49384-…

下载www.fztxylgy.com:65/20210218/7E… 视频,就会出现这个异常,为什么会出现这个问题?

其中一个分片 : www.fztxylgy.com:65/20210218/7E…

contentLength=255304, totalLength=203951, fileLength=203951, url=https://www.fztxylgy.com:65/20210218/7EeCKw5j/1500kb/hls/iOzaXozO.ts
复制代码

发现其中contentLength的长度比fileLength大,说明inputstream读完了之后, 校验contentLength,发现后续的内容没有了,就出现了这个问题.

2.源码追踪

根据Android源码分析: /external/okhttp/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java

377  /** An HTTP body with a fixed length specified in advance. */
378  private class FixedLengthSource extends AbstractSource {
379    private long bytesRemaining;
380
381    public FixedLengthSource(long length) throws IOException {
382      bytesRemaining = length;
383      if (bytesRemaining == 0) {
384        endOfInput();
385      }
386    }
387
388    @Override public long read(Buffer sink, long byteCount) throws IOException {
389      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
390      if (closed) throw new IllegalStateException("closed");
391      if (bytesRemaining == 0) return -1;
392
393      long read = source.read(sink, Math.min(bytesRemaining, byteCount));
394      if (read == -1) {
395        unexpectedEndOfInput(); // The server didn't supply the promised content length.
396        throw new ProtocolException("unexpected end of stream");
397      }
398
399      bytesRemaining -= read;
400      if (bytesRemaining == 0) {
401        endOfInput();
402      }
403      return read;
404    }
405
406    @Override public void close() throws IOException {
407      if (closed) return;
408
409      if (bytesRemaining != 0
410          && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
411        unexpectedEndOfInput();
412      }
413
414      closed = true;
415    }
416  }
复制代码

贴上核心代码:

393      long read = source.read(sink, Math.min(bytesRemaining, byteCount));
394      if (read == -1) {
395        unexpectedEndOfInput(); // The server didn't supply the promised content length.
396        throw new ProtocolException("unexpected end of stream");
397      }
复制代码

bytesRemaining 初始值是 contentLength, 就是我们发送请求, 收到response 中的 "Content-Length"中的标识.

这儿的流程是:

  • 从response中的byte stream中读数据, 每次读之前, 都判断一下bytesRemaining 是否为 0, 如果为0, 说明response已经没有数据了(不管实际上有没有数据, 这时候以contentLength为标准, 反正我读contentLength长度的比特流, 读完为止.
  • 如果每次bytesRemaining 不为0, 但是byte stream中已经读完了, 就是读不到有效数据了, 那也是有问题的, 服务器告诉我应该读contentLength长度的数据, 但是我还没有读到 contentLength长度, 已经没有数据了, 这不是有问题吗?

什么情况下抛出异常?=====》读取请求的byte stream,如果发现读到结尾了,读到的长度和contentLength不一致,就会抛出这个异常。

从下面的源码中发现如果response header中的”Transfer-Encoding“头部是"chunked"的话,就创建一个newChunkedSource实例,如果response header中有contentLength的话,就创建一个FixedLengthSource实例,从字面上就可以理解,FixedLengthSource是确定长度的请求,newChunkedSource是无法确定长度的请求。

135  private Source getTransferStream(Response response) throws IOException {
136    if (!HttpEngine.hasBody(response)) {
137      return newFixedLengthSource(0);
138    }
139
140    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
141      return newChunkedSource(httpEngine);
142    }
143
144    long contentLength = OkHeaders.contentLength(response);
145    if (contentLength != -1) {
146      return newFixedLengthSource(contentLength);
147    }
148
149    // Wrap the input stream from the connection (rather than just returning
150    // "socketIn" directly here), so that we can control its use after the
151    // reference escapes.
152    return newUnknownLengthSource();
153  }
复制代码

既然FixedLengthSource是确定长度的请求处理,说明在请求的过程中contentLength是一个非常重要的校验值,如果发现读取的length和contentLength不一致,系统就会认为当前的请求出来的数据是无法通过校验的。所以就会发生本文刚开始的异常。

上面的代码给我们提供了很有意义的思考, 既然我们在使用FixedLengthSource 实例请求的时候会发生 java.net.ProtocolException: unexpected end of stream 问题, 那我们可以使用 newChunkedSource来尝试解决这类问题吗? 答案显然是可以的.

3.思考

如果在下载视频文件的过程中发生了java.net.ProtocolException: unexpected end of stream异常问题

  • (1) 在抛出java.net.ProtocolException: unexpected end of stream异常之后,确认一下fileLength和contentLength是否一致, 如果不一致的情况下, 可以设置Transfer-Encoding : chunked来躲过客户端的校验
  • (2) 针对一个资源或者同一个服务器的频繁请求,最好设置Connection : close,这样可以防止服务器出现反攻击的问题,服务器发现某短时间的请求过于频繁切长期保持长链接,会引发服务器的瘫痪,所以针对服务器的长链接请求会dismiss掉,导致当前请求异常。
文章分类
Android
文章标签