# 9.2 PHP中的資源類型
# 9.2 PHP中的資源類型
通常情況下,像{資源}這類復合類型的數據都會占用大量的硬件資源,比如內存、CPU以及網絡帶寬。對于使用頻率超級高的數據庫鏈接,我們可以獲取一個長鏈接,使其不會在腳本結束后自動銷毀,一旦創建便可以在各個請求中直接使用,從而減少每次創建它的消耗。Mysql的長鏈接在PHP內核中其實就是一種持久{資源}。 Memory Allocation 前面的章節里我們接觸了emalloc()之類的以e開頭的內存管理函數,通過它們申請的內存都會被內核自動的進行垃圾回收的操作。而對于一個持久{資源}來說,我們是絕對不希望它在腳本結束后被回收的。
假設我們需要在我們的{資源}中同時保存文件名和文件句柄兩個數據,現在我們就需要自己定義個結構了:
```
typedef struct _php_sample_descriptor_data
{
char *filename;
FILE *fp;
}php_sample_descriptor_data;
```
當然,因為結構變了(之前是個FILE\*),我們之前的代碼也需要跟著改動。這里還沒有涉及到持久{資源},僅僅是換了一種{資源}結構
```
static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr;
fclose(fdata->fp);
efree(fdata->filename);
efree(fdata);
}
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode;
int filename_len, mode_len;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",&filename, &filename_len,&mode, &mode_len) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->fp = fp;
fdata->filename = estrndup(filename, filename_len);
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
PHP_FUNCTION(sample_fwrite)
{
php_sample_descriptor_data *fdata;
zval *file_resource;
char *data;
int data_len;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",&file_resource, &data, &data_len) == FAILURE )
{
RETURN_NULL();
}
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,&file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
RETURN_LONG(fwrite(data, 1, data_len, fdata->fp));
}
```
我們這里沒有重寫sample\_fclose()函數,你可以嘗試著自己實現它。
現在編譯運行,所有代碼的結果都非常正確,我們還可以在內核中獲取每個{資源}對應的文件名稱了。
```
PHP_FUNCTION(sample_fname)
{
php_sample_descriptor_data *fdata;
zval *file_resource;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",&file_resource) == FAILURE )
{
RETURN_NULL();
}
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,&file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
RETURN_STRING(fdata->filename, 1);
}
```
現在,Persistent Resources來了!
### Delayed Destruction
在前面我們刪除一個{資源}的時候,其實是去EG(regular\_list)中將其刪掉,EG(regular\_list)存儲著所有的只用在當前請求的{資源}。
持久{資源},存儲在另一個HashTable中:EG(persistent\_list)。其與EG(regular\_list)有個明顯的區別,那就是它每個值的索引都是字符串類型的,而且它的每個值也不會在每次請求結束后被釋放掉,只能我們手動通過zend\_hash\_del()來刪除,或者在進程結束后類似于MSHUTDOWN階段將EG(persistent\_list)整體清除,最常見的情景便是操作系統關閉了Web Server。 EG(persistent\_list)對其元素也有自己的dtor回調函數,和EG(regular\_list)一樣,它將根據其值的類型去調用不同的回調函數,我們這一次注冊回調函數的時候,需要用到zend\_register\_list\_destructors\_ex()函數的第二個參數,第一個則被賦成NULL。 在底層的實現中,持久的和regular{資源}是分別在不同的地方存儲的,也分別擁有各自不同的釋放函數。但在我們為腳本提供的函數中,卻希望能夠封裝這種差異,從而使我們的用戶使用起來更加方便快捷。
```
static int le_sample_descriptor_persist;
static void php_sample_descriptor_dtor_persistent(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr;
fclose(fdata->fp);
pefree(fdata->filename, 1);
pefree(fdata, 1);
}
PHP_MINIT_FUNCTION(sample)
{
le_sample_descriptor = zend_register_list_destructors_ex(php_sample_descriptor_dtor, NULL,PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
le_sample_descriptor_persist =zend_register_list_destructors_ex(NULL, php_sample_descriptor_dtor_persistent,PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
return SUCCESS;
}
```
我們并沒有為這兩種{資源}起不同的名字,以防使用戶產生疑惑。 現在我們的PHP擴展中引進了一種新的{資源},所以我們需要改寫一下上面的函數,**盡量使**用戶使用時感覺不到這種差異。
```
//sample_fopen()
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode;
int filename_len, mode_len;
zend_bool persist = 0;
//類比一下mysql_connect函數的最后一個參數。
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b",&filename, &filename_len, &mode, &mode_len,&persist) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
if (!persist)
{
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->filename = estrndup(filename, filename_len);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
else
{
list_entry le;
char *hash_key;
int hash_key_len;
fdata =pemalloc(sizeof(php_sample_descriptor_data),1);
fdata->filename = pemalloc(filename_len + 1, 1);
memcpy(fdata->filename, filename, filename_len + 1);
fdata->fp = fp;
//在EG(regular_list中存一份)
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor_persist);
//在EG(persistent_list)中再存一份
le.type = le_sample_descriptor_persist;
le.ptr = fdata;
hash_key_len = spprintf(&hash_key, 0,"sample_descriptor:%s:%s", filename, mode);
zend_hash_update(&EG(persistent_list),hash_key, hash_key_len + 1,(void*)&le, sizeof(list_entry), NULL);
efree(hash_key);
}
}
```
在持久{資源}時,因為我們在EG(regular\_list)中也保存了一份,所以腳本中我們資源類型的變量在實現中仍然是保存著一個resource ID,我們可以用它來進行之前章節所做的工作。 將其添加到EG(persistent\_list)中時,我們進行的操作流程幾乎和ZEND\_REGISTER\_RESOURCE()宏函數一樣,唯一的不同便是索引由之前的數字類型換成了字符串類型。 當一個保存在EG(regular\_list)中的持久{資源}被腳本釋放時,內核會在EG(regular\_list)尋找它對應的dtor函數,但它找到的是NULL,因為我們在使用zend\_register\_list\_destructors\_ex()函數聲明這種資源類型時,第一個參數的值為NULL。所以此時這個{資源}不會被任何dtor函數調用,可以繼續存在于內存中,任腳本流逝,請求更迭。 當web server的進程執行完畢后,內核會掃描EG(persistent\_list)的dtor,并調用我們已經定義好的釋放函數。在我們定義的釋放函數中,一定要記得使用pfree函數來釋放內存,而不是efree。
### Reuse
創建持久{資源}的目的是為了使用它,而不是讓它來浪費內存的,我們再次重寫一下sample\_open()函數,這一次我們將檢測需要創建的資源是否已經在persistent\_list中存在了。
```
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode, *hash_key;
int filename_len, mode_len, hash_key_len;
zend_bool persist = 0;
list_entry *existing_file;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b",&filename, &filename_len, &mode, &mode_len,&persist) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
//看看是否已經存在,如果已經存在就直接使用,不再創建
hash_key_len = spprintf(&hash_key, 0,"sample_descriptor:%s:%s", filename, mode);
if (zend_hash_find(&EG(persistent_list), hash_key,hash_key_len + 1, (void **)&existing_file) == SUCCESS)
{
//存在一個,直接使用!
ZEND_REGISTER_RESOURCE(return_value,existing_file->ptr, le_sample_descriptor_persist);
efree(hash_key);
return;
}
fp = fopen(filename, mode);
if (!fp)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode);
RETURN_FALSE;
}
if (!persist)
{
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->filename = estrndup(filename, filename_len);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
else
{
list_entry le;
fdata =pemalloc(sizeof(php_sample_descriptor_data),1);
fdata->filename = pemalloc(filename_len + 1, 1);
memcpy(fdata->filename, filename, filename_len + 1);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor_persist);
/* Store a copy in the persistent_list */
le.type = le_sample_descriptor_persist;
le.ptr = fdata;
//hash_key在上面已經被創建了
zend_hash_update(&EG(persistent_list),hash_key, hash_key_len + 1,(void*)&le, sizeof(list_entry), NULL);
}
efree(hash_key);
}
```
因為所有的PHP擴展都共用同一個HashTable來保存持久{資源},所以我們在為{資源}的索引起名時,一定要唯一,同時必須簡單,方便我們在其它的函數中構造出來。
### Liveness Checking and Early Departure
一旦我們打開一個本地文件,便可以一直占有它的操作句柄,保證隨時可以打開它。但是對于一些存在于遠程計算機上的資源,比如mysql鏈接、http鏈接,雖然我們仍然握著與服務器的鏈接,但是這個鏈接在服務器端可能已經被關閉了,在本地我們就無法再用它來做一些有價值的工作了。
所以,當我們使用{資源},尤其是持久{資源}時,一定要保證獲取出來的{資源}仍然是有效的、可以使用的。如果它失效了,我們必須將其從persistent list中移除。下面就是一個檢測socket有效性的例子:
```
if (zend_hash_find(&EG(persistent_list), hash_key,hash_key_len + 1, (void**)&socket) == SUCCESS)
{
if (php_sample_socket_is_alive(socket->ptr))
{
ZEND_REGISTER_RESOURCE(return_value,socket->ptr, le_sample_socket);
return;
}
zend_hash_del(&EG(persistent_list),hash_key, hash_key_len + 1);
}
```
如你所見,{資源}失效后,我們只要把它從HashTable中刪除就行了,這一步操作同樣會激活我們設置的回調函數。On completion of this code block, the function will be in the same state it would have been if no resource had been found in the persistent list.
### Agnostic Retrieval
現在我們已經可以創建資源類型并生成新的資源,還能將持久{資源}與平常{資源}使用的差異性封裝起來。但是如果用戶對一個持久{資源}調用sample\_fwrite()時候并不會正常工作,先想一下內核是如何通過一個數字所以在regular\_list中獲取最終資源的。
```
ZEND_FETCH_RESOURCE(
fdata,
php_sample_descriptor_data*,
&file_resource,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
le_sample_descriptor
);
```
le\_sample\_descriptor可以保證你獲取到的資源確實是這種類型的,絕不會出現你想要一個文件句柄,卻返回給你一個mysql鏈接的情況。這種驗證是必須的,但有時你又想繞過這種驗證,因為我們放在persistenst\_list中的{資源}是le\_sample\_descruotor\_persist類型的,所以當我們把它復制到regular\_list中時,它也是le\_sample\_descructor\_persist的,所以如果我們想獲取它,貌似只有兩種方法,要么修改類型,要么再寫一個新的sample\_write\_persistent函數的實現。或者極端一些,在sample\_write函數里進行復雜的判斷。但是如果sample\_write()函數能同時接收它們兩種類型的{資源}多好啊....
事情沒有這么復雜,我們確實可以在sample\_write()函數里獲取{資源}時候同時指定兩種類型。那就是使用ZEND\_FETCH\_RESOURCE2()宏函數,它與ZEND\_FETCH\_RESOURCE()宏函數的唯一區別就是它可以接收兩種類型參數。
```
ZEND_FETCH_RESOURCE2(
fdata,
php_sample_descriptor_data*,
&file_resource,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
le_sample_descriptor,
le_sample_descriptor_persist
);
```
現在,只要resource ID對應的最終資源類型是persistent或者non-persistent的一種便可以正常通過驗證了。
什么,你想設置三種甚至更多的類型?!!那你只能直接使用zend\_fetch\_resource()函數了。
```
//一種類型的
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
1,
le_sample_descriptor
);
ZEND_VERIFY_RESOURCE(fp);
```
想看看ZEND\_FETCH\_RESOURCE2()宏函數的實現么?
```
//兩種類型的
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
2,
le_sample_descriptor,
le_sample_descriptor_persist
);
ZEND_VERIFY_RESOURCE(fp);
```
再給力一些,三種類型的:
```
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
3,
le_sample_descriptor,
le_sample_descriptor_persist,
le_sample_othertype
);
ZEND_VERIFY_RESOURCE(fp);
```
話都說到這份上了,你肯定知道四種、五種、更多種類型的應該怎么調用了。
## links
- 9.1 [復合類型的數據——{資源}](9.1.html)
- 9.3 [{資源}自有的引用計數](9.3.html)
- 介紹
- 1 PHP的生命周期
- 1.1 讓我們從SAPI開始
- 1.2 PHP的啟動與終止
- 1.3 PHP的生命周期
- 1.4 線程安全
- 1.5 PHP的生命周期
- 2 PHP變量在內核中的實現
- 2.1 變量的類型
- 2.2 變量的值
- 2.3 創建PHP變量
- 2.4 變量的存儲方式
- 2.5 變量的檢索
- 2.6 類型轉換
- 2.7 小結
- 3 內存管理
- 3.1 內存管理
- 3.2 引用計數
- 3.3 內存管理
- 4 動手編譯PHP
- 4.1 動手編譯PHP
- 4.2 動手編譯PHP
- 4.3 Unix/Linux平臺下的編譯
- 4.4 在Win32平臺上編譯PHP
- 4.5 動手編譯PHP
- 5 Your First Extension
- 5.1 Your First Extension
- 5.2 編譯我們的擴展
- 5.3 靜態編譯
- 5.4 編寫函數
- 5.5 Your First Extension
- 6 函數返回值
- 6.1 函數返回值
- 6.2 引用與函數的執行結果
- 6.3 函數返回值
- 7 函數的參數
- 7.1 函數的參數
- 7.2 函數的參數
- 7.3 函數的參數
- 8 使用HashTable與{數組}
- 8.1 使用HashTable與{數組}
- 8.2 使用HashTable與{數組}
- 8.3 使用HashTable與{數組}
- 8.4 使用HashTable與{數組}
- 9 PHP中的資源類型
- 9.1 PHP中的資源類型
- 9.2 PHP中的資源類型
- 9.3 PHP中的資源類型
- 9.4 PHP中的資源類型
- 10 PHP中的面向對象(一)
- 10.1 PHP中的面向對象(一)
- 10.2 PHP中的面向對象(一)
- 10.3 PHP中的面向對象(一)
- 10.4 PHP中的面向對象(一)
- 10.5 PHP中的面向對象(一)
- 11 PHP中的面向對象(二)
- 11.1 PHP中的面向對象(二)
- 11.2 PHP中的面向對象(二)
- 11.3 PHP中的面向對象(二)
- 12 啟動與終止的那點事
- 12.1 關于生命周期
- 12.2 MINFO與phpinfo
- 12.3 常量
- 12.4 PHP擴展中的全局變量
- 12.5 PHP語言中的超級全局變量(Superglobals)
- 12.6 小結
- 13 INI設置
- 13.1 聲明和訪問INI設置
- 13.2 小結
- 14 流式訪問
- 14.1 流的概覽
- 14.2 訪問流
- 14.3 靜態資源操作
- 14.4 links
- 15 流的實現
- 15.1 php流的表象之下
- 15.2 包裝器操作
- 15.3 實現一個包裝器
- 15.4 操縱
- 15.5 檢查
- 15.6 小結
- 16 有趣的流
- 16.1 上下文
- 16.2 過濾器
- 16.3 小結
- 17 配置和鏈接
- 17.1 autoconf
- 17.2 庫的查找
- 17.3 強制模塊依賴
- 17.4 Windows方言
- 17.5 小結
- 18 擴展生成
- 18.1 ext_skel
- 18.2 PECL_Gen
- 18.3 小結
- 19 設置宿主環境
- 19.1 嵌入式SAPI
- 19.2 構建并編譯一個宿主應用
- 19.3 通過嵌入包裝重新創建cli
- 19.4 老技術新用
- 19.5 小結
- 20 高級嵌入式
- 20.1 回調到php中
- 20.2 錯誤處理
- 20.3 初始化php
- 20.4 覆寫INI_SYSTEM和INI_PERDIR選項
- 20.5 捕獲輸出
- 20.6 同時擴展和嵌入
- 20.7 小結