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掉,导致当前请求异常。