前言
我們使用php開(kāi)發(fā)web應(yīng)用基本都是使用傳統(tǒng)的lamp/lnmp模式來(lái)提供http服務(wù),這種模式一般是同步且堵塞的,若我們想使用php開(kāi)發(fā)一些高級(jí)的特性(例如:異步,非堵塞,網(wǎng)絡(luò)服務(wù)器等),那么swoole無(wú)疑是最佳的選擇,那什么是swoole呢?
PHP的異步、并行、高性能網(wǎng)絡(luò)通信引擎,使用純c語(yǔ)言編寫(xiě),提供了 PHP語(yǔ)言的異步多線程服務(wù)器, 異步TCP/udp網(wǎng)絡(luò)客戶端, 異步mysql, 異步redis, 數(shù)據(jù)庫(kù)連接池, AsyncTask, 消息隊(duì)列, 毫秒定時(shí)器, 異步文件讀寫(xiě), 異步DNS查詢。 Swoole內(nèi)置了 Http/websocket服務(wù)器端/ 客戶端、 Http2.0服務(wù)器端/ 客戶端。
簡(jiǎn)單的來(lái)說(shuō),Swoole是一個(gè)PHP擴(kuò)展,實(shí)現(xiàn)了網(wǎng)絡(luò)層的很多功能,應(yīng)用場(chǎng)景非常廣,下面列舉幾個(gè)例子簡(jiǎn)單介紹一下Swoole的應(yīng)用。
推薦(免費(fèi)):swoole
安裝
按照官方文檔進(jìn)行安裝:Swoole官網(wǎng),安裝完后使用命令:
php?-m
查看是否安裝成功。注意:Swoole從2.0版本開(kāi)始支持了內(nèi)置協(xié)程,需使用PHP7。
基于TCP的郵件服務(wù)器
使用Swoole提供TCP服務(wù),異步任務(wù)發(fā)送郵件。
郵件功能:
PHPMailer
PHP主代碼:
<?php $object = new MailServer(); $setting = [ 'log_file' =>?'swoole.log', ????'worker_num'?=>?4,?//?4個(gè)工作進(jìn)程 ????'task_worker_num'?=>?10,?//?10個(gè)任務(wù)進(jìn)程 ]; $server?=?new?swoole_server("127.0.0.1",?9501); $server->set($setting); $server->on('WorkerStart',?array($object,?'onWorkerStart')); $server->on('Connect',?array($object,?'onConnect')); $server->on('Receive',?array($object,?'onReceive')); $server->on('Close',?array($object,?'onClose')); $server->on('Task',?array($object,?'onTask')); $server->on('Finish',?array($object,?'onFinish')); $server->start(); class?MailServer { ????/**?@var?Mail?*/ ????private?$handle; ????public?function?__construct() ????{ ????????require?'Mail.php';?//?PHPMailer郵件服務(wù)類(lèi) ????} ????public?function?onWorkerStart($server,?$workerId) ????{ ????????$mailConfig?=?require?'MailConfig.php';?//?發(fā)件人信息,重啟時(shí)會(huì)重新加載配置文件 ????????$this->handle?=?new?Mail($mailConfig); ????} ????public?function?onConnect($server,?$fd,?$reactorId) ????{ ????} ????public?function?onReceive($server,?$fd,?$reactorId,?$data) ????{ ????????$return?=?[]; ????????$dataArr?=?JSon_decode($data,?true); ????????if?(empty($dataArr)?||?empty($dataArr['address'])?||?empty($dataArr['subject'])?||?empty($dataArr['body']))?{ ????????????$return['code']?=?-1; ????????????$return['msg']?=?'參數(shù)不能為空'; ????????}?else?{?//?參數(shù)校驗(yàn)成功 ????????????$server->task($data);?//?投遞一個(gè)任務(wù) ????????????$return['code']?=?0; ????????????$return['msg']?=?'投遞任務(wù)成功'; ????????} ????????$server->send($fd,?json_encode($return)); ????} ????public?function?onTask($server,?$taskId,?$workerId,?$data) ????{ ????????$data?=?json_decode($data,?true); ????????$this->handle->send($data['address'],?$data['subject'],?$data['body']);?//?發(fā)送郵件 ????} ????public?function?onFinish($server,?$task_id,?$data) ????{ ????} ????public?function?onClose($server,?$fd,?$reactorId) ????{ ????} }
發(fā)件人信息配置:
<?php // 郵件發(fā)送人信息配置 return [ 'host' =>?'smtp.qq.com', ????'port'?=>?'465', ????'fromName'?=>?'Mr.litt', ????'username'?=>?'137057181@qq.com', ????'password'?=>?'', ];
PHPMailer郵件服務(wù)類(lèi):
<?php require 'vendor/phpmailer/phpmailer/src/Exception.php'; require 'vendor/phpmailer/phpmailer/src/PHPMailer.php'; require 'vendor/phpmailer/phpmailer/src/SMTP.php'; use PHPMailerPHPMailerPHPMailer; class Mail { private $host; private $port; private $fromName; private $username; private $password; public function __construct($config) { !empty($config['host']) && $this->host?=?$config['host']; ????????!empty($config['port'])?&&?$this->port?=?$config['port']; ????????!empty($config['fromName'])?&&?$this->fromName?=?$config['fromName']; ????????!empty($config['username'])?&&?$this->username?=?$config['username']; ????????!empty($config['password'])?&&?$this->password?=?$config['password']; ????????if?(empty($this->host)?||?empty($this->port)?||?empty($this->fromName)?|| ????????????empty($this->username)?||?empty($this->password))?{ ????????????throw?new?Exception('發(fā)件人信息錯(cuò)誤'); ????????} ????} ????public?function?send($address,?$subject,?$body) ????{ ????????if?(empty($address)?||?empty($subject)?||?empty($body))?{ ????????????throw?new?Exception('收件人信息錯(cuò)誤'); ????????} ????????//?實(shí)例化PHPMailer核心類(lèi) ????????$mail?=?new?PHPMailer(); ????????//?是否啟用smtp的debug進(jìn)行調(diào)試?開(kāi)發(fā)環(huán)境建議開(kāi)啟?生產(chǎn)環(huán)境注釋掉即可?默認(rèn)關(guān)閉debug調(diào)試模式 ????????$mail->SMTPDebug?=?0; ????????//?使用smtp鑒權(quán)方式發(fā)送郵件 ????????$mail->isSMTP(); ????????//?smtp需要鑒權(quán)?這個(gè)必須是true ????????$mail->SMTPAuth?=?true; ????????//?鏈接郵箱的服務(wù)器地址 ????????$mail->Host?=?$this->host; ????????//?設(shè)置使用ssl加密方式登錄鑒權(quán) ????????$mail->SMTPSecure?=?'ssl'; ????????//?設(shè)置ssl連接smtp服務(wù)器的遠(yuǎn)程服務(wù)器端口號(hào) ????????$mail->Port?=?$this->port; ????????//?設(shè)置發(fā)送的郵件的編碼 ????????$mail->CharSet?=?'UTF-8'; ????????//?設(shè)置發(fā)件人昵稱(chēng)?顯示在收件人郵件的發(fā)件人郵箱地址前的發(fā)件人姓名 ????????$mail->FromName?=?$this->fromName; ????????//?smtp登錄的賬號(hào)?QQ郵箱即可 ????????$mail->Username?=?$this->username; ????????//?smtp登錄的密碼?使用生成的授權(quán)碼 ????????$mail->Password?=?$this->password; ????????//?設(shè)置發(fā)件人郵箱地址?同登錄賬號(hào) ????????$mail->From?=?$this->username; ????????//?郵件正文是否為html編碼?注意此處是一個(gè)方法 ????????$mail->isHTML(true); ????????//?設(shè)置收件人郵箱地址 ????????$mail->addAddress($address); ????????//?添加多個(gè)收件人?則多次調(diào)用方法即可 ????????//$mail->addAddress('87654321@163.com'); ????????//?添加該郵件的主題 ????????$mail->Subject?=?$subject; ????????//?添加郵件正文 ????????$mail->Body?=?$body; ????????//?為該郵件添加附件 ????????//$mail->addAttachment('./example.pdf'); ????????//?發(fā)送郵件?返回狀態(tài) ????????$status?=?$mail->send(); ????????return?$status; ????} }
注意事項(xiàng):
- 修改發(fā)件人信息后,只需重啟task_worker就生效,命令 kill -USER1 主進(jìn)程PID。
- TCP客戶端可使用swoole_client類(lèi)來(lái)模擬。
- 短信、推送等異步任務(wù)同樣適用于此場(chǎng)景。
?
基于WebSocket多房間聊天功能
使用Swoole提供WebSocket服務(wù),使用Redis保存房間人員信息。
PHP主代碼:
<?php $object = new ChatServer(); $setting = [ 'log_file' =>?'swoole_ws.log', ????'worker_num'?=>?4,?//?4個(gè)工作進(jìn)程 ]; $ws?=?new?swoole_websocket_server("127.0.0.1",?9502); $ws->set($setting); $ws->on('WorkerStart',?array($object,?'onWorkerStart')); $ws->on('open',?array($object,?'onOpen')); $ws->on('message',?array($object,?'onMessage')); $ws->on('close',?array($object,?'onClose')); $ws->start(); class?ChatServer { ????/**?@var??Redis?*/ ????private?$redis; ????public?function?__construct() ????{ ????????echo?"啟動(dòng)前清理數(shù)據(jù) "; ????????$redis?=?new?Redis(); ????????$redis->connect('127.0.0.1',?6379); ????????if?($redis->ping()?!=?'+PONG')?{ ????????????echo?"redis連接失敗 ";exit; ????????} ????????$delKeys?=?$redis->keys('fd_*'); ????????foreach?($delKeys?as?$key)?{ ????????????$redis->del($key); ????????} ????????$delKeys?=?$redis->keys('roomId_*'); ????????foreach?($delKeys?as?$key)?{ ????????????$redis->del($key); ????????} ????} ????public?function?onWorkerStart($ws,?$workerId) ????{ ????????$redis?=?new?Redis(); ????????$redis->connect('127.0.0.1',?6379); ????????if?($redis->ping()?!=?'+PONG')?{ ????????????echo?"redis連接失敗 "; ????????} ????????$this->redis?=?$redis; ????} ????public?function?onOpen($ws,?$request) ????{ ????????echo?"fd:{$request->fd}?is?open "; ????????if?(empty($request->get['roomId'])?||?empty($request->get['nick']))?{ ????????????$status?=?'fail'; ????????}?else?{ ????????????//建立身份關(guān)聯(lián) ????????????$this->redis->hSet("fd_".$request->fd,?'roomId',?$request->get['roomId']); ????????????$this->redis->hSet("fd_".$request->fd,?'nick',?$request->get['nick']); ????????????$this->redis->sAdd("roomId_".$request->get['roomId'],?$request->fd); ????????????$status?=?'success'; ????????} ????????$sendData?=?[ ????????????'cmd'?=>?'open', ????????????'data'?=>?[ ????????????????'status'?=>?$status ????????????] ????????]; ????????$ws->push($request->fd,?json_encode($sendData)); ????} ????public?function?onMessage($ws,?$frame) ????{ ????????echo?"fd:[$frame->fd},?Message:?{$frame->data} "; ????????if?(!empty($frame->data))?{ ????????????$fdInfo?=?$this->redis->hGetAll("fd_".$frame->fd); ????????????if?(!empty($fdInfo['nick'])?&&?!empty($fdInfo['roomId']))?{ ????????????????$sendData?=?[ ????????????????????'cmd'?=>?'ReceiveMessage', ????????????????????'data'?=>?[ ????????????????????????'nick'?=>?$fdInfo['nick'], ????????????????????????'msg'?=>?$frame->data, ????????????????????] ????????????????]; ????????????????$fdArr?=?$this->redis->sMembers("roomId_".$fdInfo['roomId']); ????????????????foreach?($fdArr?as?$fd)?{ ????????????????????$ws->push($fd,?json_encode($sendData)); ????????????????} ????????????} ????????} ????} ????public?function?onClose($ws,?$fd,?$reactorId) ????{ ????????echo?"fd:{$fd}?is?closed "; ????????//刪除fd身份數(shù)據(jù)并在房間內(nèi)移動(dòng)該fd ????????$fdInfo?=?$this->redis->hGetAll("fd_".$fd); ????????if?(!empty($fdInfo['roomId']))?{ ????????????$this->redis->sRem("roomId_".$fdInfo['roomId'],?$fd); ????????} ????????$this->redis->del("fd_".$fd); ????} }
注意事項(xiàng):
1.Worker進(jìn)程之間不能共享變量,這里使用Redis來(lái)共享數(shù)據(jù)。
2.Worker進(jìn)程不能共用同一個(gè)Redis客戶端,需要放到onWorkerStart中實(shí)例化。
3.客戶端可使用JS內(nèi)置等WebSokcet客戶端,異步的PHP程序可使用SwooleHttpClient,同步可以使用swoole/framework提供的同步WebSocket客戶端。
?
基于HTTP的簡(jiǎn)易框架
使用Swoole提供HTTP服務(wù),模擬官方Swoole框架實(shí)現(xiàn)一個(gè)簡(jiǎn)易框架。
PHP主代碼:
<?php $object = new AppServer(); $setting = [ 'log_file' =>?'swoole_http.log', ????'worker_num'?=>?4,?//?4個(gè)工作進(jìn)程 ]; $server?=?new?swoole_http_server("127.0.0.1",?9503); $server->set($setting); $server->on('request',?array($object,?'onRequest')); $server->on('close',?array($object,?'onClose')); $server->start(); /** ?*?Class?AppServer ?*?@property?swoole_http_request?$request ?*?@property?swoole_http_response?$response ?*?@property?PDO?$db ?*?@property?libSession?$session ?*/ class?AppServer { ????private?$module?=?[]; ????/**?@var?AppServer?*/ ????private?static?$instance; ????public?static?function?getInstance() ????{ ????????return?self::$instance; ????} ????public?function?__construct() ????{ ????????$baseControllerFile?=?__DIR__?.'/controller/Base.php'; ????????require_once?"$baseControllerFile"; ????} ????/** ?????*?@param?swoole_http_request?$request ?????*?@param?swoole_http_response?$response ?????*/ ????public?function?onRequest($request,?$response) ????{ ????????$this->module['request']?=?$request; ????????$this->module['response']?=?$response; ????????self::$instance?=?$this; ????????list($controllerName,?$methodName)?=?$this->route($request); ????????empty($controllerName)?&&?$controllerName?=?'index'; ????????empty($methodName)?&&?$methodName?=?'index'; ????????try?{ ????????????$controllerClass?=?"controller"?.?ucfirst($controllerName); ????????????$controllerFile?=?__DIR__?.?"/controller/"?.?ucfirst($controllerName)?.?".php"; ????????????if?(!class_exists($controllerClass,?false))?{ ????????????????if?(!is_file($controllerFile))?{ ????????????????????throw?new?Exception('控制器不存在'); ????????????????} ????????????????require_once?"$controllerFile"; ????????????} ????????????$controller?=?new?$controllerClass($this); ????????????if?(!method_exists($controller,?$methodName))?{ ????????????????throw?new?Exception('控制器方法不存在'); ????????????} ????????????ob_start(); ????????????$return?=?$controller->$methodName(); ????????????$return?.=?ob_get_contents(); ????????????ob_end_clean(); ????????????$this->session->end(); ????????????$response->end($return); ????????}?catch?(Exception?$e)?{ ????????????$response->status(500); ????????????$response->end($e->getMessage()); ????????} ????} ????private?function?route($request) ????{ ????????$pathInfo?=?explode('/',?$request->server['path_info']); ????????return?[$pathInfo[1],?$pathInfo[2]]; ????} ????public?function?onClose($server,?$fd,?$reactorId) ????{ ????} ????public?function?__get($name) ????{ ????????if?(!in_array($name,?array('request',?'response',?'db',?'session')))?{ ????????????return?null; ????????} ????????if?(empty($this->module[$name]))?{ ????????????$moduleClass?=?"lib"?.?ucfirst($name); ????????????$moduleFile?=?__DIR__?.?'/lib/'?.?ucfirst($name)?.?".php"; ????????????if?(is_file($moduleFile))?{ ????????????????require_once?"$moduleFile"; ????????????????$object?=?new?$moduleClass; ????????????????$this->module[$name]?=?$object; ????????????} ????????} ????????return?$this->module[$name]; ????} }
使用header和setCooike示例:
<?php namespace controller; class Http extends Base { public function header() { //發(fā)送Http狀態(tài)碼,如500, 404等等 $this->response->status(302); ????????//使用此函數(shù)代替PHP的header函數(shù) ????????$this->response->header('Location',?'http://www.baidu.com/'); ????} ????public?function?cookie() ????{ ????????$this->response->cookie('http_cookie','http_cookie_value'); ????} }
Session實(shí)現(xiàn):
<?php namespace lib; class Session { private $sessionId; private $cookieKey; private $storeDir; private $file; private $isStart; public function __construct() { $this->cookieKey?=?'PHPSESSID'; ????????$this->storeDir?=?'tmp/'; ????????$this->isStart?=?false; ????} ????public?function?start() ????{ ????????$this->isStart?=?true; ????????$appServer?=?AppServer::getInstance(); ????????$request?=?$appServer->request; ????????$response?=?$appServer->response; ????????$sessionId?=?$request->cookie[$this->cookieKey]; ????????if?(empty($sessionId)){ ????????????$sessionId?=?uniqid(); ????????????$response->cookie($this->cookieKey,?$sessionId); ????????} ????????$this->sessionId?=?$sessionId; ????????$storeFile?=?$this->storeDir?.?$sessionId; ????????if?(!is_file($storeFile))?{ ????????????touch($storeFile); ????????} ????????$session?=?$this->get($storeFile); ????????$_SESSION?=?$session; ????} ????public?function?end() ????{ ????????$this->save(); ????} ????public?function?commit() ????{ ????????$this->save(); ????} ????private?function?save() ????{ ????????if?($this->isStart)?{ ????????????$data?=?json_encode($_SESSION); ????????????ftruncate($this->file,?0); ????????????if?($data)?{ ????????????????rewind($this->file); ????????????????fwrite($this->file,?$data); ????????????} ????????????flock($this->file,?LOCK_UN); ????????????fclose($this->file); ????????} ????} ????private?function?get($fileName) ????{ ????????$this->file?=?fopen($fileName,?'c+b'); ????????if(flock($this->file,?LOCK_EX?|?LOCK_NB))?{ ????????????$data?=?[]; ????????????clearstatcache(); ????????????if?(filesize($fileName)?>?0)?{ ????????????????$data?=?fread($this->file,?filesize($fileName)); ????????????????$data?=?json_decode($data,?true); ????????????} ????????????return?$data; ????????} ????} }
注意事項(xiàng):
- 使用Redis/MySQL等客戶端理應(yīng)使用線程池,參照官方Swoole框架。
- Swoole是在執(zhí)行PHP文件這一階段進(jìn)行接管,因此header,setCooike,seesion_start不可用,header和setCooike可用$response變量實(shí)現(xiàn),Session可自行實(shí)現(xiàn)。
- 推薦查看官方框架:Swoole框架。
總結(jié)
Swoole的應(yīng)用遠(yuǎn)不如此,Swoole就像打開(kāi)了PHP世界的新大門(mén),我覺(jué)得每一位PHPer都應(yīng)該學(xué)習(xí)和掌握Swoole的用法,能學(xué)到很多使用LAMP/LNMP模式時(shí)未涉及到的知識(shí)點(diǎn)。