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

黄舟
發布: 2023-03-05 16:36:01
原創
1396 人瀏覽過


有趣的流

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中取回值時, 一般的方法是傳遞一個指向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_*的级别:


不可用來的php_stream_notifierprog結構體的遠端系統已經處理了授權認證

事件代码

含义

RESOLVE

主机地址解析完成.多数基于套接字的包装器将在连接之前执行这个查询.

CONNECT

套接字流连接到远程资源完成.

AUTH_REQUIRED

請求的資源不可用,

-type

FILE_SIZE_IS

遠端資源目前可用大小

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

PROGRESS

由於額外數據的傳輸導致php_stream_notifier結構體的

)progress_max

元素被更新
(

進度資訊,請參考php手冊curl_setopt 選項)

COMPLETED

流上沒有更多的可用數據

FAILURE

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

AUTH_RESULT

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

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



.等價於一個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, &#39;sample.one&#39;);
    stream_filter_append(STDERR, &#39;sample.3&#39;);
    stream_filter_append(STDERR, &#39;sample.filter.thingymabob&#39;);
    stream_filter_append(STDERR, &#39;sample.whatever&#39;);
?>
登入後複製

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構建系統的機制, 產生更加複雜的擴展鏈接到其他應用, 找到更加容易的方法, 使用工具集處理重複的工作.

以上就是[翻譯][php擴展開發與嵌入式]第16章-有趣的流的內容,更多相關內容請關注PHP中文網(www.php.cn)!


相關標籤:
php
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!

安全碼


資訊更新
等價於一個

E_NOTICE

錯誤

WARN

小的錯誤條件

錯誤

ERR 中斷錯誤條件.

錯誤.

錯誤.錯誤.錯誤