# 14.1 流的概覽
通常, 直接的文件描述符相比調用流包裝層消耗更少的CPU和內存; 然而, 這樣會將實現某個特定協議的所有工作都堆積到作為擴展開發者的你身上. 通過掛鉤到流包裝層, 你的擴展代碼可以透明的使用各種內建的流包裝, 比如HTTP, FTP, 以及它們對應的SSL版本, 另外還有gzip和bzip2壓縮包裝. 通過include特定的PEAR或PECL模塊, 你的代碼還可以訪問其他協議, 比如SSH2, WebDav, 甚至是Gopher!
本章將介紹內部基于流工作的基礎API. 后面到第16章"有趣的流"中, 我們將看到諸如應用過濾器, 使用上下文選項和參數等高級概念.
## 打開流
盡管是一個統一的API, 但實際上依賴于所需的流的類型, 有四種不同的路徑去打開一個流. 從用戶空間角度來看, 這四種不同的類別如下(函數列表只代表示例, 不是完整列表):
```php
<?php
/* fopen包裝
* 操作文件/URI方式指定遠程文件類資源 */
$fp = fopen($url, $mode);
$data = file_get_contents($url);
file_put_contents($url, $data);
$lines = file($url);
/* 傳輸
* 基于套接字的順序I/O */
$fp = fsockopen($host, $port);
$fp = stream_socket_client($uri);
$fp = stream_socket_server($uri, $options);
/* 目錄流 */
$dir = opendir($url);
$files = scandir($url);
$obj = dir($url);
/* "特殊"的流 */
$fp = tmpfile();
$fp = popen($cmd);
proc_open($cmd, $pipes);
```
無論你打開的是什么類型的流, 它們都存儲在一個公共的結構體php_stream中.
## fopen包裝
我們首先從實現fopen()函數開始. 現在你應該已經對創建擴展骨架很熟悉了, 如果還不熟悉, 請回到第5章"你的第一個擴展"復習一下, 下面是我們實現的fopen()函數:
```c
PHP_FUNCTION(sample5_fopen)
{
php_stream *stream;
char *path, *mode;
int path_len, mode_len;
int options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
&path, &path_len, &mode, &mode_len) == FAILURE) {
return;
}
stream = php_stream_open_wrapper(path, mode, options, NULL);
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
```
php_stream_open_wrapper()的目的應該是完全繞過底層. path指定要讀寫文件名或URL, 讀寫行為依賴于mode的值.
options是位域的標記值集合, 這里是設置為下面介紹的一組固定值:
<table>
<tr>
<td>USE_PATH</td>
<td>將php.ini文件中的include_path應用到相對路徑上. 內建函數fopen()在指定第三個參數為TRUE時將會設置這個選項.</td>
</tr>
<tr>
<td>STREAM_USE_URL</td>
<td>設置這個選項后, 將只能打開遠端URL. 對于php://, file://, zlib://, bzip2://這些URL包裝器并不認為它們是遠端URL.</td></tr>
<tr><td>ENFORCE_SAFE_MODE</td>
<td>盡管這個常量這樣命名, 但實際上設置這個選項后僅僅是啟用了安全模式(php.ini文件中的safe_mode指令)的強制檢查. 如果沒有設置這個選項將導致跳過safe_mode的檢查(不論INI設置中safe_mode如何設置)</td></tr>
<tr><td>REPORT_ERRORS</td>
<td>在指定的資源打開過程中碰到錯誤時, 如果設置了這個選項則將產生錯誤報告.</td></tr>
<tr><td>STREAM_MUST_SEEK</td>
<td>對于某些流, 比如套接字, 是不可以seek的(隨機訪問); 這類文件句柄, 只有在特定情況下才可以seek. 如果調用作用域指定這個選項, 并且包裝器檢測到它不能保證可以seek, 將會拒絕打開這個流.</td></tr>
<tr><td>STREAM_WILL_CAST</td>
<td>如果調用作用域要求流可以被轉換到stdio或posix文件描述符, 則應該給open_wrapper函數傳遞這個選項, 以保證在I/O操作發生之前就失敗</td></tr>
<tr><td>STREAM_ONLY_GET_HEADERS</td>
<td>標識只需要從流中請求元數據. 實際上這是用于http包裝器, 獲取http_response_headers全局變量而不真正的抓取遠程文件內容.</td></tr>
<tr><td>STREAM_DISABLE_OPEN_BASEDIR</td>
<td>類似safe_mode檢查, 不設置這個選項則會檢查INI設置open_basedir, 如果指定這個選項則可以繞過這個默認的檢查</td></tr>
<tr><td>STREAM_OPEN_PERSISTENT</td>
<td>告知流包裝層, 所有內部分配的空間都采用持久化分配, 并將關聯的資源注冊到持久化列表中.</td></tr>
<tr><td>IGNORE_PATH</td>
<td>如果不指定, 則搜索默認的包含路徑. 多數URL包裝器都忽略這個選項.</td></tr>
<tr><td>IGNORE_URL</td>
<td>提供這個選項時, 流包裝層只打開本地文件. 所有的is_url包裝器都將被忽略.</td></tr>
</table>
最后的NULL參數是char **類型, 它最初是用來設置匹配路徑, 如果path指向普通文件URL, 則去掉file://部分, 保留直接的文件路徑用于傳統的文件名操作. 這個參數僅僅是以前引擎內部處理使用的.
此外, 還有php_stream_open_wrapper()的一個擴展版本:
```c
php_stream *php_stream_open_wrapper_ex(char *path, char *mode, int options, char **opened_path, php_stream_context *context);
````
最后一個參數context允許附加的控制, 并可以得到包裝器內的通知. 你將在第16章看到這個參數的細節.
## 傳輸層包裝
盡管傳輸流和fopen包裝流是相同的組件組成的, 但它的注冊策略和其他的流不同. 從某種程度上來說, 這是因為用戶空間對它們的訪問方式的不同造成的, 它們需要實現基于套接字的其他因子.
從擴展開發者角度來看, 打開傳輸流的過程是相同的. 下面是對fsockopen()的實現:
```c
PHP_FUNCTION(sample5_fsockopen)
php_stream *stream;
char *host, *transport, *errstr = NULL;
int host_len, transport_len, implicit_tcp = 1, errcode = 0;
long port =
int options = ENFORCE_SAFE_MODE;
int flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l",
&host, &host_len, &port) == FAILURE) {
return;
}
if (port) {
int implicit_tcp = 1;
if (strstr(host, "://")) {
/* A protocol was specified,
* no need to fall back on tcp:// */
implicit_tcp = 0;
}
transport_len = spprintf(&transport, 0, "%s%s:%d",
implicit_tcp ? "tcp://" : "", host, port);
} else {
/* When port isn't specified
* we can safely assume that a protocol was
* (e.g. unix:// or udg://) */
transport = host;
transport_len = host_len;
}
stream = php_stream_xport_create(transport, transport_len,
options, flags,
NULL, NULL, NULL, &errstr, &errcode);
if (transport != host) {
efree(transport);
}
if (errstr) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "[%d] %s",
errcode, errstr);
efree(errstr);
}
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
```
這個函數的基礎構造和前面的fopen示例是一樣的. 不同在于host和端口號使用不同的參數指定, 接著為了給出一個傳輸流URL就必須將它們合并到一起. 在產生了一個有意義的路徑后, 將它傳遞給php_stream_xport_create()函數, 方式和fopen()使用的php_stream_open_wrapper()API一樣. php_stream_xport_create()的原型如下:
```c
php_stream *php_stream_xport_create(char *xport, int xport_len,
int options, int flags,
const char *persistent_id,
struct timeval *timeout,
php_stream_context *context,
char **errstr, int *errcode);
```
每個參數的含義如下:
<table>
<tr>
<td>xport</td>
<td>基于URI的傳輸描述符. 對于基于inet的套接字流, 它可以是tcp://127.0.0.1:80, udp://10.0.0.1:53, ssl://169.254.13.24:445等. 此外, UNIX域傳輸協議unix:///path/to/socket,udg:///path/to/dgramsocket等都是合法的. xport_len指定了xport的長度, 因此xport是二進制安全的.</td>
</tr>
<tr>
<td>options</td>
<td>這個值是由前面php_stream_open_wrapper()中介紹的選項通過按位或組成的值.</td>
</tr>
<tr>
<td>flags</td>
<td>由STREAM_XPORT_CLIENT或STREAM_XPORT_SERVER之一與下面另外一張表中將列出的STREAM_XPORT_*常量通過按位或組合得到的值.</td>
</tr>
<tr>
<td>persistent_id</td>
<td>如果請求的傳輸流需要在請求間持久化, 調用作用域可以提供一個key名字描述連接. 指定這個值為NULL創建非持久化連接; 指定為唯一的字符串值將嘗試首先從持久化池中查找已有的傳輸流, 或者沒有找到時就創建一個新的持久化流.</td>
</tr>
<tr>
<td>timeout</td>
<td>在超時返回失敗之前連接的嘗試時間. 如果這個值傳遞為NULL則使用php.ini中指定的默認超時值. 這個參數對服務端傳輸流沒有意義.</td>
</tr>
<tr>
<td>errstr</td>
<td>如果在選定的套接字上創建, 連接, 綁定或監聽時發生錯誤, 這里傳遞的char *引用值將被設置為一個描述發生錯誤原因的字符串. errstr初始應該指向的是NULL; 如果在返回時它被設置了值, 則調用作用域有責任去釋放這個字符串相關的內存.</td>
</tr>
<tr>
<td>errcode</td>
<td>通過errstr返回的錯誤消息對應的數值錯誤代碼.php_stream_xport_create()的flags參數中使用了STREAM_XPORT_*一族常量定義如下:
<table>
<tr>
<td>STREAM_XPORT_CLIENT</td>
<td>本地端將通過傳輸層和遠程資源建立連接. 這個標記通常和STREAM_XPORT_CONNECT或STREAM_XPORT_CONNECT_ASYNC聯合使用.</td>
</tr>
<tr>
<td>STREAM_XPORT_SERVER</td>
<td>本地端將通過傳輸層accept連接. 這個標記通常和STREAM_XPORT_BIND以及STREAM_XPORT_LISTEN一起使用.</td>
</tr>
<tr>
<td>STREAM_XPORT_CONNECT</td>
<td>用以說明建立遠程資源連接是傳輸流創建的一部分. 在創建客戶端傳輸流時省略這個標記是合法的, 但是這樣做就要求手動的調用php_stream_xport_connect().</td>
</tr>
<tr>
<td>STREAM_XPORT_CONNECT_ASYNC</td>
<td>嘗試連接到遠程資源, 但不阻塞。</td>
</tr>
<tr>
<td>STREAM_XPORT_BIND</td>
<td>將傳輸流綁定到本地資源. 用在服務端傳輸流時,這將使得accept連接的傳輸流準備端口, 路徑或特定的端點標識符等信息.</td>
</tr>
<tr>
<td>STREAM_XPORT_LISTEN</td>
<td>在已綁定的傳輸流端點上監聽到來的連接. 這通常用于基于流的傳輸協議, 比如: tcp://, ssl://,unix://.</td>
</tr>
</table>
</td>
</tr>
</table>
## 目錄訪問
fopen包裝器支持目錄訪問, 比如file://和ftp://, 還有第三種流打開函數也可以用于目錄訪問, 下面是對opendir()的實現:
```c
PHP_FUNCTION(sample5_opendir)
{
php_stream *stream;
char *path;
int path_len, options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",
&path, &path_len) == FAILURE) {
return;
}
stream = php_stream_opendir(path, options, NULL);
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
```
同樣的, 也可以為某個特定目錄打開一個流, 比如本地文件系統的目錄名或支持目錄訪問的URL格式資源. 這里我們又看到了options參數, 它和原來的含義一樣, 第三個參數NULL原型是php_stream_context類型.
在目錄流打開后, 和文件以及傳輸流一樣, 返回給用戶空間.
## 特殊流
還有一些特殊類型的流不能歸類到fopen/transport/directory中. 它們中每一個都有自己獨有的API:
```c
php_stream *php_stream_fopen_tmpfile(void);
php_stream *php_stream_fopen_temporary_file(const char *dir, const char *pfx, char **opened_path);
```
創建一個可seek的緩沖區流用于讀寫. 在關閉時, 這個流使用的所有臨時資源, 包括所有的緩沖區(無論是在內存還是磁盤), 都將被釋放. 使用這一組API中的后一個函數, 允許臨時文件被以特定的格式命名放到指定路徑. 這些內部API調用被用戶空間的tmpfile()函數隱藏.
```c
php_stream *php_stream_fopen_from_fd(int fd, const char *mode, const char *persistent_id);
php_stream *php_stream_fopen_from_file(FILE *file, const char *mode);
php_stream *php_stream_fopen_from_pipe(FILE *file, const char *mode);
```
這3個API方法接受已經打開的FILE *資源或文件描述符ID, 使用流API的某種操作包裝. fd格式的接口不會搜索匹配你前面看到過的fopen函數打開的資源, 但是它會注冊持久化的資源, 后續的fopen可以使用到這個持久化資源.
## links
* [目錄](<preface.md>)
* 14 [流式訪問](<14.md>)
* 14.2 [訪問流](<14.2.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定