[翻譯][php擴展開發和嵌入式]第16章-有趣的流

全部翻譯內容pdf文檔下載地址: http://download.csdn.net/detail/lgg201/5107012

本書目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)兩位大牛組織翻譯. 該翻譯項目地址爲: https://github.com/walu/phpbook

本書在github上的地址: https://github.com/goosman-lei/php-eae

未來本書將可能部分合併到phpbook項目中, 同時保留一份獨立版本.


原書名: <Extending and Embedding PHP>

原作者: Sara Golemon

譯者: goosman.lei(雷果國)

譯者Email: [email protected]

譯者Blog: http://blog.csdn.net/lgg201

權利聲明

此譯本在不獲利的情況下, 可以無限制自由傳播.

有趣的流

php常被提起的一個特性是流上下文. 這個可選的參數甚至在用戶空間大多數流創建相關的函數中都可用, 它作爲一個泛化的框架用於向給定包裝器或流實現傳入/傳出額外的信息.

上下文

每個流的上下文包含兩種內部消息類型. 首先最常用的是上下文選項. 這些值被安排在上下文中一個二維數組中, 通常用於改變流包裝器的初始化行爲. 還有一種則是上下文參數, 它對於包裝器是未知的, 當前提供了一種方式用於在流包裝層內部的事件通知.

php_stream_context *php_stream_context_alloc(void);

通過這個API調用可以創建一個上下文, 它將分配一些存儲空間並初始化用於保存上下文選項和參數的HashTable. 還會自動的註冊爲一個請求終止後將被清理的資源.

設置選項

設置上下文選項的內部API和用戶空間的API是等同的:

int php_stream_context_set_option(php_stream_context *context,
            const char *wrappername, const char *optionname,
            zval *optionvalue);

下面是用戶空間的原型:

bool stream_context_set_option(resource $context,
            string $wrapper, string $optionname,
            mixed $value);

它們的不同僅僅是用戶空間和內部需要的數據類型不同.下面的例子就是使用這兩個API調用, 通過內建包裝器發起一個HTTP請求, 並通過一個上下文選項覆寫了user_agent設置.

php_stream  *php_varstream_get_homepage(const char *alt_user_agent TSRMLS_DC)
{
    php_stream_context  *context;
    zval    tmpval;

    context = php_stream_context_alloc(TSRMLS_C);
    ZVAL_STRING(&tmpval, alt_user_agent, 0); 
    php_stream_context_set_option(context, "http", "user_agent", &tmpval);
    return php_stream_open_wrapper_ex("http://www.php.net", "rb", REPORT_ERRORS | ENFORCE_SAFE_MODE, NULL, context);
}

譯者使用的php-5.4.10中php_stream_context_alloc()增加了線程安全控制, 因此相應的對例子進行了修改, 請讀者測試時注意.

這裏要注意的是tmpval並沒有分配任何持久性的存儲空間, 它的字符串值是通過複製設置的. php_stream_context_set_option()會自動的對傳入的zval內容進行一次拷貝.

取回選項

用於取回上下文選項的API調用正好是對應的設置API的鏡像:

int php_stream_context_get_option(php_stream_context *context,
            const char *wrappername, const char *optionname,
            zval ***optionvalue);

回顧前面, 上下文選項存儲在一個嵌套的HashTable中, 當從一個HashTable中取回值時, 一般的方法是傳遞一個指向zval **的指針給zend_hash_find(). 當然, 由於php_stream_context_get_option()是zend_hash_find()的一個特殊代理, 它們的語義是相同的.

下面是內建的http包裝器使用php_stream_context_get_option()設置user_agent的簡化版示例:

zval **ua_zval;
char *user_agent = "PHP/5.1.0";
if (context &&
    php_stream_context_get_option(context, "http",
                "user_agent", &ua_zval) == SUCCESS &&
                Z_TYPE_PP(ua_zval) == IS_STRING) {
    user_agent = Z_STRVAL_PP(ua_zval);
}

這種情況下, 非字符串值將會被丟棄, 因爲對用戶代理字符串而言, 數值是沒有意義的. 其他的上下文選項, 比如max_redirects, 則需要數字值, 由於在字符串的zval中存儲數字值並不通用, 所以需要執行一個類型轉換以使設置合法.

不幸的是這些變量是上下文擁有的, 因此它們不能直接轉換; 而需要首先進行隔離再進行轉換, 最終如果需要還要進行銷燬:

long max_redirects = 20;
zval **tmpzval;
if (context &&
    php_stream_context_get_option(context, "http",
            "max_redirects", &tmpzval) == SUCCESS) {
    if (Z_TYPE_PP(tmpzval) == IS_LONG) {
        max_redirects = Z_LVAL_PP(tmpzval);
    } else {
        zval copyval = **tmpzval;
        zval_copy_ctor(©val);
        convert_to_long(©val);
        max_redirects = Z_LVAL(copyval);
        zval_dtor(©val);
    }
}

實際上, 在這個例子中, zval_dtor()並不是必須的. IS_LONG的變量並不需要zval容器之外的存儲空間, 因此zval_dtor()實際上不會有真正的操作. 在這個例子中包含它是爲了完整性考慮, 對於字符串, 數組, 對象, 資源以及未來可能的其他類型, 就需要這個調用了.

參數

雖然用戶空間API中看起來參數和上下文選項是類似的, 但實際上在語言內部的php_stream_context結構體中它們被定義爲不同的成員.

目前只支持一個上下文參數: 通知器. php_stream_context結構體中的這個元素可以指向下面的php_stream_notifier結構體:

typedef struct {
    php_stream_notification_func func;
    void (*dtor)(php_stream_notifier *notifier);
    void *ptr;
    int mask;
    size_t progress, progress_max;
} php_stream_notifier;

當將一個php_stream_notifier結構體賦值給context->notifier時, 它將提供一個回調函數func, 在特定的流上發生下表中的PHP_STREAM_NOTIFY_*代碼表示的事件時被觸發. 每個事件將會對應下面第二張表中的PHP_STREAM_NOTIFY_SEVERITY_*的級別:


事件代碼

含義

RESOLVE

主機地址解析完成.多數基於套接字的包裝器將在連接之前執行這個查詢.

CONNECT

套接字流連接到遠程資源完成.

AUTH_REQUIRED

請求的資源不可用,原因是訪問控制以及缺失授權

MIME_TYPE_IS

遠程資源的mime-type不可用

FILE_SIZE_IS

遠程資源當前可用大小

REDIRECTED

原來的URL請求導致重定向到其他位置

PROGRESS

由於額外數據的傳輸導致php_stream_notifier結構體的progress以及(可能的)progress_max元素被更新(進度信息,請參考php手冊curl_setoptCURLOPT_PROGRESSFUNCTIONCURLOPT_NOPROGRESS選項)

COMPLETED

流上沒有更多的可用數據

FAILURE

請求的URL資源不成功或未完成

AUTH_RESULT

遠程系統已經處理了授權認證



安全碼


INFO

信息更新.等價於一個E_NOTICE錯誤

WARN

小的錯誤條件.等價於一個E_WARNING錯誤

ERR

中斷錯誤條件.等價於一個E_ERROR錯誤.


通知器實現提供了一個便利指針*ptr用於存放額外數據. 這個指針指向的空間必須在上下文析構時被釋放, 因此必須指定一個dtor函數, 在上下文的最後一個引用離開它的作用域時調用這個dtor進行釋放.

mask元素允許事件觸發限定特定的安全級別. 如果發生的事件沒有包含在mask中, 則通知器函數不會被觸發.

最後兩個元素progress和progress_max可以由流實現設置, 然而, 通知器函數應該避免使用這兩個值, 除非它接收到PHP_STREAM_NOTIFY_PROGRESS或PHP_STREAM_NOTIFY_FILE_SIZE_IS事件通知.

下面是一個php_stream_notification_func()回調原型的示例:

void php_sample6_notifier(php_stream_context *context,
        int notifycode, int severity, char *xmsg, int xcode,
        size_t bytes_sofar, size_t bytes_max,
        void *ptr TSRMLS_DC)
{
    if (notifycode != PHP_STREAM_NOTIFY_FAILURE) {
        /* 忽略所有通知 */
        return;
    }
    if (severity == PHP_STREAM_NOTIFY_SEVERITY_ERR) {
        /* 分發到錯誤處理函數 */
        php_sample6_theskyisfalling(context, xcode, xmsg);
        return;
    } else if (severity == PHP_STREAM_NOTIFY_SEVERITY_WARN) {
        /* 日誌記錄潛在問題 */
        php_sample6_logstrangeevent(context, xcode, xmsg);
        return;
    }
}

默認上下文

在php5.0中, 當用戶空間的流創建函數被調用時, 如果沒有傳遞上下文參數, 請求一般會使用默認的上下文. 這個上下文變量存儲在文件全局結構中: FG(default_context), 並且它可以和其他所有的php_stream_context變量一樣訪問. 當在用戶空間腳本執行流的創建時, 更好的方式是允許用戶指定一個上下文或者至少指定一個默認的上下文. 將用戶空間的zval *解碼得到php_stream_context可以使用php_steram_context_from_zval()宏完成, 比如下面改編自第14章"訪問流"的例子:

PHP_FUNCTION(sample6_fopen)
{
    php_stream *stream;
    char *path, *mode;
    int path_len, mode_len;
    int options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
    zend_bool use_include_path = 0;
    zval *zcontext = NULL;
    php_stream_context *context;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
            "ss|br", &path, &path_len, &mode, &mode_len,
                &use_include_path, &zcontext) == FAILURE) {
        return;
    }
    context = php_stream_context_from_zval(zcontext, 0);
    if (use_include_path) {
        options |= PHP_FILE_USE_INCLUDE_PATH;
    }
    stream = php_stream_open_wrapper_ex(path, mode, options,
                                    NULL, context);
    if (!stream) {
        RETURN_FALSE;
    }
    php_stream_to_zval(stream, return_value);
}

如果zcontext包含一個用戶空間的上下文資源, 通過ZEND_FETCH_RESOURCE()調用獲取到它關聯的指針設置到context中. 否則, 如果zcontext爲NULL並且php_stream_context_from_zval()的第二個參數設置爲非0值, 這個宏則直接返回NULL. 這個例子以及幾乎所有的核心流創建的用戶空間函數中, 第二個參數都被設置爲0, 此時將使用FG(default_context)的值.

過濾器

過濾器作爲讀寫操作的流內容傳輸過程中的附加階段. 要注意的是直到php 4.3中才加入了流過濾器, 在php 5.0對流過濾器的API設計做過較大的調整. 本章的內容遵循的是php 5的流過濾器規範.

在流上應用已有的過濾器

在一個打開的流上應用一個已有的過濾器只需要幾行代碼即可:

php_stream *php_sample6_fopen_read_ucase(const char *path
                                        TSRMLS_DC) {
    php_stream_filter *filter;
    php_stream *stream;

    stream = php_stream_open_wrapper_ex(path, "r",
                        REPORT_ERRORS | ENFORCE_SAFE_MODE,
                        NULL, FG(default_context));
    if (!stream) {
        return NULL;
    }

    filter = php_stream_filter_create("string.toupper", NULL,
                                        0 TSRMLS_CC);
    if (!filter) {
        php_stream_close(stream);
        return NULL;
    }
    php_stream_filter_append(&stream->readfilters, filter);

    return stream;
}

首先來看看這裏引入的API函數以及它的兄弟函數:

php_stream_filter *php_stream_filter_create(
                const char *filtername, zval *filterparams,
                int persistent TSRMLS_DC);
void php_stream_filter_prepend(php_stream_filter_chain *chain,
                php_stream_filter *filter);
void php_stream_filter_append(php_stream_filter_chain *chain,
                php_stream_filter *filter);

php_stream_filter_create()的filterparams參數和用戶空間對應的stream_filter_append()和stream_filter_prepend()函數的同名參數含義一致. 要注意, 所有傳遞到php_stream_filter_create()的zval *數據都不是過濾器所擁有的. 它們只是在過濾器創建期間被借用而已, 因此在調用作用域分配傳入的所有內存空間都要手動釋放.

如果過濾器要被應用到一個持久化流, 則必須設置persistent參數爲非0值. 如果你不確認你要應用過濾器的流是否持久化的, 則可以使用php_stream_is_persistent()宏進行檢查, 它只接受一個php_stream *類型的參數.

如在前面例子中看到的, 流過濾器被隔離到兩個獨立的鏈條中. 一個用於寫操作中對php_stream_write()調用響應時的stream->ops->write()調用之前. 另外一個用於讀操作中對stream->ops->read()取回的所有數據進行處理.

在這個例子中你使用&stream->readfilters指示讀的鏈條. 如果你想要在寫的鏈條上應用一個過濾器, 則可以使用&stream->writefilters.

定義一個過濾器實現

註冊過濾器實現和註冊包裝器遵循相同的基礎規則. 第一步是在MINIT階段向php中引入你的過濾器, 與之匹配的是在MSHUTDOWN階段移除它. 下面是需要調用的API原型以及兩個註冊過濾器工廠的示例:

int php_stream_filter_register_factory(
            const char *filterpattern,
            php_stream_filter_factory *factory TSRMLS_DC);
int php_stream_filter_unregister_factory(
            const char *filterpattern TSRMLS_DC);

PHP_MINIT_FUNCTION(sample6)
{
    php_stream_filter_register_factory("sample6",
            &php_sample6_sample6_factory TSRMLS_CC);
    php_stream_filter_register_factory("sample.*",
            &php_sample6_samples_factory TSRMLS_CC);
    return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(sample6)
{
    php_stream_filter_unregister_factory("sample6" TSRMLS_CC);
    php_stream_filter_unregister_factory("sample.*"
                                        TSRMLS_CC);
    return SUCCESS;
}

這裏註冊的第一個工廠定義了一個具體的過濾器名sample6; 第二個則利用了流包裝層內部的基本匹配規則. 爲了進行演示, 下面的用戶空間代碼, 每行都將嘗試通過不同的名字實例化php_sample6_samples_factory.

<?php
    stream_filter_append(STDERR, 'sample.one');
    stream_filter_append(STDERR, 'sample.3');
    stream_filter_append(STDERR, 'sample.filter.thingymabob');
    stream_filter_append(STDERR, 'sample.whatever');
?>

php_sample6_samples_factory的定義如下面代碼, 你可以將這些代碼放到你的MINIT塊上面:

#include "ext/standard/php_string.h"

typedef struct {
    char    is_persistent;
    char    *tr_from;
    char    *tr_to;
    int     tr_len;
} php_sample6_filter_data;

/* 過濾邏輯 */
static php_stream_filter_status_t php_sample6_filter(
        php_stream *stream, php_stream_filter *thisfilter, 
        php_stream_bucket_brigade *buckets_in, 
        php_stream_bucket_brigade *buckets_out, 
        size_t *bytes_consumed, int flags TSRMLS_DC) 
{
    php_stream_bucket       *bucket;
    php_sample6_filter_data *data       = thisfilter->abstract;
    size_t                  consumed    = 0;

    while ( buckets_in->head ) { 
        bucket      = php_stream_bucket_make_writeable(buckets_in->head TSRMLS_CC);
        php_strtr(bucket->buf, bucket->buflen, data->tr_from, data->tr_to, data->tr_len);
        consumed    += bucket->buflen;
        php_stream_bucket_append(buckets_out, bucket TSRMLS_CC);
    }   
    if ( bytes_consumed ) { 
        *bytes_consumed = consumed;
    }   
    return PSFS_PASS_ON;
}

/* 過濾器的釋放 */
static void php_sample6_filter_dtor(php_stream_filter *thisfilter TSRMLS_DC)
{
    php_sample6_filter_data *data   = thisfilter->abstract;
    pefree(data, data->is_persistent);
}

/* 流過濾器操作表 */
static php_stream_filter_ops php_sample6_filter_ops = { 
    php_sample6_filter, 
    php_sample6_filter_dtor, 
    "sample.*",
};

/* 字符翻譯使用的表 */
#define PHP_SAMPLE6_ALPHA_UCASE     "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define PHP_SAMPLE6_ALPHA_LCASE     "abcdefghijklmnopqrstuvwxyz"
#define PHP_SAMPLE6_ROT13_UCASE     "NOPQRSTUVWXYZABCDEFGHIJKLM"
#define PHP_SAMPLE6_ROT13_LCASE     "nopqrstuvwxyzabcdefghijklm"

/* 創建流過濾器實例的過程 */
static php_stream_filter *php_sample6_filter_create(
        const char *name, zval *param, int persistent TSRMLS_DC)
{
    php_sample6_filter_data *data;
    char                    *subname;

    /* 安全性檢查 */
    if ( strlen(name) < sizeof("sample.") || strncmp(name, "sample.", sizeof("sample.") - 1) ) { 
        return NULL;
    }   

    /* 分配流過濾器數據 */
    data    = pemalloc(sizeof(php_sample6_filter_data), persistent);

    if ( !data ) { 
        return NULL;
    }   

    /* 設置持久性 */
    data->is_persistent = persistent;

    /* 根據調用時的名字, 對過濾器數據進行適當初始化 */
    subname = (char *)name + sizeof("sample.") - 1;
    if ( strcmp(subname, "ucase") == 0 ) { 
        data->tr_from   = PHP_SAMPLE6_ALPHA_LCASE;
        data->tr_to     = PHP_SAMPLE6_ALPHA_UCASE;
    } else if ( strcmp(subname, "lcase") == 0 ) { 
        data->tr_from   = PHP_SAMPLE6_ALPHA_UCASE;
        data->tr_to     = PHP_SAMPLE6_ALPHA_LCASE;
    } else if ( strcmp(subname, "rot13") == 0 ) { 
        data->tr_from   = PHP_SAMPLE6_ALPHA_LCASE
                        PHP_SAMPLE6_ALPHA_UCASE;;
        data->tr_to     = PHP_SAMPLE6_ROT13_LCASE
                        PHP_SAMPLE6_ROT13_UCASE;
    } else {
        /* 不支持 */
        pefree(data, persistent);
        return NULL;
    }   

    /* 節省未來使用時每次的計算 */
    data->tr_len    = strlen(data->tr_from);

    /* 分配一個php_stream_filter結構並按指定參數初始化 */
    return php_stream_filter_alloc(&php_sample6_filter_ops, data, persistent);
}

/* 流過濾器工廠, 用於創建流過濾器實例(php_stream_filter_append/prepend的時候) */
static php_stream_filter_factory php_sample6_samples_factory = { 
    php_sample6_filter_create
};

譯註: 下面是譯者對整個流程的分析

1. MINIT階段的register操作將在stream_filters_hash這個HashTable中註冊一個php_stream_filter_factory結構, 它只有一個成員create_filter, 用來創建過濾器實例.

2. 用戶空間代碼stream_filter_append(STDERR, 'sapmple.one');在內部的實現是apply_filter_to_stream()函數(ext/standard/streamsfuncs.c中), 這裏有兩步操作, 首先創建過濾器, 然後將過濾器按照參數追加到流的readfilters/writefilters相應鏈中;

2.1 創建過濾器(php_stream_filter_create()): 首先直接按照傳入的名字精確的從stream_filters_hash(或FG(stream_filters))中查找, 如果沒有, 從右向左替換句點後面的內容爲星號"*"進行查找, 直到找到註冊的過濾器工廠或錯誤返回. 一旦找到註冊的過濾器工廠, 就調用它的create_filter成員, 創建流過濾器實例.

2.2 直接按照參數描述放入流的readfilters/writefilters相應位置.

3. 用戶向該流進行寫入或讀取操作時(以寫爲例): 此時內部將調用_php_stream_write(), 在這個函數中, 如果流的writefilters非空, 則調用流過濾器的fops->filter()執行過濾, 並根據返回狀態做相應處理.

4. 當流的生命週期結束, 流被釋放的時候, 將會檢查流的readfilters/writefilters是否爲空, 如果非空, 相應的調用php_stream_filter_remove()進行釋放, 其中就調用了fops->fdtor對流過濾器進行釋放.

上一章我們已經熟悉了流包裝器的實現, 你可能能夠識別這裏的基本結構. 工廠函數(php_sample6_samples_filter_create)被調用分配一個過濾器實例, 並賦值給一個操作集合和抽象數據. 這上面的例子中, 你的工廠爲所有的過濾器類型賦值了相同的ops結構, 但使用了不同的初始化數據.

調用作用域將得到這裏分配的過濾器, 並將它賦值給流的readfilters鏈或writefilters鏈. 接着, 當流的讀/寫操作被調用時, 過濾器鏈將數據放入到一個或多個php_stream_bucket結構體, 並將這些bucket組織到一個隊列php_stream_bucket_brigade中傳遞給過濾器.

這裏, 你的過濾器實現是前面的php_sample6_filter, 它取出輸入隊列bucket中的數據, 使用php_sample6_filter_create中確定的字符表執行字符串翻譯, 並將修改後的bucket放入到輸出隊列.

由於這個過濾器的實現並沒有其他內部緩衝, 因此幾乎不可能出錯, 因此它總是返回PSFS_PASS_ON, 告訴流包裝層有數據被過濾器存放到了輸出隊列中. 如果過濾器執行了內部緩衝消耗了所有的輸入數據而沒有產生輸出, 就需要返回PSFS_FEED_ME標識過濾器循環週期在沒有其他輸入數據時暫時停止. 如果過濾器碰到了關鍵性的錯誤, 它應該返回PSFS_ERR_FATAL, 它將指示流包裝層, 過濾器鏈處於不穩定狀態. 這將導致流被關閉.

用於維護bucket和bucket隊列的API函數如下:

php_stream_bucket *php_stream_bucket_new(php_stream *stream,
                      char *buf, size_t buflen, int own_buf,
                      int buf_persistent TSRMLS_DC);

創建一個php_stream_bucket用於存放到輸出隊列. 如果own_buf被設置爲非0值, 流包裝層可以並且通常都會修改它的內容或在某些點釋放分配的內存. buf_persistent的非0值標識buf使用的內存是否持久分配的:

int php_stream_bucket_split(php_stream_bucket *in,
        php_stream_bucket **left, php_stream_bucket **right,
        size_t length TSRMLS_DC);

這個函數將in這個bucket的內容分離到兩個獨立的bucket對象中. left這個bucket將包含in中的前length個字符, 而right則包含剩下的所有字符.

void php_stream_bucket_delref(php_stream_bucket *bucket
                                                 TSRMLS_DC);
void php_stream_bucket_addref(php_stream_bucket *bucket);

Bucket使用和zval以及資源相同的引用計數系統. 通常, 一個bucket僅屬於一個上下文, 也就是它依附的隊列.

void php_stream_bucket_prepend(
                    php_stream_bucket_brigade *brigade,
                    php_stream_bucket *bucket TSRMLS_DC);
void php_stream_bucket_append(
        php_stream_bucket_brigade *brigade,
        php_stream_bucket *bucket TSRMLS_DC);

這兩個函數扮演了過濾器子系統的苦力, 用於附加bucket到隊列的開始(prepend)或末尾(append)

void php_stream_bucket_unlink(php_stream_bucket *bucket
                                                 TSRMLS_DC);

在過濾器邏輯應用處理完成後, 舊的bucket必須使用這個函數從它的輸入隊列刪除(unlink).

php_stream_bucket *php_stream_bucket_make_writeable(
        php_stream_bucket *bucket TSRMLS_DC);

將一個bucket從它所依附的隊列中移除, 並且如果需要, 賦值bucket->buf的內部緩衝區, 這樣就使得它的內容可修改. 在某些情況下, 比如當輸入bucket的引用計數大於1時, 返回的bucket將會是不同的實例, 而不是傳入的實例. 因此, 我們要保證在調用作用域使用的是返回的bucket, 而不是傳入的bucket.

小結

過濾器和上下文可以讓普通的流類型行爲被修改, 或通過INI設置影響整個請求, 而不需要直接的代碼修改. 使用本章設計的計數, 你可以使你自己的包裝器實現更加強大, 並且可以對其他包裝器產生的數據進行改變.

接下來, 我們將離開PHPAPI背後的工作, 回到php構建系統的機制, 產生更加複雜的擴展鏈接到其他應用, 找到更加容易的方法, 使用工具集處理重複的工作.


目錄
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章