2

PHP-流(Stream)

 2 years ago
source link: https://shadowdragons.github.io/2019/06/10/php-stream/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
PHP-流(Stream) - ben blog

在现代的PHP特性中,流或许是最出色但最少使用的。虽然PHP 4.3.0就 引人了流,但是 很多开发者不知道流的存在, 因为人们很少提及流,而且流的文档也匮乏。

流在PHP 4.3.0 中引入,作用是使用统一的方式处理文件、网络和数据压缩等共用同一套函数和用法的操作。 简单而言,流是具有流式行为的资源对象。因此,流可以线性读写,或许还能使用fseek()函数定位到流中的任何位置。

这段定义很难理解是吧,下面我们简化一下,以便更易于理解。流的作用是在出发地和目的地之闾传输数据。就这么简单。 出发地和目的地可以是文件、命令行进程、网络连接、 ZIPTAR压缩文件、临时内存、标准输入或输出,或者是通过PHP流封装协议实现的任何其他资源。

如果你读写过文件,就使用过流;如果你从php://stdin读取过数据,或者把数据写人过php://stdout, 就使用过流。流为PHP的很多IO函数提供了底层实现, 例如fi1e_get_contents()fopen()fgets()fwrite()PHP的流函数提供了处理不同流资源 (出发地和目的地) 的统一接口。

注意: 我把流理解为管道,相当于把水从一个地方引到另一个地方。在水从出发地流到目的地的过程中,我们可以过滤水,可以改变水质,可以添加水,也可以排出水(提示:水是数据的隐喻)。

流封装协议

流式数据的种类各异,每种类型需要独特的协议,以便读写数据。我们称这些协议为流封装协议。例如,我们可以读写文件系统,可以通过HTTP、 HTTPS或SSH(安全的shell)与远程Web服务器通信,还可以打开并读写ZIPRARPHAR压缩文件。这些通信方式都包含下述相同的过程:

  1. 开始通信。
  2. 读取数据。
  3. 写入数据。
  4. 结束通信。

虽然过程是一祥的,但是读写文件系统中文件的方式与收发HTTP消息的方式有所不同。流封装协议的作用是使用通用的接口封装这种差异。

每个流邯有一个协议和一个目标。指定协议和目标的方法是使用流标识符,其格式如下所示,我们对此已经熟悉了:

<scheme>://<target>

其中,<scheme>是流的封装协议,<target>是流的数据源。如下所示使用HTTP流封装协议创建了一个与Flickr API通信的PHP流。

<?php
$json = file_get_contents(
    'http://api.flickr.com/services/feeds/photos_public.gne?format=json'
);

不要误以为这是普通的网页URLfile_get_contents()函数的字符串参数其实是一个流标识符。http协议会让PHP使用HTTP流封装协议。 在这个参数中,http之后是流的目标。流的目标之所以看起来像是普通的网页URL,是因为HTTP流封装协议就是这祥规定的。其他流封装协议可能不是这样。

注意:前而一段要多读几遍,直到熟记为止。很多PHP开发者不知道普通的URL其实是PHP流封装协议标识符的伪装。

file://流封装协议

我们使用fi1e_get_contents()fopen()fwrite()fc1ose()函数读写文件系统。因为PHP默认使用的流封装协议是file://,所以我们很少认为这些函数使用的是PHP流。我们在不经意间就使用了PHP流!如下所示使用file://流封装协议创建了一个读写/etc/hosts文件的流。

<?php
$handle = fopen('/etc/hosts', 'rb');
while (feof($handle) !== true) {
    echo fgets($hand1e);
}
fclose($handle);

和下面示例的作用一样,不过这一次我们在流标识符中明确指定了file://流封装协议。

<?php
$handle = fopen('file:///etc/hosts', 'rb');
while (feof($hand1e) !== true) {
    echo fgets($handle);
}
fclose($handle);

我们通常会省略file://封装协议,因为这是PHP使用的默认值。

php://流封装协议

编写命令行脚本的PHP开发者会感激php://流封装协议。这个流封装协议的作用是与PHP脚本的标准输人、标准输出和标准错误文件描述符通信。我们可以使用PHP提供的文件系统函数打开、读取或写入下述四个流:

php://stdin

这是个只读PHP流,其中的数据来自标准输入。例如,PHP脚本可以使用这个流接收命令行传入脚本的信息。

php://stdout

这个PHP流的作用是把数据写入当前的输出缓冲区。这个流只能写,无法读或寻址。

php://memory

这个PHP流的作用是从系统内存中读取数据,或者把数据写入系统内存。这个PHP流的缺点是,可用内存是有限的。使用php://temp流更安全。

php://temp

这个PHP流的作用和php://memoey类似,不过,没有可用内存时,PHP会把数据写入临时文件。

其他流封装协议

PHPPHP扩展还提供了很多其他流封装协议,例如,与ZIPTAR压缩文件、FTP服务器、数据压缩库、亚马逊API等通信的流封装协议。开发者经常误以为PHP中的fopen()fgets()fputs()feof()fc1ose()等文件系统函数只能用来处理文件系统中的文件。 事实并非如此。 PHP的文件系统函数能在所有支持这些函数的流封装协议中使用。例如,我们可以使用fopen()fgets()fputs()feof()fc1ose()函数处理ZIP压缩文件和亚马逊S3服务(通过自定义的S3封装协议,http://bit.1y/streamwrap),甚至还能处理Dropbox中的文件(通过自定义的Dropbox封装协议)。

注意: 关于php://流封装协议的更多信息,请查看PHP网站

自定义流封装协议

我们还可以白己编写PHP流封装协议。PHP提供了一个示例streamWrapper类,演示如何编写自定义的流封装协议,支持部分或全部PHP文件系统函数。关于如何编写自定义的PHP流封装协议,更多信息参见:

有些PHP流能接受一系列可选的参数,这些参数叫流上下文,用干定制流的行为。不同的流封装协议使用的上下义参数有所不同。流上下文使用stream_context_create()函数创建。这个函数返回的上下文对象可以传入大多数文件系统和流函数。

例如, 你知道可以使用fi1e_get_contents()函数发送HTTP POST请求吗? 如果想这么做,可以使用一个流上下文对象 (如下所示) 。

<?php
$quuestBody = '{"username":"josh"}';
$context = stream_context_create(array(
    'http' => array(
        'method' => 'POST',
        'header' => "Content-Type: application/json;charset=utf-8;\r\n" .
                    "Content-Length: " . mb_strlen($requestBody),
        'content' => $requestBody
    )
));

$response = file_get_contents('https://my-api.com/users', false, $context);

流上下文是个关联数组,最外层键是流封装协议的名称。流上下文数组中的值针对每个具体的流封装协议。可用的设置参见各个PHP流封装协议的文档。

目前为止我们讨论了如何打开流,从流中读取数据,以及把数据写入流。可是,PHP流真正强大的地方在于过滤、转换、添加或删除流中传输的数据。例如,我们可以打开一个流处理Markdown文件,在把文件内容读入内存的过程中自动将其转换成HTML。

注意:PHP内置了几个流过滤器: string.rot13string.toupperstring.tolowerstring.strip_tags。这些过滤器没什么用,我们要使用自定义的过滤器。

若想把过滤器附加到现有的流上,要使用stream_filter_append()函数。如下所示从本地文件系统中的文本文件里读取数据时使用了string.toupper过滤器,目的是把义件中的内容转换成大写字母。我不建议使用这个过滤器,这里只是演示如何把过滤器附加到流上。

<?php
$handle = fopen('data.txt', 'rb');
stream_filter_append($handle, 'string.toupper');
while(feof($handle) !== true) {
    echo fgets($handle); // <-- 输出的全是大写字母
}
fclose($handle);

我们还可以使用php://filter流封装协议把过滤器附加到流上。不过,使用这种方式之前必须先打开PHP流。和下面的示例的作用和前一个示例一样,可是这里我们使用php://filter方式附加过滤器。

<?php
$handle = fopen('php://filter/read=string.toupper/resource-data.txt', 'rb');
whi1e(feof($handle) !== true) {
    echo fgets($hand1e); // <-- 输出的全是大写字母
}
fclose($handle);

我们要特别注意fopen()函数的第一个参数。这个参数的值是php://流封装协议的流标识符。达个流标识符中的目标如下所示:

filter/read=<filter_name>/resource=<scheme>://<target>

这种方式和stream_fi1ter_append()函数相比较为繁琐。可是,PHP的某些文件系统函数在调用后无法附加过滤器,例如f11e()fpassthru()。所以,使用这些函数时只能使用php://filter流封装协议附加流过滤器。

下面看个更实际的流过滤器示例。 在New Media Campaigns,我们内部的内容管理系统会把nginx访问日志保存到rsync.net。我们把一天的访问情况保存在一个日志文件中,而且会使用bzip2压缩每个日志文件。日志文件的名称使用yyyy-MM-DD.log.bz2格式。领导让我提取过去30天某个域名的访问数据,这听起来有很多事要做,对吧?我要计算日期范围,确定日志文件的名称,通过FTP连接rsync.net,下载文件,解压缩文件,逐行迭代每个文件,把相应的行提取出来,然后把访间数据写入一个输出目标。你可能不相信,使用PHP流,不到20行代码就能做完所有这些事情(如下所示)。

<?php

$dateStart = new \DateTime;

$dateInterval = \DateInterval::createFromDateString('-1 day');
$datePeriod = new \DatePeriod($dateStart, $dateInterval, 30);
foreach ($datePeriod as $date) {
    $file = 'sftp://USER:[email protected]/' . $date->format('Y—m-d') . '.log.bz2';
    if (file_exists($file)) {
        $handle = fopen($file, 'rb');
        stream_filter_append($hand1e, 'bzip2.decompress');
        while (feof($handle) !== true) {
            $line = fgets($handle);
            if (strpos($line, 'www.example.com') !== false) {
                fwrite(STDOUT, $line);
            }
        }
        fclose($handle);
    }
}

在上面的例子中:

  • 第2~4行创建一个持续30天的DatePeriod实例,一天一天反向向前推移。

  • 第6行使用每次迭代DatePeriod实例得到的DateTime实例创建日志文件的文件名。

  • 第8~9行使用SFTP流封装协议打开位于rsync.net上的日志文件流资源。我们把bzip2-decompress流过滤器附加到日志文件流资源上,实时解压缩bzip2格式的日志文件。

  • 第10~15行使用PHP原生的文件系统函数迭代解压缩后的日志文件。

  • 第12~14行检查各行日志,看访问的是不是指定域名。如果是,把这一行日志写入标准输出。

使用bzip2.decompress流过滤器可以在读取日志文件的同时自动解压缩。除此之外,我们还可以使用shell_exe()bzdecompress()函数,手动把日志文件解压缩到临时目录中,然后迭代解压缩后的文件,等PHP脚本完成任务后再清理这些解压缩后的文件。不过,使用PHP流更简单,也更忧雅。

自定义流过滤器

我们还可以编写自定义的流过滤器。其实,犬多数情况下部要使用自定义的流过滤器。自定义的流过滤器是个PHP类,扩展内置的php_user_filter类这个类必须实现filter()onCreate()onC1ose()方法。而且,必须使用stream_filter_register()函数注册自定义的流过滤器。

注意 桶排成一排流过来了! PHP流会把数据分成按次序排列的桶,一个桶中盛放的流数据量是固定的(例如4096字节)。如果还用管道比喻,就是把水放在一个个水桶中,顺着管道从出发地溧流到目的地,在漂流的过程中会经过流过滤器,流过滤器一次能接收并处理一个或多个桶。一定时闾内过滤器接收到的桶叫做桶队列。

下面我们自定义一个流过滤器,在把流中的数据读入内存时审查其中的脏字 (如下所示) 。首先,我们必须创建一个PHP类,让它扩展php_user_filter类。这个类必须实现filter()方法,这个方法是个筛子,用于过滤流经的桶。这个方法的参数是上游漂来的桶队列,处理过队列中的每个桶对象后,再把桶排成一排,向下游的目的地漂去。我们自定义的DirtyWordsFilter流过滤器如下所示。

建议: 桶队列中的每个桶对象都有两个公开属性:datadatalen。这两个属性的值分别是桶中的内容和内容的长度。

class DirtyWordsFilter extends php_user_fi1ter
{
    /**
    * @param resource $in 流来的桶队列
    * @param resource $out 流走的桶队列
    * @param int $consumed 处理的字节数
    * @param bool $closing 是流中的最后一个桶排序吗?
    */
    public function filter($in, $out, &$consumed, $closing)
    {
        $words = array('inme', 'dirt', 'grease');
        $wordData = array();
        foreach ($words as $word) {
            $replacement = array_fill(0, mh_strlen($word), '*');
            $wordData[$word] = implode(", $replacement);
        }
        $bad = array_keys($wordData);
        $good = array_values($wordData);

        // 迭代流来的桶队列中的每个桶

        while ($bucket = stream_bucket_make_writeable($in)) {
            // 审查桶数据中的脏字
            $bucket->data = str_replace($bad, $good, $bucket->data);

            // 增加已处理的数据量

            $consumed += $bucket->datalen;

            // 把桶放入流向下游的队列中
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

filter()方法的作用是接收、处理再转运桶中的流数据。在filter()方法中,我们迭代桶队列$in中的桶,把脏字替换成审查后的值。这个方法的返回值是PSFS_PASS_ON常量,表示操作成功。这个方法接收四个参数:

$in

上游流来的一个队列,有一个或多个桶,桶中是从出发地流来的数据。

$out

由一个桶或多个桶组成的队列,流向下游的流目的地。

&$consumed

自定义的过滤器处理的流数据总字节数。

$closing

filter())方法接收的是最后一个桶队列吗?

然后,我们必须使用stream_filter_register函数注册这个自定义的DirtyWordsFilter流过滤器(如下所示)。

<?php
stream_filter_register('dirty_words_filter', 'DirtyNordsFilter');

第一个参数是用于识别这个自定义过滤器的过滤器名,第二个参数是这个自定义过滤器的类名。现在可以使用这个自定义的流过滤器了(如下所示)。

<?php
$hand1e = fopen('data.txt', 'rb');
stream_filter_append($handle, 'dirty_words_filter');
while (feof($handle) !== true) {
    echo fgets($hand1e); // <-- 输出审查后的文本
}
fclose($handle);

建议:如果想进一步学习PHP流,请观看Nomad PHP网站中伊丽莎白-史密斯的演讲。这个视频不是免费的, 但值那个价。你还可以阅读PHP文档,进一步学习PHP流。


Powered By Valine
v1.4.18

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK