PHP之文件的鎖定、上傳與下載
小結文件的鎖定機制、上傳和下載
1.文件鎖定
現在都在講究什麼分布式、並發等,實際上文件的操作也是並發的,在網絡環境下,多個用戶在同一時刻訪問頁面,對同一服務器上的同一文件進行著讀取,如果,這個用戶剛好讀到一半,另一個用戶就寫入了消息,那麼前一個用戶讀到的就是錯誤數據,在數據庫裡面好像是稱為髒數據,而如果某用戶寫到一半時,另一用戶也對該文件進行寫操作,那麼就造成了寫入數據的混亂和錯誤,因此才php有一個鎖機制,類似於數據庫的鎖,當某用戶在對文件操作時就加上某種鎖,使得在同一時間其他用戶不能對該文件進行操作或只能進行有限的操作,來保證在這些情況下的文件數據的正確性。
主要使用flock函數,原型:bool flock(resource $handle , int $operation [, int &$wouldblock ]),第一個參數是指向文件的句柄變量,第二個是加鎖的方式,分別為
LOCK_SH:共享鎖(share),在讀取文件時加的鎖,加鎖後就其他用戶不能再對該文件進行寫,但可以讀取該文件內容;
LOCK_EX:排他鎖(exclude),或者叫獨占鎖,在寫文件時使用,加了該鎖後,只能是當前用戶進行寫操作,其他的用戶不能讀取和寫入;
LOCK_NB:附加鎖,在文件鎖定短時間大量用戶的訪問操作可能會造成flock在鎖定時堵塞,如果再加上該鎖後可避免該情況(是不是這麼一弄就能解決大量讀寫操作的問題,怕不行...);
LOCK_UN:釋放鎖,對前面的各種鎖進行一次性釋放,解鎖。
如果容易堵塞,還可使用第三個參數wouldblock,如果把它設置為1,在鎖定後就會阻擋其他進程來進行一些操作,但是windows上不支持,另外附加鎖LOCK_NB,windows也是不支持的。
另外,關閉句柄變量的fclose操作也可以釋放這些鎖。
廢話少說,看代碼
復制代碼
<?php
function readFileData($filename){
if(true == ($handle = fopen($filename, 'r'))){
if(flock($handle, LOCK_SH+LOCK_NB)){ // 加共享鎖和附加鎖,附加鎖防止阻塞
$str = '';
while(!feof($handle)){
$str .= fread($handle, 128);
}
flock($handle, LOCK_UN); // 釋放該鎖
fclose($handle);
return $str;
}
else{
echo 'add a share lock failed';
return '';
}
}
else{
return '';
}
}
復制代碼
注意使用多重鎖的方式,是LOCK_SH+LOCK_NB,,在排他鎖時也可這麼用,它們都是枚舉變量。對於寫操作類似
復制代碼
<?php
function writeInFile($filename, $data){
if(true == ($handle = fopen($filename, 'a'))){
if(flock($handle, LOCK_EX)){ //加上排他鎖
fwrite($handle, $data);
flock($handle, LOCK_UN); //釋放鎖
fclose($handle);
}
else{
echo 'add exclusive lock failed';
return;
}
}
}
復制代碼
實際上這也是解決php來實現多進程/線程讀取文件的方式,php沒有多線程這麼一說,但加鎖機制可以來模擬這種方式,實現對文件某些操作的隊列處理,面試時別說你不知道-_-
2.文件上傳
文件上傳就是將本地的文件上傳到服務器上,我們在用度場的雲、鵝場的雲上傳文件時均是如此,當然實際情況肯定比這簡單程序復雜。HTTP協議實現了文件的上傳機制,首先要在本地選擇上傳的文件,上傳到服務器後,服務端又要做一些處理,為此客戶端和服務端均要做一些設置。
1、客戶端
文件上傳最基本的方法是通過form表單進行POST傳遞文件,實際上通過PUT方法也可以上傳文件,只不過這種方法不安全,需要配置一些安全驗證機制,這裡只寫最常用的方式。form表單的input標簽可以設置成文件上傳按鈕type="file",直接解決了如何選擇文件的問題,接下來需要設置form的兩個屬性:enctype和method
enctype:設置成multipart/form-data
method:設置成post
關於enctype屬性設置可參考W3School的解釋
第一條是默認的值,在我們使用HTTP協議傳遞一般的表單數據時,實際上默認對數據進行了分塊編碼,比如默認的urlencode方式(空格轉為+,其他非字母字符轉為%加兩個十六進制大寫數);當enctype設置為第二個時,不會進行字符編碼,使用上傳控件(input標簽type設為file時即是)上傳文件,必須設定為這個值;第三個則是值對空格編碼為+,但不對非字母字符進行編碼。
我們知道GET方法一般用於獲取數據,且傳遞數據大小有限,而且POST方法可以傳遞比GET大得多的數據。
form的屬性設置完成後,還要傳遞一個值過去,使用隱藏域(<input type="hidden">),它的name屬性設為MAX_FILE_SIZE,之所以要設定這個值,是先大概定一個文件尺寸值,避免在用戶傳一個大文件傳了半天再告訴他:sorry,你的文件太大了-_-它的value屬性值就是文件的size,以字節為單位。當然某些書上說,這個值只是作為參考,可輕易進行欺騙,這裡只是象征性的表示,很可惜我這只菜鳥對安全了解甚少,只知道普通注入、XSS等,暫且用著吧。
那麼就可以寫一個簡單得不能再簡單的頁面了,作為客戶端用:
<form method="post" action="upload.php" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
選擇文件: <input type="file" name="uploadFile" value="upload"/><br/>
<input type="submit" name="submit" value="上傳" />
</form>
2、服務端
文件上傳到了服務器上還要經過一些處理過程,就像網購派送快遞,到了目的地也還得分個類,確認下目的地對錯吧。到了目的地的後續處理需要php腳本,上面在提交表單時的action屬性就指定了提交的處理腳本。我們知道在php中,$_POST保存的是post傳遞的數據,而上傳文件的相關信息保存在$_FILES裡邊,假設服務端腳本是這樣的:
<?php
echo '_FILES: <pre>';
print_r($_FILES);
echo '_POST: <pre>';
print_r($_POST);
不管服務端如何處理的,先看看這兩個數組裡面有什麼:
看FILES數組的選項就猜得到,這些就是上傳文件的名字、類型、尺寸、錯誤信息等等,還有這個FILES是二維數組。在弄清楚這些選項之前有必要了解幾個php配置選項,打開php.ini文件,找到下面四項(其實看注釋也明白了):
file_uploads:是否允許通過HTTP傳遞文件,默認是On允許;
upload_max_filesize:允許傳遞文件的最大大小,以M為單位,這是服務端配置文件設定的選項;
max_file_uploads:一次請求所允許傳遞的做多文件個數;
post_max_size:通過POST傳遞數據的最大大小,因為文件傳遞也是post方式,也算post傳遞,需要特別注意的是,它必須要大於upload_max_filesize選項,因為在一次post傳遞過程中不僅會上傳文件,還會傳遞其他數值,比如上面的POST數組中的數據,必須考慮到,比如upload_max_filesize設為150M,這個就可以設為200M;
upload_tmp_dir:上傳文件的臨時目錄,配置文件裡面默認為空,會使用操作系統默認的臨時目錄,因此上面的FILES數組中的tmp_name中的眼熟的路徑就可以解釋了,使用windows默認的存放臨時文件的目錄,而且服務器默認對文件名作了修改。
那麼FILES數組中的uploadFile哪裡來的,為什麼要用它做鍵名,這是因為在上傳控件的name屬性就是uploadFile,它標記的是這個控件的上傳文件信息,因此我們可以放多個上傳控件,設置不同的name,當然設置一樣的name也可以,完全可以把它們全放在一個數組裡邊,如<input type="file" name="upload[]">。
現在回過頭看FILE數組的鍵名代表的信息,type是MIME類型,以/分隔,前面是主要類型,後面是具體文件類型,error肯定表示錯誤,有這麼幾種情況,0:沒有錯誤,上傳成功; 1:文件超過了PHP配置指令中的upload_max_filesize規定的大小; 2:文件超過HTML表單中MAX_FILE_SIZE規定的大小,3:文件只有部分上傳; 4:沒有文件上傳。現在關於FILES數組的問題全部明白了。
問題是,是不是上傳成功就不做任何處理了,當然不是,總不能全堆在一個臨時目錄裡面,上傳多了必然就要將文件移到別的地方,而php提供了專門而安全的函數。is_uploaded_file函數,判斷是否通過HTTP POST上傳,可以確保惡意的用戶去欺騙腳本而管理這些文件,例如/etc/pass(又是linux...),至於具體怎樣,我還不清楚。move_uploaded_file函數,將上傳文件移動到新位置,同時還可判斷文件是否為合法上傳,即通過HTTP POST方式,他們運行成功均返回布爾類型true。
扯了半天,上傳文件大概要經過這樣幾個步驟:
1、客戶端寫好上傳控件腳本,並傳遞一個限制文件大小的隱藏值;
2、服務端首先判斷FILES數組error值,看是否出錯;
3、判斷是否為允許上傳的類型(可以不判斷);
4、判斷在服務端腳本裡邊是否超過指定的文件大小;
5、上傳到臨時位置,生成新文件名(防止把已有同名文件覆蓋掉),檢查並移動到新目錄下。
客戶端准備工作剛已做,看服務端處理代碼:
復制代碼
<?php
$typeWhiteList = array('txt', 'doc', 'php', 'zip', 'exe'); // 類型白名單,過濾不允許上傳的文件類型
$max_size = 1000000; // 大小限制 為1M
$upload_path = 'D:/WAMP/upload/'; // 指定移至的目錄
// 1、判斷是否成功上傳到服務器
$error = $_FILES['uploadFile']['error'];
if($error > 0){
switch($error){
case 1: exit('超過php配置的最大文件上傳限制');
case 2: exit('超過HTML表單的最大文件上傳限制');
case 3: exit('文件只有部分被上傳');
case 4: exit('沒有上傳任何文件');
default: exit('未知類型錯誤');
}
}
// 2、判斷是否為允許上傳的類型
$extension = pathinfo($_FILES['uploadFile']['name'], PATHINFO_EXTENSION); // 獲取擴展名
if(!in_array($extension, $typeWhiteList)){
if($extension == '')
exit('不允許上傳空類型文件');
else
exit('不允許上傳'.$extension.'類型文件');
}
// 3、判斷是否為允許大小
if($_FILES['uploadFile']['size'] > $max_size){
exit('超過了允許上傳到的'.$max_size.'字節');
}
// 4、已到指定位置
$filename = date('Ymd').rand(1000, 9999); // 生成一個新文件名,防止覆蓋
if(is_uploaded_file($_FILES['uploadFile']['tmp_name'])){ // 判斷是否通過HTTP POST上傳
if(!move_uploaded_file($_FILES['uploadFile']['tmp_name'], $upload_path.$filename.'.'.$extension)){
exit('無法移動到指定位置');
}
else{
echo '文件上傳成功<br/>';
echo '文件名: '.$upload_path.$filename.'.'.$extension.'<br>';
}
}
else{
exit('文件未通過合法途徑上傳');
}
復制代碼
本想迅速體驗一把,結果報了個Warning,說時間設置依賴系統...bug總是這麼不期而遇,設置好時間後,再試,perfect!
3.文件下載
文件下載就比較簡單了,簡單的文件下載只需要用一個HTML鏈接就夠了,使用<a>標簽,href屬性指定資源位置,一點就可。但這種方式只能處理浏覽器默認無法識別的MIME類型,比如rar、7z等壓縮的數據。
復制代碼
<html>
<head>
<title>donwload file</title>
<meta http-equiv="Content-Type" content="text/html"; charset="utf-8" />
</head>
<body>
<a href="resource/header.txt">header.txt</a><br/>
<a href="resource/php.zip">php.zip</a><br/>
<a href="resource/pic.ico">pic.ico</a>
</body>
</html>
復制代碼
對於這些浏覽器不認識的類型文件,點鏈接,它直接彈框讓你下載,有的浏覽器甚至直接就下了,那麼對於文本txt、jpg等浏覽器默認識別的類型的文件,一點擊則會直接展現在頁面上,比如上面header.txt、pic.ico。如何不展示在頁面上而去下載它們呢,使用header函數。
header函數會通過發送頭信息告知,請把該文件當成一個附件,這樣點擊的時候,就也會下載了。
復制代碼
<?php
$filename = 'header.txt';
header('Content-Type: text/plain'); // 類型為普通文本
header('Content-Disposition:attachment; filename="$filename"'); // Content-Disposition:attachment,告訴它這是附件
header('Content-Length:'.filesize($filename)); // 告知文件大小
readfile($filename); // 讀取文件直接輸出,便於下載
復制代碼