源碼地址: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:\wamp64\www\cli 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)."\r\n"; 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:\wamp64\bin\php\php7.0.10\php.exe E:\wamp64\www\ThinkPhpStudy\cli.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:\wamp64\bin\php\php7.0.10\php.exe E:\wamp64\www\ThinkPhpStudy\cli.php /Library/index/cmdCliTest",$output, $return_val);
可以更改如下:
exec("E:\wamp64\bin\php\php7.0.10\php.exe E:\wamp64\www\ThinkPhpStudy\cli.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:\wamp64\bin\php\php7.0.10\php.exe E:\wamp64\www\ThinkPhpStudy\cli.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 '開始發送郵件:' . "\r\n"; 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