Android 网络框架 多线程下载的原理与实现

网络框架系列的博客中,前面那张已经了解了一下 OKHttp 的同步请求,异步请求,还有了解了一下 OKHttp 实现 Http 请求缓存的原理和实现,这篇我们主要来了解一下多线程下载。

一、粗略了解多线程下载的原理

1.Http 相关的字段

之前也提过网络框架的核心就是 Http 协议,还为此写了一篇博客,在多线程下载中,也是离不开 Http 协议的,对,如果对于用惯开源网络框架的开发者来说,可能不会有什么感觉,因为优秀的开源框架都帮我们封装好了,使用起来也相当的方便,可是为了更好的进阶,我们还是得了解一下别人是如何实现的。我们都知道,多线程下载就是分块下载,因此分块下载的话,我们就必需获取到需要下载文件的大小才能更好的多线程下载是吧?因此我们需要知道文件的大小,那么就可## ##以通过 Http 协议中的Transfer-Encoding:chunked ,content-length 的字段来获取文件的大小了。跟多线程下载有关的字段有哪些呢?

(1) Transfer-Encoding:chunked

定义:分块传输编码(Chunked transfer encoding)是超文本传输协议(HTTP)中的一种数据传输机制,允许HTTP由网页服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供。

通常,HTTP 应答消息中发送的数据是整个发送的,Content-Length 消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。

如果一个 HTTP 消息(请求消息或应答消息)的 Transfer-Encoding 消息头的值为 chunked,那么,消息体由数量未定的块组成,并以最后一个大小为 0 的块为结束。每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个 CRLF (回车及换行),然后是数据本身,最后块 CRLF 结束。在一些实现中,块大小和 CRLF 之间填充有白空格(0x20)。
最后一块是单行,由块大小(0),一些可选的填充白空格,以及 CRLF 。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。
消息最后以 CRLF 结尾。

chunk 编码将数据分成一块一块的发生。Chunked 编码将使用若干个 Chunk 串连而成,由一个标明长度为0 的 chunk 标示结束。每个 Chunk 分为头部和正文两部分,头部内容指定正文的字符总数(十六进制的数字 )和数量单位(一般不写),正文部分就是指定长度的实际内容,两部分之间用回车换行(CRLF) 隔开。在最后一个长度为 0 的 Chunk 中的内容是称为 footer 的内容,是一些附加的 Header 信息(通常可以直接忽略)。

总的来说:如果出现了 Transfer-Encoding:chunked 那处理起来就是相当的麻烦。

(2) content-length

定义:Conent-Length 表示实体内容长度,客户端(服务器)可以根据这个值来判断数据是否接收完成

那么这里就有个问题了,如果在传输的 Http 中没有 Conent-Length 这个请求头的信息呢,我们怎么才能知道数据是否接收完成呢?

在解决上面提出的问题之前,我们需要清楚什么时候才会出现这个问题,很明显,当客户端需要向服务器请求一张图片或一份文件的时候,服务器是可以知道这张图片或者这份文件的大小的,然后就可以通过 Content-Length 请求头信息把图片或文件的大小告诉客户端,需要接收多少的数据。可是如果服务器一边产生数据,一边发送数据给客户端呢?这个时候,服务器是没办法知道文件的大小的,因此服务器就需要使用 Transfer-Encoding: chunked 这样的方式来代替 Content-Length

但是有一点我们必须要清楚:有了 Transfer-Encoding,则不能有 Content-Length ,为什么这样说呢?因为如果存在 Transfer-Encoding(重点是chunked),则在 header 中不能有 Content-Length,有也会被忽视。

(3) Range

定义:用于请求头中,指定第一个字节的位置和最后一个字节的位置

这个有什么用呢?为什么跟多线程有关系呢?

多线程下载就是分成多个线程下载,文件大小为300个byte,那么你的分割方式可以为:0-99 (前100个字节),100-199(第二个100字节),200-299(第三个100字节) 。分割完成后,每个线程都有自己的任务,比如线程3的任务是负责下载200-299这部分文件,现在的问题是:线程3发送一个什么样的请求报文,才能够保证只请求文件的200-299字节,而不会干扰其他线程的任务。这时,我们可以使用HTTP1.1的Range头。Range头域可以请求实体的一个或者多个子范围,Range的值为0表示第一个字节,也就是Range计算字节数是从0开始的,最后我们就可以分成这样子:
第三个线程:Range: bytes=200-299
第二个线程:Range: bytes=100-199
第一个线程:Range: bytes=0-99

2.设计多线程下载

多线程下载的核心就是分块下载,通过上面 Http 的相关字段,我们基本就可以了解分块下载的实现,那么多线程下载是不是就这样就结束了呢?不是的,我们要考虑的问题还有很多很多,我经常说的一句话,任何一个功能都包含了设计者的理念。就算是一个看似很简单的功能,设计者也是考虑了很多问题的,就比如现在的多线程下载,看似只要实现了分块下载就完成了,其实不是的,我们还要考虑的问题还多着呢。

(1) 多线程下载文件,文件存储的位置放在哪里呢?
当然是放在 SD 卡上,可是现在有很多的手机是没有 SD 卡的,因此我们需要判断存储文件的位置

(2) 文件空间大小是否足够呢?
文件空间大小的判断这个就不用多说了,如果文件空间不够,就会下载失败,跑出异常

(3) 数据库保存的数据,突然停止了下载,突然断开网络,数据库记录的数据该是怎样的?
当然我们要存储每个线程下载的数据是多少,突然停止了,数据库该保存什么信息,这些都是我们需要考虑的。

(4) 如果需要显示下载的进度条,该如何显示?
这需要我们注意只有主线程才能更新UI,所以该用何种方式展现才是更好的,这也是我们需要考虑的。这里就有个问题了,真的只有主线程才能更新UI吗?这是我之前看到的一篇文章《真的只有主线程才能更新UI吗?》,写的挺好的。

(5) 当然多线程下载,少不了线程安全的问题?还有怎么管理线程的问题呢?

两点水 wechat
扫一扫订阅我的微信公众号