[ PHP 内核与扩展开发系列] 流式访问:流的概览

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

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

打开流

尽管是一个统一的 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() 函数开始。现在你应该已经对创建扩展骨架很熟悉了, 如果还不熟悉, 请回到你的第一个扩展复习一下, 下面是我们实现的 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 允许附加的控制,并可以得到包装器内的通知。你将在后续章节看到这个参数的细节。

传输层包装

尽管传输流和 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:80udp://10.0.0.1:53ssl://169.254.13.24:445 等。此外,UNIX域传输协议 unix:///path/to/socketudg:///path/to/dgramsocket 等都是合法的。xport_len 指定了 xport 的长度,因此 xport 是二进制安全的。
  • options:这个值是由前面 php_stream_open_wrapper() 中介绍的选项通过按位或组成的值。
  • flags:由 STREAM_XPORT_CLIENTSTREAM_XPORT_SERVER 之一与下面另外一张表中将列出的 STREAM_XPORT_* 常量通过按位或组合得到的值:
标记 含义
STREAM_XPORT_CLIENT 本地端将通过传输层和远程资源建立连接。这个标记通常和 STREAM_XPORT_CONNECTSTREAM_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://
  • persistent_id:如果请求的传输流需要在请求间持久化, 调用作用域可以提供一个 key 名字描述连接。指定这个值为 NULL 创建非持久化连接;指定为唯一的字符串值将尝试首先从持久化池中查找已有的传输流,或者没有找到时就创建一个新的持久化流。
  • timeout:在超时返回失败之前连接的尝试时间。如果这个值传递为 NULL 则使用 php.ini 中指定的默认超时值。这个参数对服务端传输流没有意义。
  • errstr:如果在选定的套接字上创建、连接、绑定或监听时发生错误,这里传递的 char * 引用值将被设置为一个描述发生错误原因的字符串。errstr 初始应该指向的是 NULL;如果在返回时它被设置了值,则调用作用域有责任去释放这个字符串相关的内存。
  • errcode:通过 errstr 返回的错误消息对应的数值错误代码。

目录访问

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

学院君 has written 703 articles

资深PHP工程师,Laravel学院院长

One thought on “[ PHP 内核与扩展开发系列] 流式访问:流的概览

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>