热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

PHP回顾之流

PHP回顾系列目录PHP基础web请求cookieweb响应session数据库操作加解密Composer创建自己的Composer包发送邮件IO上篇“PHP回顾之IO”提到读取文

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\n");
fwrite($fp, "IPX-021\n");
fwrite($fp, "IPZ-933\n");
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等。

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

感谢阅读,欢迎指正!


参考



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

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

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

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

感谢阅读,敬请指正!



推荐阅读
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文介绍了基于c语言的mcs51单片机定时器计数器的应用教程,包括定时器的设置和计数方法,以及中断函数的使用。同时介绍了定时器应用的举例,包括定时器中断函数的编写和频率值的计算方法。主函数中设置了T0模式和T1计数的初值,并开启了T0和T1的中断,最后启动了CPU中断。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文讨论了在Spring 3.1中,数据源未能自动连接到@Configuration类的错误原因,并提供了解决方法。作者发现了错误的原因,并在代码中手动定义了PersistenceAnnotationBeanPostProcessor。作者删除了该定义后,问题得到解决。此外,作者还指出了默认的PersistenceAnnotationBeanPostProcessor的注册方式,并提供了自定义该bean定义的方法。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • mac php错误日志配置方法及错误级别修改
    本文介绍了在mac环境下配置php错误日志的方法,包括修改php.ini文件和httpd.conf文件的操作步骤。同时还介绍了如何修改错误级别,以及相应的错误级别参考链接。 ... [详细]
author-avatar
tuir
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有