如何在OSS上實(shí)現(xiàn)大文件的斷點(diǎn)下載和上傳
OSS(開(kāi)放存儲(chǔ)服務(wù))是面向海量非結(jié)構(gòu)化數(shù)據(jù)對(duì)象的存儲(chǔ)服務(wù)。隨著云計(jì)算的普及和飛速增長(zhǎng),越來(lái)越多的開(kāi)發(fā)者把他們的應(yīng)用建筑在了 OSS之上,然而OSS的開(kāi)發(fā)資料還不是很多。我在這里拋磚引玉,通過(guò)回答一個(gè)經(jīng)常被問(wèn)及的問(wèn)題——如何在OSS上實(shí)現(xiàn)大文件的斷點(diǎn)續(xù)傳功能——希望有更多的高手來(lái)一同分享自己在云存儲(chǔ)上的開(kāi)發(fā)經(jīng)驗(yàn)。
OSS對(duì)外提供的是RESTful形式的接口,其最重要的特點(diǎn)之一是無(wú)狀態(tài)性(statelessness),即OSS服務(wù)器不會(huì)保持除了單次請(qǐng)求之外的,任何與其通信的客戶端的通信狀態(tài)。因此對(duì)于斷點(diǎn)續(xù)傳這樣有狀態(tài)功能的實(shí)現(xiàn),關(guān)鍵點(diǎn)在于如何在客戶端完成狀態(tài)維護(hù) 。在以下章節(jié)中,我以Python為例,介紹如何實(shí)現(xiàn)OSS的斷點(diǎn)下載和斷點(diǎn)上傳。
1. 在OSS上實(shí)現(xiàn)大文件的斷點(diǎn)下載:
所謂斷點(diǎn)下載,就是要從文件已經(jīng)下載的地方開(kāi)始繼續(xù)下載。為了方便理解,我們先來(lái)看一個(gè)從OSS下載一個(gè)文件保存到本地的Python例子。在這個(gè)例子[1]中,我們從一個(gè)名為 “lingyun”的bucket里面,下載一個(gè)叫“example.dat”的文件,并且以相同名字保存在當(dāng)前目錄。
from oss_api import *
HOST="oss.aliyuncs.com"
BUCKET = "lingyun"
OBJECT = "example.dat"
ACCESS_ID = "*******************"
SECRET_ACCESS_KEY = "*******************"
#下載文件
oss = OssAPI(HOST, ACCESS_ID, SECRET_ACCESS_KEY)
res = oss.get_object(BUCKET, OBJECT)
#保存文件
if 200 == res.status:
f = file(OBJECT, 'w')
f.write(res.read())
f.close()
print "Download succeeded."
else:
print "Download failed."
基于上面的代碼,下面的程序顯示了增加斷點(diǎn)續(xù)傳功能的文件下載代碼,變化的地方用粗體標(biāo)注出來(lái)了:
from oss_api import *
HOST="oss.aliyuncs.com" #ads
BUCKET = "lingyun"
OBJECT = "example.dat"
BUFFER_SIZE = 10240 # 寫(xiě)入數(shù)據(jù)的buffer大小
ACCESS_ID = "*******************"
SECRET_ACCESS_KEY = "*******************"
oss = OssAPI(HOST, ACCESS_ID, SECRET_ACCESS_KEY)
# 流式地將數(shù)據(jù)寫(xiě)入文件
def flush_data(file, http_res):
while True:
data = res.read(BUFFER_SIZE)
if len(data) != 0:
file.write(data)
else:
break
# 獲取本地文件長(zhǎng)度
f = file(OBJECT, 'a')
file_len = f.tell()
# 設(shè)置HTTP Header里面的Range參數(shù),跳過(guò)已經(jīng)收到的數(shù)據(jù)
headers = {}
headers["range"] = "bytes=" + str(file_len) + "-"
res = oss.get_object(BUCKET, OBJECT, headers)
if 206 == res.status: # 返回指定范圍內(nèi)的數(shù)據(jù)
flush_data(f, res)
print "Download succeeded."
else: # 下載失敗
print "Download failed."
f.close()
這段代碼和前段代碼相比,有四處發(fā)生了變化:
1) 增加了流式寫(xiě)入本地文件的邏輯。防止下載的數(shù)據(jù)對(duì)象過(guò)大,無(wú)法一下子讀入本地的內(nèi)存中。
2) 向OSS發(fā)送數(shù)據(jù)前,獲取本地文件長(zhǎng)度。
3) 構(gòu)造HTTP的Range Header,要求OSS從指定的位置開(kāi)始下載。
4) 判斷OSS返回的HTTP值,并做出相應(yīng)的處理:如果OSS返回206,說(shuō)明下載的是指定位置范圍內(nèi)的數(shù)據(jù);其他狀態(tài)碼表明“Range”參數(shù)錯(cuò)誤或者發(fā)生異常。
在使用“Range”這個(gè)HTTP 參數(shù)時(shí),請(qǐng)注意以下三點(diǎn):
·Range參數(shù)中的文件位置是從0開(kāi)始,最大值是文件長(zhǎng)度減1
·如果Range參數(shù)填寫(xiě)錯(cuò)誤,OSS將忽視這個(gè)參數(shù)[2]。
·Range參數(shù)設(shè)置正確的話,OSS將返回HTTP狀態(tài)碼206(不是200)以表示返回的是部分?jǐn)?shù)據(jù)
通過(guò)“Range”參數(shù),還可以實(shí)現(xiàn)大文件的并發(fā)下載。這個(gè)功能作為思考題留給各位讀者,感興趣的讀者可以自己實(shí)現(xiàn)一下。OSS官方的SDK里面也提供了一個(gè)多線程下載功能的實(shí)現(xiàn),供大家參考。
2. 在OSS上實(shí)現(xiàn)大文件的斷點(diǎn)上傳:
相對(duì)于斷點(diǎn)下載,斷點(diǎn)上傳的實(shí)現(xiàn)顯然要復(fù)雜得多。OSS提供的解決辦法可以理解為:在客戶端將大文件切分成若干適合公網(wǎng)傳輸?shù)男?shù)據(jù)塊;然后將這些小數(shù)據(jù)塊分別上傳到OSS上;最后在OSS服務(wù)器端將這些小數(shù)據(jù)塊合并成最終的文件。為了實(shí)現(xiàn)這個(gè)功能,OSS單獨(dú)發(fā)布一套上傳API接口——Multipart Upload。這套API接口共有6個(gè):
· Initiate Multipart Upload:初始化一個(gè)Multipart Upload事件
·Upload Part:上傳數(shù)據(jù)塊
· Complete Multipart Upload:完成一個(gè)Multipart Upload事件
· Abort Multipart Upload:中止一個(gè)Multipart Upload事件
·List Multipart Uploads:列出所有存在的Multipart Upload事件
·List Parts:列出某個(gè)Multipart Upload事件下的所有數(shù)據(jù)塊
這套接口中定義了兩個(gè)唯一識(shí)別碼(UUID):Upload ID和Part ID,分別用于標(biāo)識(shí)某個(gè)Multipart Upload上傳事件和某個(gè)數(shù)據(jù)塊。一個(gè)完整的Multipart上傳過(guò)程由以下幾步組成:
1) Initiate Multipart Upload: 初始化一個(gè)Multipart Upload事件
客戶端通知OSS要上傳一個(gè)大文件,OSS返回給客戶端一個(gè)唯一標(biāo)識(shí)這次Multipart上傳事件的Upload ID。Python示例代碼如下:
oss = OssAPI(HOST, ACCESS_ID, SECRET_ACCESS_KEY)
res = oss.init_multi_upload(BUCKET, OBJECT)
下面是OSS返回的HTTP結(jié)果示例:
BUCKET
OBJECT
0004D4184129F5A1A42663160C4C58B1
其中“0004D4184129F5A1A42663160C4C58B1”就是OSS為這次Multipart Upload事件分配的Upload ID。通過(guò)這個(gè)接口,用戶只是在OSS上注冊(cè)了一個(gè)Multipart Upload事件,并沒(méi)有任何文件被創(chuàng)建或改變。你可以對(duì)同一個(gè)文件創(chuàng)建多個(gè)Multipart Upload事件,在這些Multipart Upload事件沒(méi)有完成(Complete)或被中止(Abort)之前,它們都是同時(shí)存在的。
2) Upload Part:上傳數(shù)據(jù)塊
在客戶端將大文件切分成多個(gè)適合公網(wǎng)傳輸大小(建議5MB)的數(shù)據(jù)塊(Part),然后分別上傳到OSS上,并告知OSS這些數(shù)據(jù)塊屬于某個(gè)Upload ID。Python 示例代碼如下:
oss = OssAPI(HOST, ACCESS_ID, SECRET_ACCESS_KEY)
res = oss. upload_part (BUCKET, OBJECT, data, upload_id, part_id )
其中,“data”表示要上傳的Part數(shù)據(jù)內(nèi)容;“upload_id”為此次上傳事件的ID;“part_id”是該數(shù)據(jù)塊的索引。Part ID不但唯一標(biāo)識(shí)這一數(shù)據(jù)塊,還標(biāo)識(shí)了這個(gè)數(shù)據(jù)塊在整個(gè)文件內(nèi)的相對(duì)位置。如果你在同一個(gè)Upload ID下,使用一個(gè)已上傳過(guò)的Part ID上傳了新的數(shù)據(jù),那么OSS上已有的這個(gè)part數(shù)據(jù)將被覆蓋。除了最后一塊Part數(shù)據(jù)沒(méi)有大小限制以外,其他的Part數(shù)據(jù)不能小于5MB。Part ID的有效范圍是1~10000。OSS并不要求屬于同一個(gè)Upload ID的Part ID必須是連續(xù)的,比如:用戶可以只上傳Part ID為1、16、51的數(shù)據(jù)塊;但Part ID的大小表示了數(shù)據(jù)塊之間的相對(duì)位置,例如Part ID為16的數(shù)據(jù)塊,在整個(gè)文件中必須在Part ID為51的數(shù)據(jù)塊之前。Upload Part命令執(zhí)行成功后,OSS會(huì)返回這個(gè)Part數(shù)據(jù)的MD5值給客戶端。用戶需要保存這些MD5值,以便在OSS上最后生成文件時(shí)使用。
3) Complete Multipart Upload:完成一個(gè)Multipart Upload事件
在上傳完所有的數(shù)據(jù)塊到OSS上之后,我們就可以要求OSS在服務(wù)器端將指定的某個(gè)Upload ID所屬的數(shù)據(jù)塊組合成最終的文件。在執(zhí)行該操作時(shí),客戶端需要提供一個(gè)XML格式的文件,其中詳細(xì)列舉出了該文件所需的Part ID及其對(duì)應(yīng)的MD5值。一個(gè)XML的例子如下:
|
<CompleteMultipartUpload> <Part> <PartNumber>1</PartNumber> <ETag>1DC6D29FD1E1989793B83F5C2FD0C5E0</ETag> </Part> <Part> <PartNumber>16</PartNumber> <ETag>E17AC4037030A1227D1C1B115619C6F1</ETag> </Part> <Part> <PartNumber>51</PartNumber> <ETag>807014FC970ED07BA28DE40B20E5BD59</ETag> </Part> </CompleteMultipartUpload> |
當(dāng)我們構(gòu)建好這個(gè)XML文件后,就可以通過(guò)調(diào)用OSS Python SDK的接口來(lái)發(fā)送完成Multipart Upload事件的請(qǐng)求,代碼示例如下:
oss = OssAPI(HOST, ACCESS_ID, SECRET_ACCESS_KEY)
res = oss.complete_upload(BUCKET, OBJECT, upload_id, part_msg_xml)
OSS收到提交的XML列表后,會(huì)逐一判斷每個(gè)Part是否存在,以及對(duì)應(yīng)的MD5值是否和客戶端提供的MD5值相等。當(dāng)所有的Part驗(yàn)證通過(guò)后,OSS將把這些數(shù)據(jù)Part組合成一個(gè)最終的Object。需要注意的是,用戶可以在這次請(qǐng)求里,不指定所有已經(jīng)上傳的Part。例如,剛才我們成功上傳了1、16和51共三個(gè)數(shù)據(jù)塊到某個(gè)Upload ID名下,我們可以只指定用Part 1、51來(lái)組成最后的文件(注意Part的ID仍然要求是升序的)。當(dāng)OSS生成最終的文件后,會(huì)將沒(méi)有用到的16號(hào)Part刪除,以釋放磁盤(pán)空間。
整個(gè)Multipart Upload流程的Python偽代碼如下所示:
#初始化OSS對(duì)象
oss = OssAPI(HOST, ACCESS_ID, SECRET_ACCESS_KEY)
# 初始化Multipart Upload事件并獲得Upload ID
upload_id = init_multi_upload(oss, bucket_name, object_name)
# 將本地文件分解成為多個(gè)part,并計(jì)算出每個(gè)part的起始位置和長(zhǎng)度
(pos_list, len_list) = split_file_to_part_list(file_name)
# 開(kāi)啟多個(gè)線程來(lái)上傳part
thread_pool = []
for index in range(0, thread_sum):
# 在create_thread_worker 里調(diào)用OSS API上傳指定的part,上傳結(jié)果保存在upload_res
upload_thread = create_thread_worker (oss, file_name, pos_list[index],
len_list[index], upload_res[index])
thread_pool.append(upload_thread)
upload_thread.start()
# 等待所有線程結(jié)束
for upload_thread in thread_pool:
upload_thread.join()
# 創(chuàng)建最終合成文件的part列表(XML格式)
part_msg_xml = create_part_xml(upload_res)
# 要求OSS完成本次Multipart Upload事件
res = complete_multipart_upload(oss, bucket, object, upload_id, part_msg_xml)
上面的例子中,使用到了OSS提供的三個(gè)接口。其余的三個(gè)接口主要提供了對(duì)Upload ID和Part ID的查詢和刪除,方便用戶的管理。由于篇幅原因,這三個(gè)接口就不在這里做展開(kāi)說(shuō)明了,感興趣的朋友可以參考《OSS API文檔》里面的相應(yīng)章節(jié)。
在OSS提供的Multipart Upload方法中,由于各個(gè)數(shù)據(jù)塊之間是相互獨(dú)立的,所以在傳輸過(guò)程中,如果任何一個(gè)數(shù)據(jù)塊傳輸失敗或者進(jìn)程被掛起,只需要客戶端記錄下每個(gè)數(shù)據(jù)塊的上傳狀態(tài),下次重啟上傳進(jìn)程時(shí),繼續(xù)上傳那些還未上傳成功的數(shù)據(jù)塊即可,這樣就實(shí)現(xiàn)了斷點(diǎn)上傳功能。另外,通過(guò)這個(gè)接口,還可以實(shí)現(xiàn)大文件的并發(fā)上傳、向OSS流式地寫(xiě)入數(shù)據(jù)等功能,有興趣的讀者可以自己實(shí)現(xiàn)一下。
后記:
希望通過(guò)這篇文章,大家可以對(duì)如何使用OSS進(jìn)行大文件的斷點(diǎn)下載和上傳的方法有所了解,也希望更多的朋友能分享更多更好的使用OSS的經(jīng)驗(yàn)。
[1]為了便于理解,本文的代碼實(shí)例忽略了一些簡(jiǎn)單的出錯(cuò)處理以及極端情況的判斷邏輯。
[2] 如果其他參數(shù)都合法,這個(gè)請(qǐng)求將符合get object請(qǐng)求的語(yǔ)法,OSS會(huì)返回整個(gè)object的內(nèi)容,而不是用戶期望的部分?jǐn)?shù)據(jù)。





























