PHP实现Websocket异步客户端 (workerman方式)
1. 通过composer load workman
composer安装
php -r "readfile('https://getcomposer.org/installer');" | php
mv composer.phar /usr/local/bin/composer
mkdir /home/wsclient
cd /home/wsclient
composer require workerman/workerman
若出错,一般是 Composer 未配置正确的源,可以尝试切换Composer到官方源
composer config --global --unset repos.packagist
composer install
2. 创建workerman执行文件 /home/wsclient/wsclient.php
本例使用NowAPI提供的消息推送服务 Websocket消息推送服务
<?php
date_default_timezone_set('PRC');
require_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use Workerman\Timer;
use Workerman\Connection\AsyncTcpConnection;
//配置
$setParmA['host'] = 'nms-aaa.nowapi.com';
$setParmA['port'] = '443';
$setParmA['ssl'] = '1';//是否ssl加密 0:否 1:是
$setParmA['instanceId'] = 'ins-10003';
$setParmA['accessKey'] = '8512547106565e12544125007942565e';//测试key
$setParmA['secretKey'] = '';
$setParmA['userId'] = 'php-'.rand(10000,99999);//指定客户端唯一ID,这里随机一个
$setParmA['channels'] = 'myChannel';//订阅通道(多个用逗号隔开)
$setParmA['onOpen'] = function(){};//连接成功触发
$setParmA['onMessage'] = function($msgData){};//收到消息触发
$setParmA['onClose'] = function(){};//与服务器断开
$setParmA['onFailed'] = function($msgFailed){};//异常消息触发
$setParmA['onTaskTimer'] = function($worker,$con){};//定时器
//接收到消息时触发
$setParmA['onMessage'] = function($msgData){
$msgId = $msgData['i'];
$msgTime = $msgData['t'];
$msgChannel = $msgData['n'];
$msgContent = $msgData['c'];
echo " msgId : ".$msgId."\r\n";
echo " msgTime : ".date('Y-m-d H:i:s',substr($msgTime,0,-3))." ".substr($msgTime,0,-3)."\r\n";
echo " msgChannel : ".$msgChannel."\r\n";
echo " msgContent : ".$msgContent."\r\n";
echo "base64decode : ".base64_decode($msgContent)."\r\n";
echo "----\r\n";
};
//定时器触发 (每10秒执行一次,推送源)
$setParmA['onTaskTimer'] = function($worker,$con){
$phpOS = strtolower(PHP_OS);
//数据采集提取
$rspData = '';
if($phpOS=='linux'){
$rspData = shell_exec('ping 119.29.29.29 -c 1');
}elseif($phpOS=='winnt'){
$rspData = shell_exec('ping 119.29.29.29 -n 1');
}
//编码后传递
$rspData = base64_encode($rspData);
//
$pushChannel = 'myChannel';//向该通道推送
$con->send('["publish",{"channels":"'.$pushChannel.'","content":"'.$rspData.'"}]');
};
//异常时执行
$setParmA['onFailed'] = function($msgFailed){
echo date('Y-m-d H:i:s')." ERR: ".$msgFailed."\r\n";
};
//与服务端连接断开执行
$setParmA['onClose'] = function(){
echo date('Y-m-d H:i:s')." Connection closed and try to reconnect\r\n";
};
// 创建一个worker实例
$worker = new Worker();
$worker->onWorkerStart = function($worker) use($setParmA){
//配置
$host = $setParmA['host'];
$port = $setParmA['port'];
$ssl = $setParmA['ssl'];//是否ssl加密 0:否 1:是
$instanceId = $setParmA['instanceId'];
$accessKey = $setParmA['accessKey'];
$secretKey = $setParmA['secretKey'];
$userId = $setParmA['userId'];//指定客户端唯一ID
$channels = $setParmA['channels'];//订阅通道(多个用逗号隔开)
//运行参数
$worker->heartbeatPingInterval = 5000;//心跳包间隔
$worker->heartbeatTimeId = 0;//心跳定时器ID
$worker->taskTimeId = 0;//任务定时器ID
//连接
$con = new AsyncTcpConnection('ws://'.$host.':'.$port.'/?instanceId='.$instanceId);
// 设置以ssl加密方式访问,使之成为wss
if($ssl==1){
$con->transport = 'ssl';
}
//连接
$con->onWebSocketConnect = function(AsyncTcpConnection $con) use($accessKey,$secretKey,$userId) {
$safeCode = '';
if(!empty($secretKey)){
$safeCode = openssl_encrypt(round(microtime(true)*1000).'___','AES-128-ECB',$secretKey,2,'');
}
$con->send('["authorize",{"accessKey":"'.$accessKey.'","userId":"'.$userId.'","safeCode":"'.$safeCode.'"}]');
};
//收到消息
$con->onMessage = function(AsyncTcpConnection $con,$onData) use($worker,$setParmA){
$channels = $setParmA['channels'];
$onMessage = $setParmA['onMessage'];
try{
//心跳
if(trim($onData)==='2'){
throw new Exception('INF_HEARTBEAT',1);
}
//数据
$onDataA = json_decode($onData,true);
if(!is_array($onDataA)){
throw new Exception('ERR_DATA_FORMAT',-1);
}
$rcvType = $onDataA[0];
$rcvData = $onDataA[1];
//通用响应
if(in_array($rcvType,array('subscribe','unsubscribe','publish'))){
if($rcvData['resultCode']!='200'){
throw new Exception($onData,-1);
}else{
throw new Exception($onData,1);
}
}elseif($rcvType=='message'){
//消息确认
if(!empty($rcvData['i'])){
$con->send('["messageack",{"i":"'.$rcvData['i'].'"}]');
}else{
throw new Exception($onData,-1);
}
}
//逻辑响应
if($rcvType=='authorize'){
if($rcvData['resultCode']=='200' && is_numeric($rcvData['resultContent']['pingInterval'])){
//启动定时器推送心跳包
$worker->heartbeatPingInterval = $rcvData['resultContent']['pingInterval'];
$worker->heartbeatTimeId = Timer::add($worker->heartbeatPingInterval/1000,function() use ($worker,$con){
//var_dump('ping '.date('Y-m-d H:i:s').' '.$worker->heartbeatPingInterval.' / '.$worker->heartbeatTimeId);
$con->send('1');
});
//清空订阅
$con->send('["unsubscribe",{"channels":[]}]');
//新订阅
$con->send('["subscribe",{"channels":"'.$channels.'"}]');
}
throw new Exception('INF_AUTHORIZE ['.$worker->heartbeatPingInterval.'/'.$worker->heartbeatTimeId.']',1);
}
//收到消息
elseif($rcvType=='message'){
if(is_object($onMessage)){
$onMessage($rcvData);
}
}
}catch(Exception $ex){
$errCode = $ex->getCode();
if($errCode!==1){
if(is_object($setParmA['onFailed'])){
$setParmA['onFailed']($ex->getMessage());
}
}
}
};
//
$con->onError = function(AsyncTcpConnection $con,$code,$msg) use($setParmA){
if(is_object($setParmA['onFailed'])){
$setParmA['onFailed']($msg);
}
};
//
$con->onClose = function($con) use($worker,$setParmA){
// 清理timer
Timer::del($worker->heartbeatTimeId);
Timer::del($worker->taskTimeId);
//
if(is_object($setParmA['onClose'])){
return $setParmA['onClose']();
}
// 如果连接断开,1秒后重连
$con->reConnect(5);
};
//task
$worker->taskTimeId = Timer::add(10,function() use ($worker,$con,$setParmA){
if(is_object($setParmA['onTaskTimer'])){
return $setParmA['onTaskTimer']($worker,$con);
}
});
$con->connect();
};
Worker::runAll();
3. 运行
本例需要php_openssl支持,注意在php.ini中添加php_openssl ext
/usr/bin/php /home/wsclient/wsclient.php start
如果不想配置PHP运行环境,也可以直接用swoole编译好的二进制版
/usr/bin/swoole-cli /home/wsclient/wsclient.php start