访问流
PHP用户空间中所有的文件I/O处理都是通过php 4.3引入的php流包装层处理的. 在内部, 扩展代码可以选择使用stdio或posix文件处理和本地文件系统或伯克利域套接字进行通信, 或者也可以调用和用户空间流I/O相同的API.
流的概览
通常, 直接的文件描述符相比调用流包装层消耗更少的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 = 0; 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可以使用到这个持久化资源.
访问流
在你打开一个流之后, 就可以在它上面执行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_DTOR和RELEASE_STREAM的联合. 这是关闭
非持久化流的一般选项.
CLOSE_CASTEDCLOSE和PRESERVE_HANDLE的联合.
CLOSE_PERSISTENTCLOSE和PERSISTENT的联合. 这是永久关闭持久化流的一
般选项.
实际上, 你并不需要直接调用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 *有一个类似的宏. 可以看出, 这个宏只是对资源获取函数(第9章"资源数据类型")的一个简单封装. 请回顾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参数, 它在其他的一些流函数中也有出现, 我们将在第16章去详细学习.
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用于指定一个八进制的值表明读写执行权限.
小结
本章中你接触了一些基于流的I/O的内部表象. 下一章将演示做呢样实现自己的协议包装, 甚至是定义自己的流类型.
以上就是[翻译][php扩展开发和嵌入式]第14章-php中流的访问的内容,更多相关内容请关注PHP中文网(www.php.cn)!