《現代 PHP》學習筆記(十八):串流

前言

本文為《現代 PHP》一書的學習筆記。

環境

  • Windows 10
  • Wnmp 3.1.0

串流

串流在 PHP 4.3.0 中被採用,串流用於概括檔案、網路、資料壓縮和其他的程序中共通的函式和行為。一個串流可以用串流線性的方式被讀取或寫入。

串流是從來源到目的地的資料,可以是一個檔案、命令列程序、網路連線、壓縮檔、暫存記憶體、標準輸出/輸入或任何其他在串流包裝中的資源。

串流提供許多 PHP 的 I/O 函式底層的實作,例如 file_get_contents()fopen()fgets()fwrite() 函式。

串流包裝

許多種不同的串流資料都需要用特殊的協定來讀寫資料,這些協定就是串流包裝。

可以在檔案系統中讀寫資料、用 HTTPHTTPSSSH 跟遠端網頁伺服器溝通,或讀寫 ZIP、RAR 或 PHAR 檔。這些所有的通訊方法都可以使用下列相同的程序:

  1. 開啟通訊。
  2. 讀取資料。
  3. 寫入資料。
  4. 關閉通訊。

每一個串流都有一個 schemetarget,藉由以下格式使用串流的辨識碼來指定:

1
<scheme>://<target>
  • <scheme> 用來辨識串流包裝,而 <target> 用來識別串流資料的來源。

http:// 串流包裝

file_get_contents() 函式的字串參數實際上是個串流辨識碼,URL 其實就是 PHP 串流包裝的一種形式,

範例 5-28:使用 HTTP 串流包裝的 Flickr API

1
2
3
4
$json = file_get_contents(
'http://api.flickr.com/services/feeds/photos_public.gne?format=json'
);
echo $json;

file:// 串流包裝

讀取檔案系統的 PHP 串流包裝就是 file://,由於 PHP 視其為預設設定,因此經常省略 file://

範例 5-29:不明顯的 file:// 串流包裝

1
2
3
4
5
$handle = fopen('/etc/hosts', 'rb');
while (feof($handle) !== true) {
echo fgets($handle);
}
fclose($handle);

範例 5-30:明顯的 file:// 串流包裝

1
2
3
4
5
$handle = fopen('file:///etc/hosts', 'rb');
while (feof($handle) !== true) {
echo fgets($handle);
}
fclose($handle);

php:// 串流包裝

php:// 串流包裝可以和 PHP 腳本的標準輸入/輸出、標準錯誤敘述進行溝通,可以使用 PHP 的檔案系統函式開啟和讀寫這四種串流:

  • php://stdin,顯示來自標準輸入的資料,例如來自命令列傳入的資訊。
  • php://stdout,將資料寫入當前的緩衝,這個串流只允許寫入。
  • php://memory,讀寫資料到系統記憶體。
  • php://temp,當可用記憶體耗盡時,轉而寫入暫存檔。

PHP 檔案系統函式只是為了檔案系統而存在,這是錯誤的觀念,PHP 檔案系統函式可以跟所有有支援的串流包裝一起使用,例如 Amazon、Dropbox 等服務。

自製串流包裝

PHP 提供 streamWrapper 類別說明如何撰寫自製的串流包裝,並支援部分或全部的檔案系統函式。

串流背景

有些 PHP 串流可以接受一些額外參數,用來客製化串流的行為。使用 stream_context_create() 函式來建立串流背景,它會回傳一個背景物件,可以被大部分的 PHP 檔案系統和串流函式使用。

範例 5-31:串流背景

1
2
3
4
5
6
7
8
9
10
$requestBody = '{"username":"josh"}';
$context = stream_context_create(array(
'http' => array(
'method' => 'POST',
'header' => "Content-Type: application/json;charset=utf-8;\r\n" .
"Content-Length: " . mb_strlen($requestBody),
'content' => $requestBody
)
));
$response = file_get_contents('https://my-api.com/users', false, $context);

串流過濾器

PHP 串流的厲害之處在於傳送時過濾、轉化、添加和移除串流資料的功能。

以下範例使用 tream_filter_append() 函式將過濾器加裝在串流上。

範例 5-32:串流過濾器 string.toupper 範例

1
2
3
4
5
6
$handle = fopen('data.txt', 'rb');
stream_filter_append($handle, 'string.toupper');
while(feof($handle) !== true) {
echo fgets($handle); // <--輸出所有大寫字元
}
fclose($handle);

以下範例使用 php://filter 串流包裝加裝過濾器到串流。

範例 5-33:串流過濾器 string.toupper 範例

1
2
3
4
$handle = fopen('php://filter/read=string.toupper/resource=data.txt', 'rb');
while(feof($handle) !== true) {
echo fgets($handle); // 輸出所有大寫字元
}
  • 有些 PHP 檔案系統函式只能使用 php://filter 串流包裝加裝過濾器。

假想每天都要開啟 rsync.net 中的日誌檔,每個日誌檔都用 bzip2 壓縮,檔名以 YYYY-MM-DD 作為格式,需要取得前 30 天的日誌紀錄。

範例 5-34:利用 DateTime 和串流過濾器疊代壓縮的日誌檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$dateStart = new \DateTime();
$dateInterval = \DateInterval::createFromDateString('-1 day');
$datePeriod = new \DatePeriod($dateStart, $dateInterval, 30);
foreach ($datePeriod as $date) {
$file = 'sftp://USER:PASS@rsync.net/' . $date->format('Y-m-d') . '.log.bz2';
if (file_exists($file)) {
$handle = fopen($file, 'rb');
stream_filter_append($handle, 'bzip2.decompress'); // 解壓縮
while (feof($handle) !== true) {
$line = fgets($handle);
if (strpos($line, 'www.example.com') !== false) {
fwrite(STDOUT, $line);
}
}
fclose($handle);
}
}

自製串流過濾器

自製的串流過濾器繼承了 php_user_filter 內建類別的 PHP 類別。

PHP 串流將資料分成連續的 bucket,每個 bucket 包含固定長度的串流資料(例如 4096 位元組)。每個串流過濾器可以同時接收並且處理數個 bucket,一段時間內被過濾器接收的 bucket 稱為 bucket brigade

假想需要建立一個可以過濾不雅字眼的過濾器。

範例 5-35:自製的 DirtyWordsFilter 串流過濾器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class DirtyWordsFilter extends php_user_filter
{
/**
* @param resource $in Incoming bucket brigade
* @param resource $out Outgoing bucket brigade
* @param int $consumed Number of bytes consumed
* @param bool $closing Last bucket brigade in stream?
*/
public function filter($in, $out, &$consumed, $closing)
{
$words = array('grime', 'dirt', 'grease');
$wordData = array();
foreach ($words as $word) {
$replacement = array_fill(0, mb_strlen($word), '*');
$wordData[$word] = implode('', $replacement);
}
$bad = array_keys($wordData);
$good = array_values($wordData);

// 從進來的 bucket brigade 中疊代每個 bucket
while ($bucket = stream_bucket_make_writeable($in)) {
// 過濾掉不雅字眼
$bucket->data = str_replace($bad, $good, $bucket->data);

// 增加資料的讀取總數
$consumed += $bucket->datalen;

// 將 bucket 發起送下游的 brigade
stream_bucket_append($out, $bucket);
}

return PSFS_PASS_ON;
}
}
  • 過濾器如果處理成功,回傳 PSFS_PASS_ON 常數。

上述例子 filter() 函式接受四個參數:

  • $in,一個 brigade 帶有多個從串流起點而來的 bucket
  • $out,一個 brigade 帶有多個通向串流目的地的 bucket
  • &$consumed,被自製的過濾器所篩選掉的位元組總數。
  • $closing,表示 filter() 方法是否接收了最後一個 bucket bragade

範例 5-36:註冊自製的 DirtyWordsFilter 串流過濾器

1
stream_filter_register('dirty_words_filter', 'DirtyWordsFilter');

範例 5-37:使用自製 DirtyWordsFilter 串流過濾器

1
2
3
4
5
6
$handle = fopen('data.txt', 'rb');
stream_filter_append($handle, 'dirty_words_filter'); // 加裝自製的過濾器
while (feof($handle) !== true) {
echo fgets($handle); // 輸出被掃瞄的文字
}
fclose($handle);

參考資料

  • Josh Lockhart(2015)。現代 PHP。台北市:碁峯資訊。