用 PHP 做檔案下載功能時遇到的記憶體問題

一般來說,為了避免使用者上傳了什麼奇奇怪怪的檔案被執行,會將上傳的檔案放在網頁根目錄以外的地方,再利用 PHP 將檔案內容輸出給 Client。

最簡單的方法是用 readfile 這個函式直接將檔案內容輸出,然而如果允許上傳的檔案比較大的話, readfile 會一次將檔案讀到記憶體內,造成記憶體用量超出上限的問題。

這個不能單純將記憶體上限提高來解決,不然只要同時幾個人下載檔案就會造成伺服器記憶體用盡。我想目前最簡單的方法是利用 stream 的功能來做:

這樣子就可以了,經測試下載數百 MB 的檔案也不會直接造成 PHP 錯誤。

PHP include 的運作方式

PHP 的 include 暗藏了一些潛規則,如果沒搞懂的話,遇到問題會以為見鬼了。所以這篇主要就在研究 PHP 的 include 運作方式。

首先,我們從官方文件可以看到 include 會區分為兩種模式,一種是 include 檔案,另一種是 include 路徑。只要是絕對路徑,或是相對路徑,就是以 include 路徑來運作。這裡比較容易搞混的是相對路徑的部份,因為 PHP 只把 “.” 或 “..” 開頭的作為相對路徑看待。

因此 include “foo/bar.php” 是屬於 include 檔案,而 include “./foo/bar.php” 則是 include 路徑。

在 include 檔案時,會先從 include_path 開始搜尋,再來是搜尋此 PHP 程式所在的資料夾(the calling script’s own directory),最後是搜尋 current working directory(CWD)。

而在 include 路徑時,則只會從 CWD 去搜尋。

為了驗證,我設置了這樣的環境來作測試:

其中 lib{xx,yy,zz}.php 的內容都是一樣的:

include_priority.php 是將要執行的主程式,分別將 include_path 以及 CWD 設置到 inc_path 與 cwd 資料夾,這樣就能分辨優先順序,其內容是:

最終結果是這樣子:

由結果來看,符合文件中所描述的運作方式。

接著來驗證第二種情況,如果 a.php include b.php 且 b.php 又 include c.php 的話,那麼在排除 include_path 與 CWD 的情況下,會如何運作?
環境設置:

root.php 是主程式,內容為:

sub.php 的內容:

libxx.php 的內容與前一個實驗相同。
最後的輸出結果是:

由此可見程式所在的資料夾(非CWD)是會隨著 include 所在的檔案而變動的。換句話說,就是以這一行 include 語法所在的檔案的位置為主。

綜合以上的內容,就可以解釋下面這個看到鬼的例子了:

在 root.php 裡面 include 了 sub.php 檔案:

然後在 sub.php 裡面又分別 include libxx.php 與 tools.php 兩個檔案:

因為 include “library/libxx.php” 成功了,於是以為相對路徑是由 sub.php 所在位置來計算,因此又下了 include “../tools.php” 這樣的指令,結果死的不明不白。

其實 libxx.php 是以檔案的方式去 include 的,所以會搜尋三個地方:include_path,sub.php 所在的位置,以及 CWD。而 sub.php 所在的位置剛好就可以找到 library/libxx.php 這檔案,所以 include 成功。

在 include tools.php 時則是因為它以 “..” 開頭,所以只會從 CWD 來計算相對路徑找檔案。而此時的 CWD 是在 root.php 那邊,自然就找不到東西了。

Windows Server 2012 安裝 IIS PHP MySQL

今天下載了 Windows Server 2012 試用版來玩玩。
第一個感想:不知道怎麼關機…
以下隨性的紀錄一下過程與問題。

安裝 IIS,PHP 與 MySQL
IIS 就從 Server Manager 去新增 Role 就可以了,PHP 跟 MySQL 則是去下載 Web Platform Installer (WebPI) 來安裝。

WebPI 這東西很像 Linux 上的套件管理系統,可以幫你安裝許多東西,還順便弄些初始設定。

MySQL安裝好後,預設是 listen 0.0.0.0 的。所以如果只是本機用,二話不說先去改成 127.0.0.1。
去 Program Files 裡找到 MySQL 的資料夾,修改 my.ini ,加入 bind-address=127.0.0.1 然後重啟服務就可以了。

PHP 的部份沒遇到什麼特殊的問題,基本上直接就可以使用。

接著將一個使用 CodeIgniter 的網站放上去測試看看,第一步先設定 url rewrite:

  • 打開IIS Manager
  • 選擇要編輯的站台
  • 點選 URL Rewrite
  • 點選 Add Rules
  • 點選 Blank rule
  • Name: 隨意取個名字
  • Pattern: 輸入 ^(.*)$
  • 點開 Conditions
  • 新增 rule,選擇 Is Not a File
  • 新增 rule,選擇 Is Not a Directory
  • 最後到底下的 Action,Rewrite URL 填入 index.php

這樣子就設定好了,正常運作。
參考: http://quitedestroyer.blogspot.tw/2012/05/codeigniter-url-rewriting-on-iis-7.html

若是需要上傳檔案,對資料夾要有寫入權限。
安全性比較要求的站可以針對 IIS 來設定權限。
懶一點的就將上傳資料夾直接開放所有權限給 Users 群組即可。

當 php 連往 localhost 的資料庫時,會有延遲。
在我的環境大約是延遲了一秒鐘,簡直慢到想直接放棄…
最後查到這個是普遍問題,在 Server 2012 以及 Windows 8 都會發生。
最佳解法就是在 php 連接資料庫時,不要用 “localhost”,而是改用 127.0.0.1 去連線。
參考: http://forums.iis.net/t/1200661.aspx