资讯专栏INFORMATION COLUMN

PHP回顾之流

gself / 2359人阅读

摘要:本文先简要跟踪底层流的原理,再回到用户态中流的使用。底层流我们知道中的函数可以打开本地文件等并返回一个句柄,函数能对资源句柄进行读写,用于关闭资源。更多关于底层流的操作可参考官方文档中开发者的流章节,本文不再深入。

转载请注明文章出处: https://tlanyan.me/php-review...
PHP回顾系列目录

PHP基础

web请求

cookie

web响应

session

数据库操作

加解密

Composer

创建自己的Composer包

发送邮件

IO

上篇 “PHP回顾之IO” 提到读取文件、网络通信等操作,本质上是与 “流(stream)” 打交道。流机制是许多编程语言的重要机制,程序通过流可自由操作文件、内存、网络等设备的数据。

本文先简要跟踪PHP底层流的原理,再回到用户态中流的使用。

底层流

我们知道PHP中的fopen函数可以打开本地文件、URL等并返回一个句柄,freadfwrite函数能对资源句柄进行读写,fclose用于关闭资源。PHP如何做到使用一致API对不同数据源进行操作?答案是PHP引入了“流”的概念,在底层对操作进行抽象,带来的好处是上层可用同一套API。

为了理解PHP中的流,我们先追踪PHP中fopen函数调用过程。PHP的底层用C实现,阅读文中的代码需要一定的C语言基础。如果不熟悉C语言,关注其思路即可。

用户态的fopen函数定义在ext/standard/file.c文件中,函数体如下:

PHP_NAMED_FUNCTION(php_if_fopen)
{
    // 一些初始化代码
    context = php_stream_context_from_zval(zcontext, 0);
    stream = php_stream_open_wrapper_ex(filename, mode, (use_include_path ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);

    if (stream == NULL) {
        RETURN_FALSE;
    }
    php_stream_to_zval(stream, return_value);
}

PHP_NAMED_FUNCTION(php_if_fopen)定义PHP中的fopen函数(区别C中的fopen),有拓展开发基础的应当对这种写法熟悉。略过初始化等无关紧要的代码,fopen主要工作是获取流对象(stream)并转成PHP值类型(zval)返回。

流对象由php_stream_open_wrapper_ex函数返回,该函数位于main/php_streams.h中,是定义在main/streams/streams.c_php_stream_open_wrapper_ex的别名:

PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode, int options,
        zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
    // 初始化代码
    wrapper = php_stream_locate_url_wrapper(path, &path_to_open, options);
    if (options & STREAM_USE_URL && (!wrapper || !wrapper->is_url)) {
        php_error_docref(NULL, E_WARNING, "This function may only be used against URLs");
        if (resolved_path) {
            zend_string_release(resolved_path);
        }
        return NULL;
    }

    if (wrapper) {
        if (!wrapper->wops->stream_opener) {
            php_stream_wrapper_log_error(wrapper, options ^ REPORT_ERRORS,
                    "wrapper does not support stream open");
        } else {
            stream = wrapper->wops->stream_opener(wrapper,
                path_to_open, mode, options ^ REPORT_ERRORS,
                opened_path, context STREAMS_REL_CC);
        }

    }
    // stream检测等代码
}

_php_stream_open_wrapper_ex函数的工作主要有两点:1. 调用php_stream_locate_url_wrapper函数获取协议包装器(wrapper);2. 调用包装器打开资源并返回流对象。

接着看同一文件内获取包装器的函数php_stream_locate_url_wrapper

PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, const char **path_for_open, int options)
{
    // 一些初始化代码
    for (p = path; isalnum((int)*p) || *p == "+" || *p == "-" || *p == "."; p++) {
        n++;
    }

    if ((*p == ":") && (n > 1) && (!strncmp("//", p+1, 2) || (n == 4 && !memcmp("data:", path, 5)))) {
        protocol = path;
    }

    if (protocol) {
        if (NULL == (wrapper = zend_hash_str_find_ptr(wrapper_hash, protocol, n))) {
            char *tmp = estrndup(protocol, n);

            php_strtolower(tmp, n);
            if (NULL == (wrapper = zend_hash_str_find_ptr(wrapper_hash, tmp, n))) {
                char wrapper_name[32];

                if (n >= sizeof(wrapper_name)) {
                    n = sizeof(wrapper_name) - 1;
                }
                PHP_STRLCPY(wrapper_name, protocol, sizeof(wrapper_name), n);

                php_error_docref(NULL, E_WARNING, "Unable to find the wrapper "%s" - did you forget to enable it when you configured PHP?", wrapper_name);

                wrapper = NULL;
                protocol = NULL;
            }
            efree(tmp);
        }
    }
    /* TODO: curl based streams probably support file:// properly */
    if (!protocol || !strncasecmp(protocol, "file", n)) {
        /* fall back on regular file access */
        php_stream_wrapper *plain_files_wrapper = (php_stream_wrapper*)&php_plain_files_wrapper;

        // 检测代码

        return plain_files_wrapper;
    }
    // 检测远程文件等
    return wrapper;
}

php_stream_locate_url_wrapper中,我们终于知道fopen支持本地文件、HTTP/FTP、php://等多种数据源的奥秘:函数先查找路径是否以“http://”、"ftp://"类似协议开头,有则从注册的包装器列表中查找对应包装器;不以协议开头则回退到本地文件模式(php_plain_files_wrapper);fopen返回的流对象由包装器打开。

追踪以上代码,fopen的奥秘已经暴露无遗,但有两个关键点:1. 流对象(php_stream)是什么?2. 包装器(php_stream_wrapper)是什么?

内核开发者在源码的README.STREAMS文件中解释使用流的原因:让拓展开发者能像普通文件一样操作数据。为达到这个目的,流操作的资源都是php_stream对象。统一好资源接口后,PHP还定义了与文件操作对应的一套流函数:

流函数的第一个参数总是php_stream对象,例如与fread对应的php_stream_read函数定义为:PHPAPI size_t php_stream_read(php_stream * stream, char * buf, size_t count)

流操作的支持和具体操作由包装器决定(流包装器实际会调用php_stream中ops成员的具体函数,这些函数在包装器打开流时被正确的赋值)。同样是读取数据(fread),从文件中读和从内存中读做法不同。另外有些操作对某些流不适用。例如http协议支持fread,但不支持fwrite;普通文件可以其大小,但ssh2://协议的数据大小不可知(stat函数不可用)。内置的协议包装器列表和可用操作可参考官方文档中的“支持的协议和包装器”。

更多关于底层流的操作可参考官方文档中开发者的“流”章节,本文不再深入。

用户态流

让我们回到PHP应用层面,即用户态中的流。PHP的官方手册有专门讲解用户态流的章节,并提供一系列以stream开头的函数。由于fread/fputs等函数已经包含常见的流操作,stream开头的函数主要分为三类:辅助的过滤器filter和上下文context,包装器以及socket编程。网络编程将在后续的文章中讲解,我们先关注包装器。

开发者可以注册流包装器实现自定义协议,通过协议才能正常解析流的数据。比如我们为下面的小姐姐实现一个专属的协议secret://

class SecretStream {
    private $position;
    private $file;
    private $cipher = "aes-256-cbc";
    private $key = "little-sister";

    function stream_open($path, $mode, $options, &$opened_path)
    {
        $info = parse_url($path);
        $this->file = fopen($info["host"], $mode);
        $this->position = 0;
        return true;
    }

    function stream_read($count)
    {
        $line = fgets($this->file);
        $text = openssl_decrypt(base64_decode($line), $this->cipher, $this->key);
        $this->position += strlen($text);
        return $text;
    }

    function stream_write($data)
    {
        $raw = @openssl_encrypt($data, $this->cipher, $this->key);
        $base64 = base64_encode($raw);
        fwrite($this->file, $base64 . PHP_EOL);
        $this->position += strlen($data);
        return strlen($data);
    }

    function stream_tell()
    {
        return $this->position;
    }

    function stream_eof()
    {
        return feof($this->file);
    }

    function stream_close()
    {
        fclose($this->file);
    }
}

使用自定义协议先要注册,然后就可以正常使用了:

// 先注册自定义协议
stream_wrapper_register("secret", "SecretStream")
    or die("Failed to register protocol");

// 写数据
$fp = fopen("secret://Akari", "w+");
fwrite($fp, "IPZ-985
");
fwrite($fp, "IPX-021
");
fwrite($fp, "IPZ-933
");
fclose($fp);

// 由于协议未实现seek功能,不能通过rewind让文件指针到头部,需要重新打开
$fp = fopen("secret://Akari", "r");
while (!feof($fp)) {
    echo fgets($fp);
}
fclose($fp);

通过简单的代码,我们安全的存储了小姐姐的数据,也守护了小姐姐的秘密。其他人即使获取到文件内容,不通过我们的协议打开也很难知道具体内容。有没有感觉很不错?小姐姐和你比心哦~

总结

本文先回顾了PHP流底层的细节,再回到应用层中流的使用,并给出了一个简单的流包装器示例(例子简单,可用流章节中的php_user_filter来实现)。有兴趣的读者可以为下面的小姐姐创建自定义的协议,示例内容可以是:SSNI-056、SSNI-014、SNIS-662等。

本文感谢“微通广州”的赞助。

感谢阅读,欢迎指正!

参考

http://php.net/manual/en/book...

http://php.net/manual/en/inte...

https://blog.csdn.net/lgg201/...

https://post.zz173.com/course...

感谢阅读,敬请指正!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/28708.html

相关文章

  • PHP回顾之socket编程

    摘要:如果你想体验原味编程,用开头的比较适合否则建议使用流函数。有关流的知识,请参考本人之前的博文回顾之流。接下来我们用流函数实现一个简单的客户端和服务端。流函数中的和两个函数是我们想要的。本文目的是简要介绍中的编程,行文到此已经达到目的。 转载请注明文章出处: https://tlanyan.me/php-review... PHP回顾系列目录 PHP基础 web请求 cookie w...

    tomorrowwu 评论0 收藏0
  • 机器学习 刀光剑影 之屠龙刀

    摘要:在下以为,集成学习就是这把屠龙刀。集成学习在众多的机器学习数据挖掘竞赛中往往探囊取物,屡试不爽,像屠龙刀一样当之无愧排行兵器谱第一。这是机器学习研究中少有的理论指导的创新案列。历史的年轮不知不觉来到了年,统计学家已开始在机器学习界站稳脚跟。 机器学习是一个大武林,这里面江湖人士颇多,发明出来的算法兵器也是五花八门,浩瀚如海,足够你数上三天两夜了。然而,这些兵器行走江湖能用的不多,真正无敌的更...

    CloudwiseAPM 评论0 收藏0
  • Node中间层实践(一)——基于NodeJS的全栈式开发

    摘要:总结我觉得,以后基于的全栈式开发的模式将会越来越流行,这也会引领前端步入工程化时代。欢迎继续关注本博的更新中间层实践一基于的全栈式开发中间层实践二搭建项目框架中间层实践三配置中间层实践四模板引擎中间层实践五中间层的逻辑处理 版权声明:更多文章请访问我的个人站Keyon Y,转载请注明出处。 前言 近期公司有个新项目,由于后端人手不足,我果断的提议用node中间层的方案,得到了老大的支持...

    warkiz 评论0 收藏0
  • PHP回顾之执行流程及相关概念

    摘要:通过,脚本层无需过多考虑执行的具体环境,而本身则可以让针对自己的特点给出特有实现。模式下,也只执行一次。这几个概念的关系如下网关协议,与语言无关,所以与关系也不大。总结本文简要回顾了程序的架构和执行流程,并对几个容易混淆概念做了介绍。 转载请注明文章出处:https://tlanyan.me/php-review... PHP回顾系列目录 PHP基础 web请求 cookie we...

    jsdt 评论0 收藏0

发表评论

0条评论

gself

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<