# 訪問流
在你打開一個流之后, 就可以在它上面執行I/O操作了. 使用哪種協議包裝API創建了流并不重要, 它們都使用相同的訪問API.
## 讀
流的讀寫可以使用下面的API函數組合完成, 它們多數都是遵循POSIX I/O中對應的API規范的:
int php_stream_getc(php_stream *stream);
從數據流中接收一個字符. 如果流上再沒有數據, 則返回EOF.
size_t php_stream_read(php_stream *stream, char *buf, size_t count);
從指定流中讀取指定字節的數據. buf必須預分配至少count字節的內存空間. 這個函數將返回從數據流實際讀到緩沖區中的數據字節數.
php_stream_read()不同于其他的流讀取函數. 如果使用的流不是普通文件流, 哪怕數據流中有超過請求字節數的數據, 并且當前也可以返回, 它也只會調用過一次底層流實現的read函數. 這是為了兼容基于包(比如UDP)的協議的這種做法.
```c
char *php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, size_t *returned_len);
char *php_stream_gets(php_stream *stream, char *buf, size_t maxlen);
```
這兩個函數從stream中讀取最多maxlen個字符, 直到碰到換行符或流結束. buf可以是一個指向預分配的至少maxlen字節的內存空間的指針, 也可以是NULL, 當它是NULL時,則會自動的創建一個動態大小的緩沖區, 用從流中實際讀出的數據填充, 成功后函數返回指向緩沖區的指針, 失敗則返回NULL. 如果returned_len傳遞了非NULL值, 則在返回時它將被設置為實際從流中讀取的字節數.
```c
char *php_stream_get_record(php_stream *stream, size_t maxlen, size_t *returned_len, char *delim, size_t delim_len TSRMLS_DC);
```
和php_stream_get_line()類似, 這個函數將讀取最多maxlen, 或到達EOF/行結束第一次出現的位置. 但是它也有和php_stream_get_line()的不同指出, 這個函數允許指定任意的停止讀取標記.
## 讀取目錄項
從php流中讀取目錄項和上面從普通文件中讀取普通數據相同. 這些數據放到了固定大小的dirents塊中. 內部的php_stream_dirent結構體如下, 它與POSIX定義的dirent結構體一致:
```c
typedef struct _php_stream_dirent {
char d_name[MAXPATHLEN];
} php_stream_dirent;
```
實際上你可以直接使用php_stream_read()函數讀取數據到這個結構體中:
```c
{
struct dirent entry;
if (php_stream_read(stream, (char*)&entry, sizeof(entry)) == sizeof(entry)) {
/* 成功從目錄流中讀取到一項 */
php_printf("File: %s\n", entry.d_name);
}
}
```
由于從目錄流中讀取是很常見的操作, php流包裝層暴露了一個API, 它將記錄大小的檢查和類型轉換處理封裝到了一次調用中:
```c
php_stream_dirent *php_stream_readdir(php_stream *dirstream, php_stream_dirent *entry);
```
如果成功讀取到目錄項, 則傳入的entry指針將被返回, 否則返回NULL標識錯誤. 使用這個為目錄流特殊構建的函數而不是直接從目錄流讀取非常重要, 這樣做未來流API改變時就不至于和你的代碼沖突.
## 寫
和讀類似, 向流中寫數據只需要傳遞一個緩沖區和緩沖區長度給流.
```c
size_t php_stream_write(php_stream *stream, char *buf, size_t count);
size_t php_stream_write_string(php_stream *stream, char *stf);
```
write_string的版本實際上是一個提供便利的宏, 它允許寫一個NULL終止的字符串,而不用顯式的提供長度. 返回的是實際寫到流中的字節數. 要特別小心的是嘗試寫大數據的時候可能導致流阻塞, 比如套接字流, 而如果流被標記為非阻塞, 則實際寫入的數據量可能會小于傳遞給函數的期望大小.
```c
int php_stream_putc(php_stream *stream, int c);
int php_stream_puts(php_string *stream, char *buf);
```
還有一種選擇是, 使用php_stream_putc()和php_stream_puts()寫入一個字符或一個字符串到流中. 要注意, php_stream_puts()不同于php_stream_write_string(), 雖然它們的原型看起來是一樣的, 但是php_stream_puts()會在寫出buf中的數據后自動的追加一個換行符.
```c
size_t php_stream_printf(php_stream *stream TSRMLS_DC, const char *format, ...);
```
功能和格式上都類似于fprintf(), 這個API調用允許在寫的同時構造字符串而不用去創建臨時緩沖區構造數據. 這里我們能夠看到的一個明顯的不同是它需要TSRMLS_CC宏來保證線程安全.
## 隨機訪問, 查看文件偏移量以及緩存的flush
基于文件的流, 以及另外幾種流是可以隨機訪問的. 也就是說, 在流的一個位置讀取了一些數據之后, 文件指針可以向前或向后移動, 以非線性順序讀取其他部分.
如果你的流應用代碼預測到底層的流支持隨機訪問, 在打開的時候就應該傳遞STREAM_MUST_SEEK選項. 對于那些原本就可隨機訪問的流來說, 這通常不會有什么影響, 因為流本身就是可隨機訪問的. 而對于那些原本不可隨機訪問的流, 比如網絡I/O或線性訪問文件比如FIFO管道, 這個暗示可以讓調用程序有機會在流的數據被消耗掉之前, 優雅的失敗.
在可隨機訪問的流資源上工作時, 下面的函數可用來將文件指針移動到任意位置:
```c
int php_stream_seek(php_stream *stream, off_t offset, int whence);
int php_stream_rewind(php_stream *stream);
```
offset是相對于whence表示的流位置的偏移字節數, whence的可選值及含義如下:
<table>
<tr>
<td>SEEK_SET</td>
<td>offset相對于文件開始位置. php_stream_rewind()API調用實際上是一個宏,展開后是php_stream_seek(stream, 0, SEEK_SET), 表示移動到文件開始位置偏移0字節處. 當使用SEEK_SET時, 如果offset傳遞負值被認為是錯誤的, 將會導致未定義行為. 指定的位置超過流的末尾也是未定義的, 不過結果通常是一個錯誤或文件被擴大以滿足指定的偏移量.</td>
</tr>
<tr>
<td>SEEK_CUR</td>
<td>offset相對于文件的當前偏移量. 調用php_stream_seek(steram, offset,SEEK_CUR)一般來說等價于php_stream_seek(stream, php_stream_tell() + offset, SEEK_SET);</td>
</tr>
<tr>
<td>SEEK_END</td>
<td>offset是相對于當前的EOF位置的. 負值的offset表示在EOF之前的位置, 正值和SEEK_SET中描述的是相同的語義, 可能在某些流實現上可以工作.</td>
</tr>
</table>
int php_stream_rewinddir(php_stream *dirstream);
在目錄流上隨機訪問時, 只有php_stream_rewinddir()函數可用. 使用php_stream_seek()函數將導致未定義行為. 所有的隨機訪問一族函數返回0標識成功或者-1標識失敗.
```c
off_t php_stream_tell(php_stream *stream);
```
如你之前所見, php_stream_tell()將返回當前的文件偏移量.
```c
int php_stream_flush(php_stream *stream);
```
調用flush()函數將強制將流過濾器此類內部緩沖區中的數據輸出到最終的資源中. 在流被關閉時, flush()函數將自動調用, 并且大多數無過濾流資源雖然不進行任何內部緩沖,但也需要flush. 顯式的調用這個函數很少見, 并且通常也是不需要的.
```c
int php_stream_stat(php_stream *stream, php_stream_statbuf *ssb);
```
調用php_stream_stat()可以獲取到流實例的其他信息, 它的行為類似于fstat()函數. 實際上, php_stream_statbuf結構體現在僅包含一一=個元素: struct statbuf sb; 因此,php_stream_stat()調用可以如下面例子一樣, 直接用傳統的fstat()操作替代, 它只是將posix的stat操作翻譯成流兼容的:
```c
int php_sample4_fd_is_fifo(int fd)
{
struct statbuf sb;
fstat(fd, &sb);
return S_ISFIFO(sb.st_mode);
}
int php_sample4_stream_is_fifo(php_stream *stream)
{
php_stream_statbuf ssb;
php_stream_stat(stream, &ssb);
return S_ISFIFO(ssb.sb.st_mode);
}
```
## 關閉
所有流的關閉都是通過php_stream_free()函數處理的, 它的原型如下:
```c
int php_stream_free(php_stream *stream, int options);
```
這個函數中的options參數允許的值是PHP_STREAM_FREE_xxx一族常量的按位或的結果, 這一族常量定義如下(下面省略PHP_STREAM_FREE_前綴):
<table>
<tr>
<td>CALL_DTOR</td>
<td>流實現的析構器應該被調用. 這里提供了一個時機對特定的流進行顯式釋放.</td>
</tr>
<tr>
<td>RELEASE_STREAM</td>
<td>釋放為php_stream結構體分配的內存</td>
</tr>
<tr>
<td>PRESERVE_HANDLE</td>
<td>指示流的析構器不要關閉它的底層描述符句柄</td>
</tr>
<tr>
<td>RSRC_DTOR</td>
<td>流包裝層內部管理資源列表的垃圾回收</td>
</tr>
<tr>
<td>PERSISTENT</td>
<td>作用在持久化流上時, 它的行為將是永久的而不局限于當前請求.</td>
</tr>
<tr>
<td>CLOSE</td>
<td>CALL_DTOR和RELEASE_STREAM的聯合. 這是關閉非持久化流的一般選項</td>
</tr>
<tr>
<td>CLOSE_CASTED</td>
<td>CLOSE和PRESERVE_HANDLE的聯合.</td>
</tr>
<tr>
<td>CLOSE_PERSISTENT</td>
<td>CLOSE和PERSISTENT的聯合. 這是永久關閉持久化流的一般選項.</td>
</tr>
</table>
實際上, 你并不需要直接調用php_stream_free()函數. 而是在關閉流時使用下面兩個宏的某個替代:
```c
#define php_stream_close(stream) \
php_stream_free((stream), PHP_STREAM_FREE_CLOSE)
#define php_stream_pclose(stream) \
php_stream_free((stream), PHP_STREAM_FREE_CLOSE_PERSISTENT)
```
## 通過zval交換流
因為流通常映射到zval上, 反之亦然, 因此提供了一組宏用來簡化操作, 并統一編碼(格式):
```c
#define php_stream_to_zval(stream, pzval) \
ZVAL_RESOURCE((pzval), (stream)->rsrc_id);
```
要注意, 這里并沒有調用ZEND_REGISTER_RESOURCE(). 這是因為當流打開的時候, 已經自動的注冊為資源了, 這樣就可以利用到引擎內建的垃圾回收和shutdown系統的優點. 使用這個宏而不是嘗試手動的將流注冊為新的資源ID是非常重要的; 這樣做的最終結果是導致流被關閉兩次以及引擎崩潰.
```c
#define php_stream_from_zval(stream, ppzval) \
ZEND_FETCH_RESOURCE2((stream), php_stream*, (ppzval), \
-1, "stream", php_file_le_stream(), php_file_le_pstream())
#define php_stream_from_zval_no_verify(stream, ppzval) \
(stream) = (php_stream*)zend_fetch_resource((ppzval) \
TSRMLS_CC, -1, "stream", NULL, 2, \
php_file_le_stream(), php_file_le_pstream())
```
從傳入的zval *中取回php_stream *有一個類似的宏. 可以看出, 這個宏只是對資源獲取函數(第9章"資源數據類型")的一個簡單封裝. 請回顧ZEND_FETCH_RESOURCE2()宏,第一個宏php_stream_from_zval()就是對它的包裝, 如果資源類型不匹配, 它將拋出一個警告并嘗試從函數實現中返回. 如果你只是想從傳入的zval *中獲取一個php_stream *, 而不希望有自動的錯誤處理, 就需要使用php_stream_from_zval_no_verify()并且需要手動的檢查結果值.
## links
* [目錄](<preface.md>)
* 14.1 [流的概覽](<14.1.md>)
* 14.3 [靜態資源操作](<14.3.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. 小結
- 約定