文件断点续传原理

很早就用过阿里云的oss,也了解过他的分片上传断点续传功能,但是当时接触的时候只使用了很基本的上传功能,连api都没用它的,而是自己直接构造tcp请求实现的简单api。近期因为要做一个类似的东西,而且要在app和web端同时支持,所以研究了一下并写在这里记录一下。

我们在下载一个大文件的时候,耗时比较长,可能经常因为网络原因而中断,一旦中断就得重头再来,很显然这是个很糟糕的体验。 于是就有断点续传功能出现,早期的断点续传是需借助客户端或浏览器插件的。后来的HTTP/1.1协议增加了Range,Content-Length,Content-Range,If-Range等字段,配合服务端,便可在浏览器里原生支持断点续传,在这里我不准备详细讲解,这里有篇文章讲的很清楚
讲讲断点续传那点儿事

今天我主要是讲解我碰到的上传问题。 首先,断点续传和分片传输是两个不同的概念,虽然他们有共通的地方且可以互相借用。

分片上传

对于webserver来说,为了安全起见,都会有一个上传数据大小限制,比如NGINX,默认大小只有1M http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size 另外,一般后端CGI也有上传数据和执行时间限制。虽说可以人工把参数调高,但是一则不安全,二则不可能无限调高。分片上传顾名思义,就是把一个文件分割成许多部分,逐一上传。此过程大约分两步。

  1. 在上传开始的时候,要对传输的数据分块。同时还要告诉服务端总的分配数和该分片的顺序。分片不必要固定大小,也不需要顺序上传(甚至可以并行上传)
  2. 服务端在等所有的分片都上传完成后,根据顺序将所有的分片合成一个文件。

需要注意的是,如果选择非顺序上传,那么每段只能分别存储,每上传完一片,服务端就要触发完整性检查( 也可以在客户端完成并通知服务端),传完后就要开始合并清理废弃的分片文件,此过程可以是同步也可以是异步(清理过程可以在完成后就执行也可以定期统一清理)。对于安卓,iOS,还有cli上传很容易实现,但是对于浏览器环境,需要js利用浏览器FileReaderapi来实现。 https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader。考虑到网络的复杂性,每个分片不一定就能一下子上传成功,所以客户端还需要配合进行分片重传,这个过程其实与续传功能有相通的地方。

断点续传

断点续传准确地说是功能,分片是实现断点续传的手段之一,对于大文件,一旦上传中断,就需要重新上传。在网络恢复后,从上次断点处上传。原理也是分片。不过大多数情况下,断点续传的时候是顺序的,每上次一片,就追加到已经上传的那部分后面。在上传前,可以根据文件指纹去服务器寻找上一个分片及所处的offset,如果存在则从下一片所处的offset处开始上传,若不存在则从头下载。对于一些公共网盘来说,很多文件本身就已经存在了,所以根本就不用上传。

其他

以上只是简单的原理介绍,实际上,可能在每个分片传输的时候,还要加上校验码,还需要鉴权等操作。 记得我当年在微博的时候,TimYang 组织过一个比赛,就是如何尽可能快得把文件从一台服务器传到另一个服务器,并且要保证准确性。这个其实就需要利用分片。但是如何分也是个学问。

首先,在传输协议的选择上,我们知道tcp是可靠传输,为了保证可靠性,增加了一些费时操作。这些在我们实现简单文件传输是不利的。所以为了最大的增加传输效率,我们采用UDP协议。

其次,UDP是不可靠的传输,并不保证到达顺序,所以我们要给每个分片里加上分片的编号,并且还要加上校验码。

第三,因为mtu的限制,每个udp分片不可能达到理论的64k,实际要小得多,所以先要通过ping命令或试探找出mtu的值,然后进行分片。

第四,因为涉及重传,所以需要专门开个线程进行tcp长连接来实现消息通知。如果发现有分片传错要通知客户端重传,对于传完后要进行合并。另外因为分片很小,所以如果每个都单独纯粹太费io,所以可以直接追加到一个或少数几个文件里,然后记录每个文件里的分配编号和大小,在传完合并的时候,直接通过seek和length一一找出所有的分片并合成一个文件。