天天看点

如何通过云存储实现大文件的断点下载和上传

背景

对象存储 oss是面向海量非结构化数据对象的存储服务。随着云计算的普及和飞速增长,越来越多的开发者把他们的应用建筑在了 oss之上。oss对外提供的是restful形式的接口,其最重要的特点之一是无状态性(statelessness),即oss服务器不会保持除了单次请求之外的,任何与其通信的客户端的通信状态。因此对于断点续传这样有状态功能的实现,关键点在于如何在客户端完成状态维护 。

本文将以python为例,介绍通过oss是实现大文件的断点下载和断点上传的。

具体操作步骤

<b>(一)在oss上实现大文件的断点下载</b>

所谓断点下载,就是要从文件已经下载的地方开始继续下载。为了方便理解,我们先来看一个从oss下载一个文件保存到本地的python例子。在这个例子[1]中,我们从一个名为 “lingyun”的bucket里面,下载一个叫“example.dat”的文件,并且以相同名字保存在当前目录。

基于上面的代码,下面的程序显示了增加断点续传功能的文件下载代码,变化的地方加粗标注出来了:

 from oss_api import *

  host="oss.aliyuncs.com" #ads

  bucket = "lingyun"

  object = "example.dat"

 <b> buffer_size = 10240 # 写入数据的buffer大小</b>

  access_id = "*******************"

  secret_access_key = "*******************"

  oss = ossapi(host, access_id, secret_access_key)

  <b># 流式地将数据写入文件</b>

<b>  def flush_data(file, http_res):</b>

<b>  while true:</b>

<b>  data = res.read(buffer_size)</b>

<b>  if len(data) != 0:</b>

<b>  file.write(data)</b>

<b>  else:</b>

<b>  break</b>

  # 获取本地文件长度

  <b>f = file(object, 'a')</b>

<b>  file_len = f.tell()</b>

  # 设置http header里面的range参数,跳过已经收到的数据

 <b> headers = {}</b>

<b>  headers["range"] = "bytes=" + str(file_len) + "-"</b>

  res = oss.get_object(bucket, object, headers)

  <b>if 206 == res.status: # 返回指定范围内的数据</b>

<b>  flush_data(f, res)</b>

  print "download succeeded."

  else: # 下载失败

  print "download failed."

  f.close()

这段代码和前段代码相比,有四处发生了变化:

<b>增加了流式写入本地文件的逻辑。</b>防止下载的数据对象过大,无法一下子读入本地的内存中;

 <b>向oss发送数据前,获取本地文件长度。</b>

 <b>构造http的range header,要求oss从指定的位置开始下载。</b>

 <b>判断oss返回的http值,并做出相应的处理:如果oss返回206,说明下载的是指定位置范围内的数据;其他状态码表明“range”参数错误或者发生异常。</b>

在使用“range”这个http 参数时,请注意以下三点:

 range参数中的文件位置是从0开始,最大值是文件长度减1;

 如果range参数填写错误,oss将忽视这个参数[2];

 range参数设置正确的话,oss将返回http状态码206(不是200)以表示返回的是部分数据。

<b>(二)在oss上实现大文件的断点上传:</b>

相对于断点下载,断点上传的实现显然要复杂得多。oss提供的解决办法可以理解为:在客户端将大文件切分成若干适合公网传输的小数据块;然后将这些小数据块分别上传到oss上;最后在oss服务器端将这些小数据块合并成最终的文件。为了实现这个功能,oss单独发布一套上传api接口——multipart upload。这套api接口共有6个:

 initiate multipart upload:初始化一个multipart upload事件;

 upload part:上传数据块;

 complete multipart upload:完成一个multipart upload事件;

 abort multipart upload:中止一个multipart upload事件;

 list multipart uploads:列出所有存在的multipart upload事件;

 list parts:列出某个multipart upload事件下的所有数据块。

这套接口中定义了两个唯一识别码(uuid):upload id和part id,分别用于标识某个multipart upload上传事件和某个数据块。一个完整的multipart上传过程由以下几步组成:

<b>(1)initiate multipart upload: 初始化一个multipart upload事件</b>

客户端通知oss要上传一个大文件,oss返回给客户端一个唯一标识这次multipart上传事件的upload id。python示例代码如下:

下面是oss返回的http结果示例:

  bucket

  object

  0004d4184129f5a1a42663160c4c58b1

其中“0004d4184129f5a1a42663160c4c58b1”就是oss为这次multipart upload事件分配的upload id。通过这个接口,用户只是在oss上注册了一个multipart upload事件,并没有任何文件被创建或改变。你可以对同一个文件创建多个multipart upload事件,在这些multipart upload事件没有完成(complete)或被中止(abort)之前,它们都是同时存在的。

<b>(2) upload part:上传数据块</b>

在客户端将大文件切分成多个适合公网传输大小(建议5mb)的数据块(part),然后分别上传到oss上,并告知oss这些数据块属于某个upload id。python 示例代码如下:

  res = oss. upload_part (bucket, object, data, upload_id, part_id )

其中,“data”表示要上传的part数据内容;“upload_id”为此次上传事件的id;“part_id”是该数据块的索引。part id不但唯一标识这一数据块,还标识了这个数据块在整个文件内的相对位置。如果你在同一个upload id下,使用一个已上传过的part id上传了新的数据,那么oss上已有的这个part数据将被覆盖。除了最后一块part数据没有大小限制以外,其他的part数据不能小于5mb。part id的有效范围是1~10000。oss并不要求属于同一个upload id的part id必须是连续的,比如:用户可以只上传part id为1、16、51的数据块;但part id的大小表示了数据块之间的相对位置,例如part id为16的数据块,在整个文件中必须在part id为51的数据块之前。upload part命令执行成功后,oss会返回这个part数据的md5值给客户端。用户需要保存这些md5值,以便在oss上最后生成文件时使用。

<b>(3)complete multipart upload:完成一个multipart upload事件</b>

在上传完所有的数据块到oss上之后,我们就可以要求oss在服务器端将指定的某个upload id所属的数据块组合成最终的文件。在执行该操作时,客户端需要提供一个xml格式的文件,其中详细列举出了该文件所需的part id及其对应的md5值。一个xml的例子如下:

当我们构建好这个xml文件后,就可以通过调用oss python sdk的接口来发送完成multipart upload事件的请求,代码示例如下:

  res = oss.complete_upload(bucket, object, upload_id, part_msg_xml)

oss收到提交的xml列表后,会逐一判断每个part是否存在,以及对应的md5值是否和客户端提供的md5值相等。当所有的part验证通过后,oss将把这些数据part组合成一个最终的object。需要注意的是,用户可以在这次请求里,不指定所有已经上传的part。例如,刚才我们成功上传了1、16和51共三个数据块到某个upload id名下,我们可以只指定用part 1、51来组成最后的文件(注意part的id仍然要求是升序的)。当oss生成最终的文件后,会将没有用到的16号part删除,以释放磁盘空间。

整个multipart upload流程的python伪代码如下所示:

在oss提供的multipart upload方法中,由于各个数据块之间是相互独立的,所以在传输过程中,如果任何一个数据块传输失败或者进程被挂起,只需要客户端记录下每个数据块的上传状态,下次重启上传进程时,继续上传那些还未上传成功的数据块即可,这样就实现了断点上传功能。另外,通过这个接口,还可以实现大文件的并发上传、向oss流式地写入数据等功能,有兴趣的读者可以自己实现一下。

后记

希望通过这篇文章,大家可以对如何使用oss进行大文件的断点下载和上传的方法有所了解,也希望更多的朋友能分享更多更好的使用oss的经验。

<b>注释:</b>[1]为了便于理解,本文的代码实例忽略了一些简单的出错处理以及极端情况的判断逻辑。

[2] 如果其他参数都合法,这个请求将符合get object请求的语法,oss会返回整个object的内容,而不是用户期望的部分数据。

相关链接:

oss api 文档简介:

<a href="https://help.aliyun.com/document_detail/31947.html?spm=5176.788315709.6.222.oxssdy">https://help.aliyun.com/document_detail/31947.html?spm=5176.788315709.6.222.oxssdy</a>