我的原文:www.hijerry.cn/p/17326.htm…
前言
幾乎所有的Web站點都需要儲存檔案資源,如圖片、視訊等。也有像百度網盤這樣的平臺專門做雲端儲存,為使用者提供了極大的便利。
基礎知識
HTTP請求報文分為請求頭、請求體。請求頭中的Content-Type
欄位,描述了請求體是什麼型別的內容,它的欄位值也叫做MIME
型別,也叫mimetype
,在這裡進行查詢:www.w3school.com.cn/media/media… 。
有關Content-Type、媒體型別等詳細知識可以參考《HTTP權威指南》,PDF版下載(密碼:7u67)。
有關使用HTTP協議上傳檔案的原理,可以參考:https://www.cnblogs.com/cswuyg/p/3185164.html。
版本1
理解了上傳檔案的原理後,我們可以完成一個較為基礎的檔案儲存函式,下面程式碼是基於Laravel5編寫。
/**
* 儲存上傳的檔案至伺服器,並返回URL
* @param Illuminate\Http\UploadedFile $file 上傳的檔案
* @return string 檔案在伺服器中的URL,失敗返回false
*/
function storeFile(UploadedFile $file) {
$store_path = 'uploads';
// 檔名稱 xxx.yy
$name = time();
if ($extension = $file->extension()) {
$name .= $extension;
}
if (Storage::putFileAs($store_path, $file, $name)) {
return '/storage/uploads/' . $name;
} else {
return false;
}
}
複製程式碼
storeFile
函式接受一個上傳檔案例項,儲存成功後返回該檔案對應的URL,失敗返回false。
做法很簡單,把上傳的檔案都放到一個HTTP可訪問的目錄下,得到URL,使用者訪問此URL即可訪問上傳的資源。
- 第10行。獲取上傳檔案的副檔名。
- 第13行。儲存上傳的檔案到指定的目錄,並指定的檔名。
但是這種做法有幾個缺點甚至有安全漏洞:
- 可上傳自定義指令碼。比如使用者可以上傳自己編寫的PHP檔案,這是非常危險的。就算使用Java等編譯語言進行後臺開發,也應該避免指令碼檔案的上傳。
- 檔案都放在同一個目錄,有的檔案系統會有數量限制。可參考:https://blog.csdn.net/leonwei/article/details/3980179
- 檔名可能衝突(併發量比較大時)。
- 可能有重複檔案。比如兩個人在不同時間點上傳了一樣的檔案,系統會儲存兩份。
版本2:校驗和分類
上程式碼:
/**
* 儲存上傳的檔案至伺服器,並返回URL
* @param Illuminate\Http\UploadedFile $file 上傳的檔案
* @return string 檔案在伺服器中的URL,失敗返回false
*/
function storeFile(UploadedFile $file) {
$time = time();
// 檔名稱 xxx.yy
$name = $time;
if ($extension = $file->extension()) {
$name .= $extension;
}
// 按日期將檔案分類
$store_path = 'uploads' . DIRECTORY_SEPARATOR . date('Ymd', $time);
if (Storage::putFileAs($store_path, $file, $name)) {
return '/storage/' . str_replace('\\', '/', $store_path) . '/' . $name;
} else {
return false;
}
}
/**
* 檢查上傳的檔案是否符合要求
* @param Illuminate\Http\UploadedFile $file 上傳的檔案
* @param array $rule 含mimetype, max_size(單位B)兩個規則
* @return int 符合所有要求返回0,不符合mimetype返回1,不符合max_size返回2
*/
function checkFile(UploadedFile $file, $rule) {
if (! $rule) $rule = ['mimetype' => [], 'max_size' => 0];
if ($rule['mimtype'] && ! in_array($file->getMimeType(), $rule['mimetype'])) {
return 1;
} else if ($rule['max_size'] && $file->getSize() > $rule['max_size']) {
return 2;
} else {
return 0;
}
}
複製程式碼
先呼叫 checkFile
檢查上傳檔案的mimetype、檔案大小是否符合要求,函式返回0表示沒有不符合的。再呼叫 storeFile
儲存檔案。這樣可以儘可能的避免版本1的第一、二個問題。
比如在上傳頭像時,可以規定只能上傳jpg、bmp等圖片格式,而且檔案大小不得超過2M。
下面解釋部分程式碼:
- 第14行。
DIRECTORY_SEPARATOR
是指當前作業系統的分隔字元,Linux是/
,windows是\
。 - 第16行。URL的分隔符都是
/
所以需要替換一下。
檔名生成函式可以替換成 microtime
,這是微秒級別的時間戳,所以就可以避免檔名衝突了(衝突可能性非常非常非常小,所以可以認為不會衝突)。
此版本仍然不能解決資源重複問題。
版本3:統一資源儲存
上程式碼:
/**
* 儲存檔案和model
* @param Illuminate\Http\UploadedFile $file
* @return ResourceModel
*/
public function storeFile(UploadedFile $file) {
// 檔案雜湊值
$this->md = md5_file($file->getRealPath());
// 查詢已存在的資源
if ($exist_rs = ResourceModel::where('md', $this->md)->first()) {
$exist_rs->from_db = true;
return $exist_rs;
}
// 檔名稱 xxx.jpeg
$this->name = $file->getClientOriginalName();
// mimetype image/jpeg
$this->mime = $file->getMimeType();
// 字尾 jpeg
$this->suffix = $file->extension();
DB::beginTransaction();
// 儲存資料庫
if (! $this->save()) {
DB::rollBack();
return false;
}
// 檔案儲存路徑 data/52/08/06
$store_path = $this->getStorePath();
// 520806eb60722ca0d10c89d3b20b370c
$store_name = $this->getStoreName();
// 儲存檔案
if (! Storage::exists($this->getFilename())) {
// 如果寫入檔案失敗,則不存入資料庫中
if (!Storage::putFileAs($store_path, $file, $store_name)) {
DB::rollBack();
return false;
}
}
DB::commit();
return $this;
}
/**
* 顯示檔案
* @param Illuminate\Http\Request $req
* @param $md
* @return mixed
*/
public function showResource(Request $req, $md) {
$rs = ResourceModel::where('md', $md)->first();
if (! $rs) {
return 'resource not found';
} else {
if ($rs->refer) return redirect($rs->refer);
// uploads/7d/s8/7ds87x...
$filename = $rs->getFilename();
// 獲取檔案物理路徑
$full_filename = storage_path("app/${filename}");
if (!file_exists($full_filename)) {
return response('file not found', Response::HTTP_NOT_FOUND);
}
return response()->file($full_filename,[
'Content-Type' => $rs->mime
]);
}
}
複製程式碼
storeFile
方法是 ResourceModel
裡的,這裡羅列一下 Resource
的表結構:
showResource
是controller裡的方法,用於處理HTTP請求。
這個版本的核心思想是,先計算檔案的MD5值,同一個檔案具有一樣的MD5值,不同的檔案MD5值不一樣。這個MD5值將作為檔名。檔案的儲存路徑是取MD5值的前四位字元,兩位分為一組,共兩組,分別作為一級目錄和二級目錄名。
此版本仍然使用 checkFile
檢查檔案型別和大小。
程式碼解釋:
- 第8行:計算檔案的MD5值。
- 第10~13行:找到系統中已經存在的資源,直接返回該資源。這樣可以避免資源重複。
- 第60行:
storage_path
返回檔案的物理儲存地址。 - 第64行:響應資原始檔,並指定響應的Content-Type,讓瀏覽器可以正常的顯示資源。
這個版本已經可以滿足絕大部分Web系統的需求了。並且還有可提升空間,可擴充性也比較強。
舉個擴充套件的例子,資原始檔越多時,可以通過目錄劃分來做分散式儲存。
視訊資源
視訊資源在存放於Web系統之前,往往要進行調整解析度、編碼格式、加水印之類的操作。對於大視訊檔案,也應該採取分片儲存的方式。
調整解析度、編碼格式、加水印等,可以使用 ffmpeg
完成。
檔案分片包含兩個話題:上傳時分片,儲存時分片。上傳時分片,儲存時就比較好操作,直接按片劃分儲存就行;上傳不分片,儲存如果要分片的話,只能能採取視訊擷取的方式(使用ffmpeg)把視訊分為幾個部分儲存,一般來說取前1分鐘為第一片,後續每4分鐘一片,是比較好的分片方法。
視訊檔案分片後,播放時也是要分片播放的,如果採取1:4:4..:4的方法分片,前端需要先載入出第一片1分鐘的視訊,後面在依次載入4分鐘的分片檔案。這裡解釋下為什麼第一片1分鐘,因為使用者一般看1分鐘,沒興趣看就溜了,所以為了不浪費流量,第一片就1分鐘比較合適。
分散式和同步
先看一個需要用到同步的場景,不同學校間共享教學視訊,訪問時又是自己內網的伺服器。要實現這個功能有兩個關鍵點:
- 學校的伺服器至少有一臺能和外部伺服器通訊。
- 採取分散式還是集中式同步。
集中式同步方案:選一箇中心主機,視訊都放在中心主機,並且通過檔案共享手段,讓伺服器在讀取資源時,直接從中心主機讀取。檔案共享手段比如NAS、rsync命令、基於xcopy的指令碼等。需要注意的是,跨校區的同步速度是比較慢的。
分散式同步方案:沒有中心主機,上傳的時候先放在本校伺服器,每一個伺服器定期向其他伺服器詢問是否有新資源,和推送資源給其他伺服器。這種方案,也可以選一箇中心主機,本地伺服器在儲存完資源後,將視訊推送到中心主機,再由中心主機下發到其他學校伺服器(類似git機制)。
他們的核心區別是:集中式,不同伺服器從同一臺伺服器上取視訊資源;分散式,伺服器從本地取視訊資源。