小結文件的鎖定機制、上傳和下載php
1.文件鎖定html
如今都在講究什麼分佈式、併發等,實際上文件的操做也是併發的,在網絡環境下,多個用戶在同一時刻訪問頁面,對同一服務器上的同一文件進行着讀取,若是,這個用戶恰好讀到一半,另外一個用戶就寫入了消息,那麼前一個用戶讀到的就是錯誤數據,在數據庫裏面好像是稱爲髒數據,而若是某用戶寫到一半時,另外一用戶也對該文件進行寫操做,那麼就形成了寫入數據的混亂和錯誤,所以才php有一個鎖機制,相似於數據庫的鎖,當某用戶在對文件操做時就加上某種鎖,使得在同一時間其餘用戶不能對該文件進行操做或只能進行有限的操做,來保證在這些狀況下的文件數據的正確性。linux
主要使用flock函數,原型:bool flock(resource $handle , int $operation [, int &$wouldblock ]),第一個參數是指向文件的句柄變量,第二個是加鎖的方式,分別爲web
LOCK_SH:共享鎖(share),在讀取文件時加的鎖,加鎖後就其餘用戶不能再對該文件進行寫,但能夠讀取該文件內容;數據庫
LOCK_EX:排他鎖(exclude),或者叫獨佔鎖,在寫文件時使用,加了該鎖後,只能是當前用戶進行寫操做,其餘的用戶不能讀取和寫入;windows
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沒有多線程這麼一說,但加鎖機制能夠來模擬這種方式,實現對文件某些操做的隊列處理。固然技術老是進步的,如今php有了pthreads擴展後,直接調用一系列的API也能在程序中直接寫多線程了。
2.文件上傳
文件上傳就是將本地的文件上傳到服務器上,咱們在用度場的雲、鵝場的雲上傳文件時均是如此,固然實際狀況確定比這簡單程序複雜。HTTP協議實現了文件的上傳機制,首先要在本地選擇上傳的文件,上傳到服務器後,服務端又要作一些處理,爲此客戶端和服務端均要作一些設置。
一、客戶端
文件上傳最基本的方法是經過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>
二、服務端
文件上傳到了服務器上還要通過一些處理過程,就像網購派送快遞,到了目的地也還得分個類,確認下目的地對錯吧。到了目的地的後續處理須要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。
扯了半天,上傳文件大概要通過這樣幾個步驟:
一、客戶端寫好上傳控件腳本,並傳遞一個限制文件大小的隱藏值;
二、服務端首先判斷FILES數組error值,看是否出錯;
三、判斷是否爲容許上傳的類型(能夠不判斷);
四、判斷在服務端腳本里邊是否超過指定的文件大小;
五、上傳到臨時位置,生成新文件名(防止把已有同名文件覆蓋掉),檢查並移動到新目錄下。
客戶端準備工做剛已作,看服務端處理代碼:
<?php $typeWhiteList = array('txt', 'doc', 'php', 'zip', 'exe'); // 類型白名單,過濾不容許上傳的文件類型 $max_size = 1000000; // 大小限制 爲1M $upload_path = 'D:/WAMP/upload/'; // 指定移至的目錄 // 一、判斷是否成功上傳到服務器 $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('未知類型錯誤'); } } // 二、判斷是否爲容許上傳的類型 $extension = pathinfo($_FILES['uploadFile']['name'], PATHINFO_EXTENSION); // 獲取擴展名 if(!in_array($extension, $typeWhiteList)){ if($extension == '') exit('不容許上傳空類型文件'); else exit('不容許上傳'.$extension.'類型文件'); } // 三、判斷是否爲容許大小 if($_FILES['uploadFile']['size'] > $max_size){ exit('超過了容許上傳到的'.$max_size.'字節'); } // 四、已到指定位置 $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); // 讀取文件直接輸出,便於下載
若是待下載的文件與腳本在同一目錄下,貌似不要最後一行也可。但假設腳本在/data0/projects/code/web/php/下邊,待下載文件在/data0/projects/patches/apks/下邊,就應該制定清晰的路徑,好比
$filename = '/data0/projects/patches/apks/com.test.apk';
header('Content-Type: application/octet-stream');
header('Content-Disposition:attachment; filename='.basename($filename));
header('Content-Length:'.filesize($filename));
readfile($filename);
在頭信息Content-Disposition裏面使用basename取基礎文件名,不然下載的文件將是帶絕對路徑的名字,最後readfile輸出文件到緩存進行下載。
涉及到頭信息的東西,要補補HTTP協議的知識~