[ PHP 内核与扩展开发系列] 流式访问:访问流与静态资源操作


在你打开一个流之后,就可以在它上面执行 I/O 操作了,使用哪种协议包装 API 创建了流并不重要,它们都使用相同的访问 API。

流的读写可以使用下面的 API 函数组合完成,它们多数都是遵循 POSIX I/O 中对应的 API 规范的:
int php_stream_getc(php_stream *stream);
从数据流中接收一个字符,如果流上没有数据,则返回 EOF。
size_t php_stream_read(php_stream *stream, char *buf, size_t count);
从指定流中读取指定字节的数据,buf 必须预分配至少 count 字节的内存空间。这个函数将返回从数据流实际读到缓冲区中的数据字节数。 php_stream_read() 不同于其他的流读取函数,如果使用的流不是普通文件流,哪怕数据流中有超过请求字节数的数据,并且当前也可以返回,它也只会调用一次底层流实现的 read 函数。这种做法这是为了兼容基于包(比如 UDP)的协议。
char *php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, size_t *returned_len);
char *php_stream_gets(php_stream *stream, char *buf, size_t maxlen);
这两个函数从 stream 中读取最多 maxlen 个字符,直到碰到换行符或流结束。buf 可以是一个指向预分配的至少 maxlen 字节的内存空间的指针,也可以是 NULL,当它是NULL时,会自动的创建一个动态大小的缓冲区,用从流中实际读出的数据填充,成功后函数返回指向缓冲区的指针,失败则返回 NULL。如果returned_len 传递了非 NULL 值,则在返回时它将被设置为实际从流中读取的字节数。
char *php_stream_get_record(php_stream *stream, size_t maxlen, size_t *returned_len, char *delim, size_t delim_len TSRMLS_DC);
php_stream_get_line() 类似,这个函数将读取最多 maxlen 或到达 EOF / 行结束第一次出现的位置。但是它也有和 php_stream_get_line() 的不同之处,这个函数允许指定任意的停止读取标记。

读取目录项

从 PHP 流中读取目录项和上面从普通文件中读取数据相同,这些数据放到了固定大小的 dirents 块中。 内部的 php_stream_dirent 结构体如下,它与 POSIX 定义的 dirent 结构体一致:
typedef struct _php_stream_dirent {
    char d_name[MAXPATHLEN];
} php_stream_dirent;
实际上你可以直接使用 php_stream_read() 函数读取数据到这个结构体中:
{
    struct dirent entry;
    if (php_stream_read(stream, (char*)&entry, sizeof(entry)) == sizeof(entry)) {
        /* 成功从目录流中读取到一项 */
        php_printf("File: %s\n", entry.d_name);
    }
}
由于从目录流中读取是很常见的操作,PHP 流包装层暴露了一个 API,它将记录大小的检查和类型转换处理封装到了一次调用中:
php_stream_dirent *php_stream_readdir(php_stream *dirstream, php_stream_dirent *entry);
如果成功读取到目录项,则传入的 entry 指针将被返回,否则返回 NULL 标识错误。使用这个为目录流特殊构建的函数而不是直接从目录流读取非常重要,这样做未来流 API 改变时就不至于和你的代码冲突。

和读类似,向流中写数据只需要传递一个缓冲区和缓冲区长度给流:
size_t php_stream_write(php_stream *stream, char *buf, size_t count);
size_t php_stream_write_string(php_stream *stream, char *stf);
write_string 实际上是提供一个相对便利的宏,它允许写一个 NULL 终止的字符串,而不用显式的提供长度,返回的是实际写到流中的字节数。要特别小心的是尝试写大数据的时候可能导致流阻塞,比如套接字流,而如果流被标记为非阻塞,则实际写入的数据量可能会小于传递给函数的期望大小。
int php_stream_putc(php_stream *stream, int c);
int php_stream_puts(php_string *stream, char *buf);
还有一种选择是使用 php_stream_putc()php_stream_puts() 写入一个字符或一个字符串到流中。要注意,php_stream_puts() 不同于 php_stream_write_string(),虽然它们的原型看起来是一样的,但是 php_stream_puts() 会在写出 buf 中的数据后自动的追加一个换行符。
size_t php_stream_printf(php_stream *stream TSRMLS_DC, const char *format, ...);
功能和格式上都类似于 fprintf(),这个 API 调用允许在写的同时构造字符串而不用去创建临时缓冲区构造数据。这里我们能够看到的一个明显的不同是它需要 TSRMLS_CC 宏来保证线程安全。

随机访问、查看文件偏移量以及缓存的flush

基于文件的流,以及另外几种流是可以随机访问的。也就是说,在流的一个位置读取了一些数据之后,文件指针可以向前或向后移动,以非线性顺序读取其他部分。 如果你的流应用代码预测到底层的流支持随机访问,在打开的时候就应该传递 STREAM_MUST_SEEK 选项。 对于那些原本就可随机访问的流来说,这通常不会有什么影响,因为流本身就是可随机访问的。而对于那些原本不可随机访问的流,比如网络 I/O 或线性访问文件比如 FIFO 管道,这个暗示可以让调用程序有机会在流的数据被消耗掉之前,优雅地失败。 在可随机访问的流资源上工作时,下面的函数可用来将文件指针移动到任意位置:
int php_stream_seek(php_stream *stream, off_t offset, int whence);
int php_stream_rewind(php_stream *stream);
offset 是相对于 whence 表示的流位置的偏移字节数,whence 的可选值及含义如下:
  • SEEK_SEToffset 相对于文件开始位置。php_stream_rewind() API 调用实际上是一个宏,展开后是 php_stream_seek(stream,0,SEEK_SET),表示移动到文件开始位置偏移 0 字节处。当使用 SEEK_SET 时,如果 offset 传递负值被认为是错误的,将会导致未定义行为。指定的位置超过流的末尾也是未定义的,不过结果通常是一个错误或文件被扩大以满足指定的偏移量。
  • SEEK_CURoffset 相对于文件当前偏移量。调用 php_stream_seek(steram, offset, SEEK_CUR) 一般来说等价于 php_stream_seek(stream, php_stream_tell() + offset, SEEK_SET);
  • SEEK_ENDoffset 相对于当前 EOF 的位置。负值的 offset 表示在 EOF 之前的位置,正值和 SEEK_SET 中描述的是相同的语义,可能在某些流实现上可以工作。
int php_stream_rewinddir(php_stream *dirstream);
在目录流上随机访问时,只有 php_stream_rewinddir() 函数可用。使用 php_stream_seek() 函数将导致未定义行为。所有的随机访问族函数返回 0 标识成功或者 -1 标识失败。
off_t php_stream_tell(php_stream *stream);
如你之前所见,php_stream_tell() 将返回当前的文件偏移量。
int php_stream_flush(php_stream *stream);
调用 flush() 函数将强制将流过滤器此类内部缓冲区中的数据输出到最终的资源中。在流被关闭时,flush()函数将自动调用,并且大多数无过滤流资源虽然不进行任何内部缓冲,但也需要flush。 显式的调用这个函数很少见,并且通常也是不需要的。
int php_stream_stat(php_stream *stream, php_stream_statbuf *ssb);
调用 php_stream_stat() 可以获取到流实例的其他信息,它的行为类似于 fstat() 函数。实际上,php_stream_statbuf 结构体现在仅包含一个元素: struct statbuf sb;,因此,php_stream_stat() 调用可以如下面例子一样,直接用传统的 fstat() 操作替代,它只是将 POSIX 的 stat 操作翻译成流兼容的:
int php_sample4_fd_is_fifo(int fd)
{
    struct statbuf sb;
    fstat(fd, &sb);
    return S_ISFIFO(sb.st_mode);
}

int php_sample4_stream_is_fifo(php_stream *stream)
{
    php_stream_statbuf ssb;
    php_stream_stat(stream, &ssb);
    return S_ISFIFO(ssb.sb.st_mode);
}

关闭

所有流的关闭都是通过 php_stream_free() 函数处理的,它的原型如下:
int php_stream_free(php_stream *stream, int options);
这个函数中的 options 参数允许的值是 PHP_STREAM_FREE_xxx 一族常量的按位或的结果,这一族常量定义如下(下面省略 PHP_STREAM_FREE_ 前缀):
  • CALL_DTOR:流实现的析构器应该被调用,这里提供了一个时机对特定的流进行显式释放。
  • RELEASE_STREAM:释放为 php_stream 结构体分配的内存。
  • PRESERVE_HANDLE:指示流的析构器不要关闭它的底层描述符句柄。
  • RSRC_DTOR:流包装层内部管理资源列表的垃圾回收。
  • PERSISTENT :作用在持久化流上时,它的行为将是永久的而不局限于当前请求。
  • CLOSECALL_DTORRELEASE_STREAM 的联合,这是关闭非持久化流的一般选项。
  • CLOSE_CASTEDCLOSEPRESERVE_HANDLE 的联合。
  • CLOSE_PERSISTENTCLOSEPERSISTENT 的联合。这是永久关闭持久化流的一般选项。
实际上,你并不需要直接调用 php_stream_free() 函数,而是在关闭流时使用下面两个宏的某个替代:
#define php_stream_close(stream) php_stream_free((stream), PHP_STREAM_FREE_CLOSE)
#define php_stream_pclose(stream) php_stream_free((stream), PHP_STREAM_FREE_CLOSE_PERSISTENT)

通过zval交换流

因为流通常映射到 zval 上,反之亦然,因此提供了一组宏用来简化操作,并统一编码(格式):
#define php_stream_to_zval(stream, pzval) \
    ZVAL_RESOURCE((pzval), (stream)->rsrc_id);
要注意,这里并没有调用 ZEND_REGISTER_RESOURCE()。这是因为当流打开的时候,已经自动注册为资源了,这样就可以利用到引擎内建的垃圾回收和shutdown系统的特性。使用这个宏而不是尝试手动将流注册为新的资源ID是非常重要的 —— 这样做的最终结果是导致流被关闭两次以及引擎崩溃。
#define php_stream_from_zval(stream, ppzval) \
    ZEND_FETCH_RESOURCE2((stream), php_stream*, (ppzval),\
    -1, "stream", php_file_le_stream(), php_file_le_pstream())
#define php_stream_from_zval_no_verify(stream, ppzval) \
    (stream) = (php_stream*)zend_fetch_resource((ppzval) \
    TSRMLS_CC, -1, "stream", NULL, 2, \
    php_file_le_stream(), php_file_le_pstream())
从传入的 zval * 中取回 php_stream * 有一个类似的宏。可以看出,这个宏只是对资源获取函数的一个简单封装。请回顾 ZEND_FETCH_RESOURCE2() 宏,第一个宏 php_stream_from_zval() 就是对它的包装,如果资源类型不匹配,它将抛出一个警告并尝试从函数实现中返回。如果你只是想从传入的 zval * 中获取一个 php_stream *,而不希望有自动的错误处理,就需要使用 php_stream_from_zval_no_verify() 并且需要手动的检查结果值。

静态资源操作

一个基于流的原子操作并不需要实际的实例,下面这些 API 仅仅使用 URL 执行这样的操作:
int php_stream_stat_path(char *path, php_stream_statbuf *ssb);
和前面的 php_stream_stat() 类似,这个函数提供了一个对 POSIX 的 stat() 函数协议依赖的包装。要注意,并不是所有的协议都支持 URL 记法,并且即便支持也可能不能报告出 statbuf 结构体中的所有成员值。一定要检查 php_stream_stat_path() 失败时的返回值,0 标识成功,要知道,不支持的元素返回时其值将是默认的 0
int php_stream_stat_path_ex(char *path, int flags, 
    php_stream_statbuf *ssb, php_stream_context *context);
这个 php_stream_url_stat() 的扩展版本允许传递另外两个参数。第一个是 flags,它的值可以是下面的 PHP_STERAM_URL_STAT_* (下面省略 PHP_STREAM_URL_STAT_ 前缀)一族常量的按位或的结果。还有一个是 context 参数,它在其他的一些流函数中也有出现,我们将在后续章节去详细学习。
  • LINK:原始的 php_stream_stat_path() 对于符号链接或目录将会进行解析直到碰到协议定义的结束资源。传递 PHP_STREAM_URL_STAT_LINK 标记将导致 php_stream_stat_path() 返回请求资源的信息而不会进行符号链接的解析。(译注: 我们可以这样理解,没有这个标记,底层使用stat(),如果有这个标记,底层使用 lstat(),关于 stat()lstat() 的区别,请查看*nix手册)
  • QUIET:默认情况下,如果在执行 URL 的 stat 操作过程中碰到错误,包括文件未找到错误,都将通过 PHP 的错误处理机制触发。传递 QUIET 标记可以使得 php_stream_stat_path() 返回而不报告错误。
   int php_stream_mkdir(char *path,int mode,int options,
       php_stream_context *context);
   int php_stream_rmdir(char *path,int options,
       php_stream_context *context);
创建和删除目录也会如你期望的工作。 这里的 options 参数和前面的php_stream_open_wrapper() 函数的同名参数含义一致。对于 php_stream_mkdir(),还有一个参数 mode 用于指定一个八进制的值表明读写执行权限。

点赞 取消点赞 收藏 取消收藏

<< 上一篇: Laravel 中使用 Vue.js 实现基于 Ajax 的表单提交错误验证

>> 下一篇: 又一个基于 Laravel 5.2 开发的后台管理系统