源码地址:https://github.com/Tinywan/PHP_Experience
测试环境配置:
- 环境:Windows 7系统 、PHP7.0、Apache服务器
- PHP框架:ThinkPHP框架(3.2)
- Redis数据库:测试数据回调函数:通过一个Redis的自增incr来测试异步脚本执行的次数和访问的时间(平时都是用Redis测试写日志的)
- 编辑器:Visual Studio Code (CLI运行环境好看点)
PHP 的命令行模式
从版本 4.3.0 开始,PHP 提供了一种新类型的 CLI SAPI(Server Application Programming Interface,服务端应用编程端口)支持,名为 CLI,意为 Command Line Interface,即命令行接口。顾名思义,该 CLI SAPI 模块主要用作 PHP 的开发外壳应用。CLI SAPI 和其它 CLI SAPI 模块相比有很多的不同之处,我们将在本章中详细阐述。值得一提的是,CLI 和 CGI 是不同的 SAPI,尽管它们之间有很多共同的行为。
PHP命令行(CLI)参数详解
-a 以交互式shell模式运行
-c | 指定php.ini文件所在的目录
-n 指定不使用php.ini文件
-d foo[=bar] 定义一个INI实体,key为foo,value为'bar'
-e 为调试和分析生成扩展信息
-f 解释和执行文件.
-h 打印帮助
-i 显示PHP的基本信息
-l 进行语法检查 (lint)
-m 显示编译到内核的模块
-r 运行PHP代码,不需要使用标签 ..?>
-B 在处理输入之前先执行PHP代码
-R 对输入的没一行作为PHP代码运行
-F Parse and execute for every input line
-E Run PHP after processing all input lines
-H Hide any passed arguments from external tools.
-S : 运行内建的web服务器.
-t 指定用于内建web服务器的文档根目录
-s 输出HTML语法高亮的源码
-v 输出PHP的版本号
-w 输出去掉注释和空格的源码
-z 载入Zend扩展文件 .
args... 传递给要运行的脚本的参数. 当第一个参数以-开始或者是脚本是从标准输入读取的时候,使用--参数
--ini 显示PHP的配置文件名
--rf 显示关于函数 的信息.
--rc 显示关于类 的信息.
--re 显示关于扩展 的信息.
--rz 显示关于Zend扩展 的信息.
--ri 显示扩展 的配置信息.
启动内建web服务器,并且默认以当前目录为工作目录:
PHP 7.0.10 Development Server started at Sun Feb 26 17:48:25 2017 Listening on http://localhost:8000 Document root is E:wamp64wwwcli Press Ctrl-C to quit.
查找PHP的配置文件
在有的时候,由于服务器上软件安装比较混乱,我们可能安装了多个版本的PHP环境,这时候,如何定位我们的PHP程序使用的是那个配置文件就比较重要了。在PHP命令行参数中,提供了–ini参数,使用该参数,可以列出当前PHP的配置文件信息。
上述的服务器上我们安装了两个版本的PHP,由上可以看到,使用php –ini命令可以很方便的定位当前PHP命令将会采用哪个配置文件。
查看类/函数/扩展信息
通常,可以使用php –info命令或者在在web服务器上的php程序中使用函数phpinfo()显示php的信息,然后再查找相关类、扩展或者函数的信息,这样做实在是麻烦了一些。
我们可以使用下列参数更加方便的查看这些信息
--rf 显示关于函数 的信息. --rc 显示关于类 的信息. --re 显示关于扩展 的信息. --rz 显示关于Zend扩展 的信息. --ri 显示扩展 的配置信息.
查看扩展redis的配置信息
查看redis类的信息
查看函数printf的信息
语法检查
有时候,我们只需要检查php脚本是否存在语法错误,而不需要执行它,比如在一些编辑器或者IDE中检查PHP文件是否存在语法错误。使用-l(–syntax-check)可以只对PHP文件进行语法检查:
假如此时我们的index.php中存在语法错误
PHP-CLI模式的优势及使用场合
-
完全支持多线程
-
实现定时任务
-
开发桌面应用就是使用PHP-CLI和GTK包
-
linux下用php编写shell脚本
PHP 的命令行模式扩展
其实PHP的运行环境远远不止apache和cli,如aolserver, apache, apache2filter, apache2handler, caudium, cgi (until PHP 5.3), cgi-fcgi, cli, continuity, embed, isapi, litespeed, milter, nsapi, phttpd, pi3web, roxen, thttpd, tux, and webjames.可以用php_sapi_name()这个函数去检测,这里只检测Apache服务器和Windows CMD扩展,下面编写一个cli.php文件进行测试:
<?php echo "PHP current cli mode :".php_sapi_name();
Windows cmd命令行模式运行结果:
在Apache服务器模式下运行结果:
PHP 的命令行自变量
和所有的外壳应用程序一样,PHP 的二进制文件(php.exe 文件)及其运行的 PHP 脚本能够接受一系列的参数。PHP 没有限制传送给脚本程序的参数的个数(外壳程序对命令行的字符数有限制,但通常都不会超过该限制)。传递给脚本的参数可在全局变量 $argv 中获取。该数组中下标为零的成员为脚本的名称(当 PHP 代码来自标准输入获直接用 -r 参数以命令行方式运行时,该名称为“-”)。另外,全局变量 $argc 存有 $argv 数组中成员变量的个数(而非传送给脚本程序的参数的个数)。
PHP CLI带有两个特殊的变量,专门用来达到这个目的:一个是$argv变量,它通过命令行把传递给PHP脚本的参数保存为单独的数组元素;另一个是$argc变量,它用来保存$argv数组里元素的个数。
建立一个测试文件cli.php:
<?php echo "argv:".print_r($argv)." "; echo "argc:".$argc;
测试结果如下所示:
了解更多,请参考官方手册:http://php.net/manual/zh/features.commandline.php
至此,PHP 命令行模式基本知识已介绍完毕!
下面进入实战模式:
环境介绍,Wamp环境,ThinkPHP 3.2 框架的cli模式
首先在应用程序(我这里的是:Backend)的下新建一个Library模块,在该模块中新建一个Index控制器,新建一个cmdCliTest方法,如下所示
// 定义应用目录 define('APP_PATH',dirname(__FILE__).'/Backend/');
// 定义CLI运行模式运行的项目路径 define('CLI_PATH',dirname(__FILE__)."\");
cmdCliTest方法文件内容如下所示:
//这个方法将被cli模式调用 public function cmdCliTest() { echo date("Y-m-d H:i:s",time()).' : ThinnPHP cli Mode Run Success:'; }
第一种,使用Apache服务器方式访问该方法,输出结果:
第二种,首先CMD到当前项目目录!!!,下面使用命令行模式输出结果:
第三种,通过Apache服务器方式运行命令行模式,这里就要涉及到一个PHP系统函数exec(),在当前控制器(Library模块index控制器)新建一个测试方法apacheToCli
//通过APache 服务器方式启动一个CLi进程 public function apacheToCli() { // echo CLI_PATH."cli.php Library/index/test"; echo '------------------------------------启动一个CLi进程 开始--------------------------------'; exec("E:wamp64inphpphp7.0.10php.exe E:wamp64wwwThinkPhpStudycli.php /Library/index/cmdCliTest 2>&1",$output, $return_val); echo "<hr/>"; var_dump($output); //命令执行后生成的数组 echo "<hr/>"; var_dump("执行的状态:".$return_val); //执行的状态 echo '-----------------------------------启动一个CLi进程 结束----------------------------------'; }
cmdCliTest方法:
//这个方法将被cli模式调用 public function cmdCliTest() { sleep(10); //方便我们在任务管理器查看PHP cli进程, echo date("Y-m-d H:i:s",time()).' cmdCliTest()这个方法将被cli模式调用: ThinnPHP cli Mode Run Success:'; }
这时候我们在Apache服务器模式下测试,可以看出在Apache服务器模式下运行的时候通过系统函数exec()成功的启动了一个php cli 进程,同时打印出了cli命令行模式执行后的结果通过第二个变量存储的$output中,打印出了返回的结果,同时第三个参数的执行装填也是:0(表示成功)
PHP的exec()函数无返回值排查方法
exec执行某命令在命令行下没有问题,但是在PHP中就出错。这个问题99.99%与权限有关,但是exec执行的命令不会返回错误。一个技巧就是使用管道命令,假设你的exec调用如下:
exec("E:wamp64inphpphp7.0.10php.exe E:wamp64wwwThinkPhpStudycli.php /Library/index/cmdCliTest",$output, $return_val);
可以更改如下:
exec("E:wamp64inphpphp7.0.10php.exe E:wamp64wwwThinkPhpStudycli.php /Library/index/cmdCliTest 2>&1",$output, $return_val); var_dump($output); var_dump($return_val);
使用 2>&1, 命令就会输出shell执行时的错误到$output变量, 输出该变量即可分析。
注意: exec有3个参数,第一个是要执行的命令,第二个是参数是一个数组,数组的值是由第一个命令执行后生成的,第三个参数执行的状态,0表示成功,其他都表示失败。在php里面一共有三个函数可以用来执行外部命令system,exec,passthru。
PHP 执行shell脚本的返回值
exec执行一个shell 脚本:
$cmdStr = "ffmpeg/script/check_location_cut.sh {$activityid2} {$sourcefile} {$starttime} {$endtime} {$editid} {$video_desc}"; exec("{$cmdStr}",$output, $sysStatus);
第一次执行这个脚本的时候,脚本中的命令是执行成功的,但是每次回调的状态码 $sysStatus 一直是1 而不是0(表示成功),最终的原因是在shell脚本最后的返回值出现了错误:exit 1 其实在这里exit 1 表示的是错误的输出。所以在这里修改为 exit 0 则就是没有错误信息了
exit 命令同于退出shell,并返回给定值。在shell脚本中可以终止当前脚本执行。执行exit可使shell以指定的状态值退出。若不设置状态值参数,则shell以预设值退出。状态值0代表执行成功,其他值代表执行失败。
PHP+Mysql批量发送邮件
关于发送邮件的见另外一篇博客:http://www.cnblogs.com/tinywan/p/5868013.html
两个方法代码(Windows 环境测试,如果是Linux测试环境的话,请看后面相关内容)
// public function apacheToCliEmail() { echo '------------------------------------启动一个CLi进程 开始--------------------------------'; exec("E:wamp64inphpphp7.0.10php.exe E:wamp64wwwThinkPhpStudycli.php /Library/Email/taskTable 2>&1", $output, $return_val); echo "<hr/>"; var_dump($output); //命令执行后生成的数组 echo "<hr/>"; var_dump("执行的状态:" . $return_val); //执行的状态 echo '-----------------------------------启动一个CLi进程 结束----------------------------------'; }
命令行模式cli 需要执行的方法(命令行下为一个文件,不一定是php文件)
//cli 命令行需要执行的php文件 public function taskTable() { $model = M("TaskList"); $status = 0; $conditions = array('status' => ':status'); $result = $model->where($conditions)->bind(':status', $status)->select(); if (empty($result)) exit('没有可发送的邮件'); echo '开始发送邮件:' . " "; foreach ($result as $key => $value) { //发送邮件 $result = send_email($value['user_email'], 'Tinywan激活邮件', "https://github.com/Tinywan"); //发送成功 if ($result['error'] == 0) { //修改数据库字段status 的值为1 $model->where(array('id' => $value['id']))->setField('status', 1); } sleep(10); //其实在这里可以添加一个状态表示没有发送成功的标记,修改数据库字段status 的值为2 //$model->where(array('id' => $value['id']))->setField('status', 2); } exit('发送邮件结束'); }
测试结果:
发送邮件的方法
/** * 发送邮件 * @param array $address 需要发送的邮箱地址 发送给多个地址需要写成数组形式 * @param string $subject 标题 * @param string $content 内容 * @return array 放回状态吗和提示信息 */ function send_email($address, $subject, $content) { $email_smtp = C('EMAIL_SMTP'); $email_username = C('EMAIL_USERNAME'); $email_password = C('EMAIL_PASSWORD'); $email_from_name = C('EMAIL_FROM_NAME'); if (empty($email_smtp) || empty($email_username) || empty($email_password) || empty($email_from_name)) { return ["error" => 1, "message" => '邮箱请求参数不全,请检测邮箱的合法性']; } $phpmailer = new PHPMailer(); // 设置PHPMailer使用SMTP服务器发送Email $phpmailer->IsSMTP(); // 设置为html格式 $phpmailer->IsHTML(true); // 设置邮件的字符编码' $phpmailer->CharSet = 'UTF-8'; // 设置SMTP服务器。 $phpmailer->Host = $email_smtp; // 设置为"需要验证" $phpmailer->SMTPAuth = true; // 设置用户名 $phpmailer->Username = $email_username; // 设置密码 $phpmailer->Password = $email_password; // 设置邮件头的From字段。 $phpmailer->From = $email_username; // 设置发件人名字 $phpmailer->FromName = $email_from_name; // 添加收件人地址,可以多次使用来添加多个收件人 if (is_array($address)) { foreach ($address as $addressv) { //验证邮件地址,非邮箱地址返回为false if(false === filter_var($address,FILTER_VALIDATE_EMAIL)){ return ["error" => 1, "message" => '邮箱格式错误']; } $phpmailer->AddAddress($addressv); } } else { //验证邮件地址,非邮箱地址返回为false if(false === filter_var($address,FILTER_VALIDATE_EMAIL)){ return ["error" => 1, "message" => '邮箱格式错误']; } $phpmailer->AddAddress($address); } // 设置邮件标题 $phpmailer->Subject = $subject; // 设置邮件正文,这里最好修改为这个,不是boby $phpmailer->MsgHTML($content); // 发送邮件。 if (!$phpmailer->Send()) { return ["error" => 1, "message" => $phpmailer->ErrorInfo]; } return ["error" => 0]; }
PHP 异步执行脚本
这里说的异步执行是让PHP脚本在后台挂起一个执行具体操作的脚本,主脚本退出后,挂起的脚本还能继续执行。比如执行某些耗时操作或可以并行执行的操作,可以采用php异步执行的方式。主脚本和子脚本的通讯可以采用外部文件或memcached的方式。原理就是通过exec或system来执行一个外部命令。注意:在这里所述的是针对Linux环境(不是Windows 环境哦!)。
在Linux下要让一个脚本挂在后台执行可以在命令的结尾加上一个 “&” 符号,有时候这还不够,需要借助nohup命令,这个命令下面有专门的介绍
Cli环境和Web环境执行的操作还不太一样。先来说CLI环境,这里需要用上nohup和&,同时还要把指定输出,如果不想要输出结果,可以把输出定向到/dev/null中。现在来做一个测试,假设在一个目录中有main.php、sub1.php和sub2.php,其中sub1和sub2内容一样都让sleep函数暂停一段时间。代码如下:
//main.php <?php $cmd = 'nohup php ./sub.php >./tmp.log &'; exec($cmd); $cmd = 'nohup php ./sub1.php >/dev/null &'; exec($cmd); ?> //sub1.php sub2.php <?php sleep(100000); ?>
上述文件中main.php是作为主脚本,在命令行中执行php main.php,可以看到main.php脚本很快就执行完并退出。在使用ps aux | grep sub命令搜索进程,应该可以在后台看到上述的两个子脚本,说明成功挂起了子脚本。
在Web环境下,执行php脚本都是Web服务器开启的cgi进程来处理,只要脚本不退出,就会一直占有该cgi进程,当启动的所有cgi进程都被占用完后就不能在处理新的请求。所以对那些可能会很费时的脚本,可以采用异步的方式。启动子脚本的方式和CLI差不多,必须要使用&和指定输出(只好是定向到/dev/null),但是不能使用nohup。例如:
<?php $cmd = 'php PATH_TO_SUB1/sub1.php >/dev/null &'; exec($cmd); $cmd = 'php PATH_TO_SUB1/sub2.php >/dev/null &'; exec($cmd); ?>
当在浏览器中访问该脚本文件,可以看到浏览器里面响应完成,同时使用ps命令查看后台可以看到sub1和sub2脚本。注意上述例子中如果php命令不在PATH中,需要指定命令完整的路径。推荐使用完整路径,特别是在Web下。
nohup命令及其输出文件
今天在linux上部署wdt程序,在SSH客户端执行./start-dishi.sh,启动成功,在关闭SSH客户端后,运行的程序也同时终止了,怎样才能保证在推出SSH客户端后程序能一直执行呢?通过网上查找资料,发现需要使用nohup命令。完美解决方案:nohup ./start-dishi.sh >output 2>&1 & ,现对上面的命令进行下解释:
- 用途:不挂断地运行命令。
- 语法:nohup Command [ Arg ... ] [ & ]
- 描述:nohup 命令运行由 Command 参数和任何相关的 Arg 参数指定的命令,忽略所有挂断(SIGHUP)信号。在注销后使用 nohup 命令运行后台中的程序。要运行后台中的 nohup 命令,添加 & ( 表示“and”的符号)到命令的尾部。
操作系统中有三个常用的流:
- 0:标准输入流 stdin
- 1:标准输出流 stdout
- 2:标准错误流 stderr
一般当我们用 > console.txt,实际是 1>console.txt的省略用法;< console.txt ,实际是 0 < console.txt的省略用法。
测试案例结果:
ww@iZ232eoxo41Z:~/tinywan $ nohup ./start-dishi.sh >output 2>&1 &
说明:
- 带&的命令行,即使terminal(终端)关闭,或者电脑死机程序依然运行(前提是你把程序递交到服务器上)。
- 2>&1的意思是把标准错误(2)重定向到标准输出中(1),而标准输出又导入文件output里面,所以结果是标准错误和标准输出都导入文件output里面了。 至于为什么需要将标准错误重定向到标准输出的原因,那就归结为标准错误没有缓冲区,stdout有。这就会导致 >output 2>output 文件output被两次打开,而stdout和stderr将会竞争覆盖,这肯定不是我门想要的。
- /dev/null文件的作用,这是一个无底洞,任何东西都可以定向到这里,但是却无法打开。 所以一般很大的stdou和stderr当你不关心的时候可以利用stdout和stderr定向到这里:./command.sh >/dev/null 2>&1
注意:这就是为什么有人会写成: nohup ./command.sh >output 2>output出错的原因了
=============在Linux环境下PHP 异步执行脚本发送事件通知消息(实践)===============
需求(思想中心):很多客户会担心消息丢了怎么办,比如客户的服务器宕机了一下会儿,消息会不会丢失呢?为了保证消息可靠性保证机制是基于简单重传实现的,即:如果一条通知消息没有成功发送到您指定的回调URL,反复重试100次(自定义次数)。那怎么确认消息是已经送达您的服务器(客户端)了呢?这里是需要您的协助的:当您的服务器成功收到一条http事件通知消息时,例如在请求的URl中请求成功的时候返回一个字段,0表示客户端服务器已经接受到服务器发送的事件通知消息了,这时候脚本符合条件直接退出脚本执行即可(也就是后台运行的Cli php 后台程序)
测试环境配置:
- 环境:Linux(ubuntu 14.04) ,必须的安装好PHP的WEB环境和CLI环境
- PHP框架:Phalcon框架(3.0)
- Redis数据库:测试数据回调函数:通过一个Redis的自增incr来测试异步脚本执行的次数和访问的时间(平时都是用Redis测试写日志的)
Server 服务器端的执行代码
//CLI模式,模拟队列的形成
public function listExecAction()
{
$streamName = 4001488177666;
$fileSize = 123.001;
$duration = 123;
$video_url = "http://ip/data/{$streamName}/video/{$streamName}.mp4";
$callBackUrl = "http://ip/openapi/videoCallbackFunction?streamName={$streamName}&fileSize={$fileSize}&duration={$duration}&video_url={$video_url}";
echo '------------------------------------启动一个CLi进程 开始--------------------------------' . date("Y-m-d H:i:s");
exec("/usr/bin/php /home/www/tinywan/cli_demo.php '{$callBackUrl}' >/dev/null 2>&1 &");
echo "<hr/>";
echo '-----------------------------------启动一个CLi进程 结束----------------------------------' . date("Y-m-d H:i:s");
die;
}
Cli.php执行脚本代码,通过使用CURL的PHP扩展完成一个HTTP请求(GET方式),默认最大发送请求1000次,如果客户端已经接受到事件通知信息了,则立马跳出while循环,当然了后台脚本也就会执行结束了,如果客户端服务器返回状态JSON字符串值为0,则表示客户服务器成功的接受到了事件通知信息,这时候符合第二个条件,则立马跳出循环,停止后台脚本的执行。
<?php $count = 0; while (true) { $count++; $ch = curl_init() or die (curl_error()); curl_setopt($ch, CURLOPT_URL, $argv[1]); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); $response = curl_exec($ch); curl_close($ch); if ($count > 1000) { break; } //解析JSON字符串为数组 $res = json_decode($response, true); //如果客户端返回数据为0 则表示接收到数据了 if ($res[0] == '0') { break; } continue; } exit(1);
Client客户服务器 模拟一个客户端程序代码(我这里是测试回调的),以下代码用来接受PHP异步执行的脚本返回的参数,同时存储在Redis数据库中去,这里做了一个自增字段VideoId,用来记录脚本执行的次数(当然你也可以直接在命令行里面写一个sleep()函数用来做测试的)
/** * 默认的录像回调函数 */ public function videoCallbackFunctionAction() { $this->view->disable(); $streamName = $this->request->getQuery("streamName"); $videoId = $this->request->getQuery("videoId"); $endTime = $this->request->getQuery("endTime"); $fileName = $this->request->getQuery("fileName"); $baseName = $this->request->getQuery("baseName"); $fileSize = $this->request->getQuery("fileSize"); $duration = $this->request->getQuery("duration"); $redis = $this->Redis(); $redis->select(8); $incrId = $redis->incr('videoIncrId'); $redis->hMset('videoCallback:' . $incrId, [ 'streamName' => $streamName, 'fileName' => $fileName, 'baseName' => $baseName, 'fileSize' => $fileSize, 'time' => date("Y-m-d H:i:s") ]); }
=======================================第一次调试=========================================
echo '------------------------------------启动一个CLi进程 开始--------------------------------' . date("Y-m-d H:i:s"); exec("nohup /usr/bin/php /home/www/tinywan/cli_demo.php '{$callBackUrl}' >/dev/null 2>&1 &"); echo "<hr/>"; echo '-----------------------------------启动一个CLi进程 结束----------------------------------' . date("Y-m-d H:i:s");
1、测试数据之前先清空Redis数据库数据(命令:FlaushDB ,清空当前数据库的所有key键):
2、在浏览器刷新执行该脚本程序:
3、通过: ps -aux | grep php 查看PHP进程
4、查看Redis数据库信息:
5、通过以上可以看出。WEB页面并没有一直等待客户端服务器的相应,而是立马结束掉,而同时PHP脚本程序在后台运行,知道跳出循环结束
6、查看PHP后台基本执行时间为3分钟左右!
================第二次调试====================修改代码程序:再次调试
1、测试数据之前先清空Redis数据库数据(命令:FlaushDB ,清空当前数据库的所有key键):
2、在浏览器刷新执行该脚本程序:
3、通过: ps -aux | grep php 查看PHP进程
4、查看Redis数据库信息:
5、通过以上可以看出。WEB页面并没有一直等待客户端服务器的相应,而是立马结束掉,而同时PHP 脚本很快就执行完并退出,立马跳出循环结束(满足条件:$res[0] == 0,客户端服务器返回信息)
6、查看PHP后台基本执行时间为不到1分钟
===============第三次调试====================
说到这里可能有点怀疑怎么没看到PHP后台进程呢!好,下来我们sleep(10)函数暂停10秒时间,继续测试一下不就知道了,哈哈!
$count = 0; sleep(10); while (true) {
1、步骤同上,清楚Redis数据库数据
2、WEB页面执行结果
3、PHP后台异步脚本程序
4、Redis数据库记录数据
测试完毕,可以的,小伙子!棒棒哒!!!!!2017-02-28 16:20:48