PHP流处理
来自《MordenPHP》的流处理篇
流的作用是在出发地和目的地之间传输数据.
如果读取过文件,就是操作过流.
如:
php://stdin 从标准输入,读取输入的数据
php://stdout 从标准输入,输出数据
file_get_contents();
fopen();
fgets();
fwrite();
等等,提供了处理不同流资源的统一接口.
流封装数据的种类各异,每个类型需要独特的协议.
以便于读写,我们称之为这些为流封装协议.
例如我们读写文件,http&https请求,或ssh连接,
还可以读写,zip,tar等压缩文件.
这些操作都包含下述相同的过程.
1. 开始通信
2. 读取数据
3. 写入数据
4. 结束通信
虽然过程是一样的, 但是读写系统文件和手法http消息的方式不同.
流封装协议的作用是使用通用的接口封装这些差异.
每个流都有一个协议和目标,指定协议的和目标的方法是使用流标识符,格式如下:
<scheme>://<targett>
scheme为封装协议
target为流的数据源
示例:
使用HTTP流分装协议与api通信
<?php
$json = file_get_contents(
'http://www.baidu.com'
);
不要以为这是普通的网页URL,file_get_contents();
函数的字符串, 其实是一个流标识符.
http协议会让PHP使用http流封装协议.这个参数中,
http之后是流的目标,流的目标之所以看起来像是普通的URL.
是因为http流封装协议就是这样的规定.其他流封装协议可能不是这样
file://流封装协议
我们使用file_get_contents();fopen();fwrite();fgets();等等
函数读写系统文件,因为PHP默认使用流封装协议是file://,
所以我们很少认为这些函数使用的时PHP流.
隐式使用file://流封装协议
<?php
$handle = fopen('/etc/hosts','rb');
while(fed($handle) != true){
echo fgets($handle);
}
flose($handle);
显式使用file://流封装协议
<?php
//我们通常会省略file://因为这是PHP使用的默认值
$handle = fopen('file:///etc/hosts','rb');
while(fed($handle) != true){
echo fgets($handle);
}
flose($handle);
PHP://流封装协议
php:// — 访问各个输入/输出流(I/O streams)
- php://input
php://input 是个可以访问请求的原始数据的只读流。
POST 请求的情况下,最好使用 php://input 来代替 $HTTP_RAW_POST_DATA,因为它不依赖于特定的 php.ini 指令。
而且,这样的情况下 $HTTP_RAW_POST_DATA($_POST) 默认没有填充, 比激活 always_populate_raw_post_data 潜在需要更少的内存。
enctype="multipart/form-data" 的时候 php://input 是无效的。
$HTTP_RAW_POST_DATA在PHP7中已被弃用所以可以用file_get_contents("php://input");
- php://output
php://output 是一个只写的数据流, 允许你以 print 和 echo 一样的方式 写入到输出缓冲区。
- php://stdin
这个是只读的PHP流其中数据来自标准输入.
如:PHP脚本可以用这个流接收命令行转入脚本的信息.
- php://stdout
这个PHP流的作用是把数据写入到当前的缓冲区.
这个流只能写,无法读取或寻址.
- php://memory
这个PHP流的作用是从系统内存中读取数据,或者把书写入系统内存.
这个PHP流的缺点是可用内存有限.
使用php://temp更安全
- php://temp
这个PHP流的作用和php://memory类似,不过没有可用内存时,PHP会把数据写入临时文件.
自定义流封装协议
我们还可以自己编写PHP流封装协议,PHP提供了一个streamWrapper类,演示如何编写自定义的流封装协议.
流的上下文
有些PHP流能接受一系列的可选参数,这些参数叫流上下文.
用于定制流的行为.不同的流封装协议使用的上下文参数也有所不同.
流上下文使用 stream_context_create()函数创建.
这个函数返回的上下文对象可以传入大多数文件系统和流函数.
流上下文是一个关联数组,最外层键是刘封装协议的名称.
上下文数组中的值,针对每个具体的流封装协议.
示例: 流上下文
<?php
$requestBody = '{'username':'jjj'}';
$context = stream_context_create([
'http' = [
'method'=>'post',
'header'=>'Content-Type:application/json;charset=utf-8;\r\n'.
'Content-Length:'.mb_strlen($reqeustBody),
'content'=>$requestBody,
]
]);
$response = file_get_contents('http://myhost.com', false, $context);
流过滤器
PHP中的流真正强大的地方在于,过滤,转换,添加,删除流中的数据.
例如我们开一个流处理Markdown文件, 在把文件内容读入内存的过程中,
自动将其转换为HTML.
注意
PHP内置了几个流过滤器,
string.rot13,string.toupper,string.tolower和string.strip_tags;
通过输出stream_get_filters()可以查看支持的内置过滤器
这些过滤器,没什么用. 我们要自定义过滤器.
若想把过滤器附加到现有的流上,要使用stream_filter_append()函数, 下面的案例是从本地文件中读取数据,同时使用了string.toupper过滤器. 目的是把文件所有的内容转换为大写字母. 我不建议这么写, 下面这里也只是做演示而已.
$handle = fopen("data.txt","rb");
stream_filter_append($handle, "string.toupper");
while (feof($handle) !== true) {
echo fgets($handle); //输出的全是大写字母
}
fclose($handle);
我们还可以使用php://filter流封装协议将过滤器附加到流上,不过使用这种方法之前, 要先打开PHP流.
$handle = fopen('php://filter/read=string.toupper/resource=data.txt','rb');
while (feof($handle) !== true) {
echo fgets($handle); //输出的全是大写字母
}
fclose($handle);
我们要特别注意fopen()函数的第一个参数,这个参数的值是php://流封装协议的流标识符.这个流标识符中的目标如下所示
filter/read=<filter_name>/resource=<scheme>://<target>
这种方式和stream_filter_append();函数相比较为繁琐.可是PHP的某些文件系统函数, 在调用后无法附加过滤器.例如:file()和fpassthru().所以这些函数只能使用php://filter流封装协议附加流过滤器
下面是一个更加实用的案例 我们内部的内容管理系统会把NGINX访问日志保存到rsync.net,我们把一天的的访问情况保存到一个日志文件中. 而且会用bz2压缩每一个日志文件.日志文件的名称使用YYYY-MM-DD.log.bz2格式. 领导让我提取过去30天的某个域名的访问数据. 我要计算日期范围,确定日志文件的名称,通过ftp连接rsync.net,下载文件,解压缩文件,逐行迭代每个文件,把响应的行取出来.然后把访问数据写入一个输出目标.
<?php
$dateStart = new \DateTime();
$dateInterval = \DateInterval::createFromDateString('-1 day');
$datePeriod = new \DatePeriod($dateStart, $dateInterval, 30);
foreach ($datePeriod as $date) {
$file = 'sftp://USER:PASS@rsync.net/' . $date->format('Y-m-d') . '.log.bz2';
if (file_exists($file)) {
$handle = fopen($file, 'rb');
stream_filter_append($handle, 'bzip2.decompress');
while (feof($handle) !== true) {
$line = fgets($handle);
if (strpos($line, 'www.example.com') !== false) {
fwrite(STDOUT, $line);
}
}
fclose($handle);
}
}
//2-4行创建有一个连续30天的DataPeriod实例,一天一天反向向前推.
//6行使用每次迭代DataPeriod实例,得到DateTime实例创建日志文件的文件名.
//8-9行使用sftp流封装协议打开位于rsync.net上的日志文件流资源.我们把bzip2.decompress流顾虑器附加到日志文件流资源上.实时解压缩bzip2格式的日志文件
//10-15行使用PHP原生的文件系统函数迭代解压缩后的日志文件
//12-14行检查各行日志,看访问的是不是指定域名,如果是把这一行日志写入到标准输出.
自定义流过滤器
我们还可以编写自定义的流过滤器.其实,大多数情况下都要用自定义的流过滤器.
自定义的流过滤器是一个PHP类,扩展内置的php_user_filter类.
这个类必须实现filter(),onCreate()和onClose()方法.
而且,必须使用 stream_filter_register() 函数注册自定义的流过滤器.
注意: 桶排场一排流过来.
PHP流会把数据分成按照次序排列的桶, 一个桶中盛放的流数据时固定的(例如4096字节).
如果还用管道比喻, 就是把水放在一个个水桶中个, 顺着管道 从出发地漂流到目的地,在漂亮的过程中,会经过流过滤器.
流过滤器一次能接受并处理一个或多个桶.一定时间内过滤器接收到的桶叫做桶队列.
下面我们自定义一个流过滤器,把流中的数据读入内存时审查其中的脏字. 首先, 我们必须建立一个PHP类,让他扩展php_user_filter类. 这个类必须实现filter()方法.这个方法是一个筛子, 用于过滤流经的桶.这个方法的参数是上游流来的桶队列. 处理经过队列中的每个桶对象后.再把桶排成一排, 向下游的目的地漂去. 我们自定义的额DirtyWordsFilter流处理器如下所示
//自定义的DirtyWordsFilter流过滤器
class DirtyWordsFilter extends php_user_filter
{
/**
* Method filter
*
* @desc ......
* @author liuhao <lh@btctrade.com>
* @date 2018/10/10
* @time 16:16
*
* @param resource $in 流来的桶队列
* @param resource $out 流走的桶队列
* @param int $consumed 处理的字节数
* @param bool $closing 是流中最后一个桶队列吗?
*
* @return int
*/
public function filter($in, $out, &$consumed, $closing)
{
$words = ['grime', 'dirt', 'grease'];
$wordData = [];
foreach ($words as $word) {
$replacement = array_filter(0, mb_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;
}
}
上面的PHP类的简单解析 filter()方法的作用是接收, 处理再转运桶中的流数据. 在filter()方法中,我们迭代桶队列$in中的桶, 把脏字替换成审核后的值. 这个方法的返回值是PSFS_PASS_ON常量. 表示操作成交, 这个方法接收4个参数:
$in 上游流来的一个队列,有一个或多个桶,桶中是从出发地流来的数据. $out 由一个桶或者多个桶组成的队列, 向下游的流目的地. $consumed 自定义的过滤器处理的流数据总字节数. $closing filter()方法接收的时最后一个桶队列吗
然后我们必须使用stream_filter_register()函数注册这个自定义的DirtWordsFilter过滤器 如下:
<?php
stream_filter_register('dirty_words_filter','DirtyWrodsFilter');
第一个参数是用于识别这个自定义过滤器的过滤器名. 第二个参数是这个自定义过滤器的类名.
现在可以使用这个自定义过滤器了, 如下所示:
<?php
//使用DirtyWordsFilter流处理器
$handle = fopen('data.txt','rb');
stream_filter_append($handle, 'dirty_word_filter');
while(feof($handle) !== true){
echo fgets($handle); //输出审查后的文本
}
fclose();
将获取到的流进行base64
之前在做OTC项目中需要将图片做成base64,作为接口参数返回
但是GD2的imagejpeg()/imagepng()等函数生成资源时不返回任何数据,它们直接将图像数据写入输出流(或文件)。
如果希望捕获编码为base64的数据,最简单的方法是使用PHP输出控制函数,然后在$image_data上使用base64_encode。
//来源: https://stackoverflow.com/questions/8551754/convert-gd-output-to-base64
//todo 这里是验证码图片的流程
//这里是在图片输出时缓冲在内存中, 一次性拿到, 然后base64一下
ob_start ();
imagejpeg ($img);
$image_data = ob_get_contents ();
ob_end_clean ();
$image_data_base64 = base64_encode ($image_data);
imagedestroy($img)