14.1 流的概览

优质
小牛编辑
133浏览
2023-12-01

通常, 直接的文件描述符相比调用流包装层消耗更少的CPU和内存; 然而, 这样会将实现某个特定协议的所有工作都堆积到作为扩展开发者的你身上. 通过挂钩到流包装层, 你的扩展代码可以透明的使用各种内建的流包装, 比如HTTP, FTP, 以及它们对应的SSL版本, 另外还有gzip和bzip2压缩包装. 通过include特定的PEAR或PECL模块, 你的代码还可以访问其他协议, 比如SSH2, WebDav, 甚至是Gopher!

本章将介绍内部基于流工作的基础API. 后面到第16章"有趣的流"中, 我们将看到诸如应用过滤器, 使用上下文选项和参数等高级概念.

打开流

尽管是一个统一的API, 但实际上依赖于所需的流的类型, 有四种不同的路径去打开一个流. 从用户空间角度来看, 这四种不同的类别如下(函数列表只代表示例, 不是完整列表):

<?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()函数:

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是位域的标记值集合, 这里是设置为下面介绍的一组固定值:

USE_PATH将php.ini文件中的include_path应用到相对路径上. 内建函数fopen()在指定第三个参数为TRUE时将会设置这个选项.
STREAM_USE_URL设置这个选项后, 将只能打开远端URL. 对于php://, file://, zlib://, bzip2://这些URL包装器并不认为它们是远端URL.
ENFORCE_SAFE_MODE尽管这个常量这样命名, 但实际上设置这个选项后仅仅是启用了安全模式(php.ini文件中的safe_mode指令)的强制检查. 如果没有设置这个选项将导致跳过safe_mode的检查(不论INI设置中safe_mode如何设置)
REPORT_ERRORS在指定的资源打开过程中碰到错误时, 如果设置了这个选项则将产生错误报告.
STREAM_MUST_SEEK对于某些流, 比如套接字, 是不可以seek的(随机访问); 这类文件句柄, 只有在特定情况下才可以seek. 如果调用作用域指定这个选项, 并且包装器检测到它不能保证可以seek, 将会拒绝打开这个流.
STREAM_WILL_CAST如果调用作用域要求流可以被转换到stdio或posix文件描述符, 则应该给open_wrapper函数传递这个选项, 以保证在I/O操作发生之前就失败
STREAM_ONLY_GET_HEADERS标识只需要从流中请求元数据. 实际上这是用于http包装器, 获取http_response_headers全局变量而不真正的抓取远程文件内容.
STREAM_DISABLE_OPEN_BASEDIR类似safe_mode检查, 不设置这个选项则会检查INI设置open_basedir, 如果指定这个选项则可以绕过这个默认的检查
STREAM_OPEN_PERSISTENT告知流包装层, 所有内部分配的空间都采用持久化分配, 并将关联的资源注册到持久化列表中.
IGNORE_PATH如果不指定, 则搜索默认的包含路径. 多数URL包装器都忽略这个选项.
IGNORE_URL提供这个选项时, 流包装层只打开本地文件. 所有的is_url包装器都将被忽略.

最后的NULL参数是char **类型, 它最初是用来设置匹配路径, 如果path指向普通文件URL, 则去掉file://部分, 保留直接的文件路径用于传统的文件名操作. 这个参数仅仅是以前引擎内部处理使用的.

此外, 还有php_stream_open_wrapper()的一个扩展版本:

php_stream *php_stream_open_wrapper_ex(char *path, char *mode, int options, char **opened_path, php_stream_context *context);

最后一个参数context允许附加的控制, 并可以得到包装器内的通知. 你将在第16章看到这个参数的细节.

传输层包装

尽管传输流和fopen包装流是相同的组件组成的, 但它的注册策略和其他的流不同. 从某种程度上来说, 这是因为用户空间对它们的访问方式的不同造成的, 它们需要实现基于套接字的其他因子.

从扩展开发者角度来看, 打开传输流的过程是相同的. 下面是对fsockopen()的实现:

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()的原型如下:

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);

每个参数的含义如下:

xport基于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是二进制安全的.
options这个值是由前面php_stream_open_wrapper()中介绍的选项通过按位或组成的值.
flags由STREAM_XPORT_CLIENT或STREAM_XPORT_SERVER之一与下面另外一张表中将列出的STREAM_XPORT_*常量通过按位或组合得到的值.
persistent_id如果请求的传输流需要在请求间持久化, 调用作用域可以提供一个key名字描述连接. 指定这个值为NULL创建非持久化连接; 指定为唯一的字符串值将尝试首先从持久化池中查找已有的传输流, 或者没有找到时就创建一个新的持久化流.
timeout在超时返回失败之前连接的尝试时间. 如果这个值传递为NULL则使用php.ini中指定的默认超时值. 这个参数对服务端传输流没有意义.
errstr如果在选定的套接字上创建, 连接, 绑定或监听时发生错误, 这里传递的char *引用值将被设置为一个描述发生错误原因的字符串. errstr初始应该指向的是NULL; 如果在返回时它被设置了值, 则调用作用域有责任去释放这个字符串相关的内存.
errcode通过errstr返回的错误消息对应的数值错误代码.php_stream_xport_create()的flags参数中使用了STREAM_XPORT_*一族常量定义如下:
STREAM_XPORT_CLIENT本地端将通过传输层和远程资源建立连接. 这个标记通常和STREAM_XPORT_CONNECT或STREAM_XPORT_CONNECT_ASYNC联合使用.
STREAM_XPORT_SERVER本地端将通过传输层accept连接. 这个标记通常和STREAM_XPORT_BIND以及STREAM_XPORT_LISTEN一起使用.
STREAM_XPORT_CONNECT用以说明建立远程资源连接是传输流创建的一部分. 在创建客户端传输流时省略这个标记是合法的, 但是这样做就要求手动的调用php_stream_xport_connect().
STREAM_XPORT_CONNECT_ASYNC尝试连接到远程资源, 但不阻塞。
STREAM_XPORT_BIND将传输流绑定到本地资源. 用在服务端传输流时,这将使得accept连接的传输流准备端口, 路径或特定的端点标识符等信息.
STREAM_XPORT_LISTEN在已绑定的传输流端点上监听到来的连接. 这通常用于基于流的传输协议, 比如: tcp://, ssl://,unix://.

目录访问

fopen包装器支持目录访问, 比如file://和ftp://, 还有第三种流打开函数也可以用于目录访问, 下面是对opendir()的实现:

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:

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()函数隐藏.

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可以使用到这个持久化资源.