1. 云栖社区>
  2. PHP教程>
  3. 正文

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

作者:用户 来源:互联网 时间:2017-12-01 16:31:46

php开发扩展内核访问系列

[ PHP 内核与扩展开发系列] 流式访问:流的概览 - 摘要: 本文讲的是[ PHP 内核与扩展开发系列] 流式访问:流的概览, 通常直接文件描述符相比调用流包装层消耗更少的 CPU 和内存; 不过, 这样会将实现某个特定协议的所有工作都堆积到作为扩展开发者的身上,通过挂钩到流包装层,你的扩展代码可以透明的使用各种内建的流包装,比如HTTP、FTP,以及它们对应的


通常直接文件描述符相比调用流包装层消耗更少的 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: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_*
常量通过按位或组合得到的值:


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://


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


以上是云栖社区小编为您精心准备的的内容,在云栖社区的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索php , 开发 , 扩展 , 内核 , 访问 , 系列 , ,以便于您获取更多的相关知识。