前提:安装需要php7.3+,所以这里设置宝塔7.4为默认环境变量
ln -sf /www/server/php/74/bin/php /usr/bin/php
注意检查安装后的整体环境是否ok
curl -Ss https://www.workerman.net/check | php
一、安装
1、composer 安装 think-worker, 版本号可以在 packagist 中搜索,例如thinkphp框架为5.1.*,则可选最高版本为2.0.8,安装后会自动安装workerman依赖workerman/workerman (v3.5.34)。
composer require topthink/think-worker=2.0.8
2、composer 安装 gateway-worker
composer require workerman/gateway-worker=3.0.22
3、composer 安装 gatewayclient
composer require workerman/gatewayclient=3.0.0
二、gateway-worker使用案例
1、下载官方案例
GatewayWorker 示例下载地址:https://www.workerman.net/download/GatewayWorker.zip
2、参考官方案例在TP项目下实现,项目根目录下创建start.php文件
<?php
/**
* run with command
* php start.php start
*/
ini_set('display_errors', 'on');
use Workerman\Worker;
if(strpos(strtolower(PHP_OS), 'win') === 0)
{
exit("start.php not support windows, please use start_for_win.bat\n");
}
// 检查扩展
if(!extension_loaded('pcntl'))
{
exit("Please install pcntl extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
}
if(!extension_loaded('posix'))
{
exit("Please install posix extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
}
// 标记是全局启动
define('GLOBAL_START', 1);
// 注意,这里需要加载thinkphp的console文件,否则访问控制台操作会报相关错,官方独立部署的不需要
require_once __DIR__ . '/thinkphp/library/think/Console.php';
require_once __DIR__ . '/vendor/autoload.php';
// 加载所有Applications/*/start.php,以便启动所有服务,注意这里的Applications名称要根据实际应用名称填写
foreach(glob(__DIR__.'/application/*/start*.php') as $start_file)
{
require_once $start_file;
}
// 运行所有服务
Worker::runAll();
3、将官方实例中Applications/YourApp下的文件全部复制到application下的自定义文件夹中,如需修改gateway默认端口8282,可以在start_gateway.php中修改。
4、启动gateway。例如以php start.php start
方式启动gateway服务,相关操作命令如下。
# 以debug(调试)方式启动
php start.php start
# 以daemon(守护进程)方式启动
php start.php start -d
# 停止
php start.php stop
# 重启
php start.php restart
# 平滑重启
hp start.php reload
# 查看状态
php start.php status
debug和daemon方式区别
1、以debug方式启动,代码中echo、var_dump、print等打印函数会直接输出在终端。
2、以daemon方式启动,代码中echo、var_dump、print等打印会默认重定向到/dev/null文件,可以通过设置Worker::$stdoutFile = '/your/path/file';来设置这个文件路径。
3、以debug方式启动,终端关闭后workerman会随之关闭并退出。
4、以daemon方式启动,终端关闭后workerman继续后台正常运行。
5、多开启几个命令窗口使用telnet 127.0.0.1 9506
测试,输入任意字符查看效果
配置websocket连接
修改start_gateway.php文件
$gateway = new Gateway("websocket://0.0.0.0:9506");
本地新建一个html,浏览器访问,注意默认情况下,需要使用ws访问,如需使用wss访问,需按照下面的步骤配置
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
</head>
<body>
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
var ws = new WebSocket("ws://111.231.194.64:9506");
// 服务端主动推送消息时会触发这里的onmessage
ws.onmessage = function(e){
// json数据转换成js对象
var data = eval("("+e.data+")");
var type = data.type || '';
switch(type){
// Events.php中返回的init类型的消息,将client_id发给后台进行uid绑定
case 'init':
// 利用jquery发起ajax请求,将client_id发给后端进行uid绑定
$.post('https://www.mylucas.com.cn/bind.php', {client_id: data.client_id}, function(data){}, 'json');
break;
// 当mvc框架调用GatewayClient发消息时直接alert出来
default :
alert(e.data);
}
};
</script>
</body>
</html>
配置wss访问,创建start_gateway_ssl.php文件
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use \Workerman\Worker;
use \Workerman\WebServer;
use \GatewayWorker\Gateway;
use \GatewayWorker\BusinessWorker;
use \Workerman\Autoloader;
// 自动加载类
require_once __DIR__ . '/../../vendor/autoload.php';
// 证书最好是申请的证书
$context = array(
// 更多ssl选项请参考手册 https://php.net/manual/zh/context.ssl.php
'ssl' => array(
// 请使用绝对路径
'local_cert' => '/www/server/panel/vhost/cert/xxx/fullchain.pem', // 也可以是crt文件
'local_pk' => '/www/server/panel/vhost/cert/xxx/privkey.pem',
'verify_peer' => false,
// 'allow_self_signed' => true, //如果是自签名证书需要开启此选项
)
);
// 或者把上面的gateway 进程,改成websocket协议,可以用wss测试
$gateway = new Gateway("websocket://0.0.0.0:9505", $context);
// gateway名称,status方便查看
$gateway->name = 'LucasGatewaySSL';
// gateway进程数
$gateway->count = 4;
$gateway->transport = 'ssl';
// 本机ip,分布式部署时使用内网ip
$gateway->lanIp = '127.0.0.1';
// 内部通讯起始端口,假如$gateway->count=4,起始端口为4000
// 则一般会使用4000 4001 4002 4003 4个端口作为内部通讯端口
$gateway->startPort = 2800;
// 服务注册地址
$gateway->registerAddress = '127.0.0.1:1238';
// 心跳间隔
//$gateway->pingInterval = 10;
// 心跳数据
//$gateway->pingData = '{"type":"ping"}';
/*
// 当客户端连接上来时,设置连接的onWebSocketConnect,即在websocket握手时的回调
$gateway->onConnect = function($connection)
{
$connection->onWebSocketConnect = function($connection , $http_header)
{
// 可以在这里判断连接来源是否合法,不合法就关掉连接
// $_SERVER['HTTP_ORIGIN']标识来自哪个站点的页面发起的websocket链接
if($_SERVER['HTTP_ORIGIN'] != 'http://kedou.workerman.net')
{
$connection->close();
}
// onWebSocketConnect 里面$_GET $_SERVER是可用的
// var_dump($_GET, $_SERVER);
};
};
*/
// 如果不是在根目录启动,则运行runAll方法
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}
再次启动后会发现,出现下图
使用 composer 安装完成后,会生成config/worker_server.php文件。服务默认监听端口2345,注意开启服务器的2345端口。
二、gateway-worker与websocket实现简单聊天室
1、修改Event.php文件
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
/**
* 用于检测业务代码死循环或者长时间阻塞等问题
* 如果发现业务卡死,可以将下面declare打开(去掉//注释),并执行php start.php reload
* 然后观察一段时间workerman.log看是否有process_timeout异常
*/
//declare(ticks=1);
use \GatewayWorker\Lib\Gateway;
/**
* 主逻辑
* 主要是处理 onConnect onMessage onClose 三个方法
* onConnect 和 onClose 如果不需要可以不用实现并删除
*/
class Events
{
/**
* 当客户端连接时触发
* 如果业务不需此回调可以删除onConnect
*
* @param int $client_id 连接id
*/
// public static function onConnect($client_id)
// {
// // 向当前client_id发送数据
// Gateway::sendToClient($client_id, "Hello $client_id\r\n");
// // 向所有人发送
// Gateway::sendToAll("$client_id login\r\n");
// }
// 当有客户端连接时,将client_id返回,让mvc框架判断当前uid并执行绑定
public static function onConnect($client_id)
{
Gateway::sendToClient($client_id, json_encode(array(
'type' => 'init',
'client_id' => $client_id,
'content' => '进入了聊天室!'
)));
// \Log::info('Workerman connection' . $client_id);
}
/**
* 当客户端发来消息时触发
* @param int $client_id 连接id
* @param mixed $message 具体消息,或者json字符串
*/
public static function onMessage($client_id, $message)
{
$req_data = json_decode($message, true);
if(!is_array($req_data)){
// 默认,向所有人发送
Gateway::sendToAll(json_encode(array(
'client_id' => $client_id,
'type' => 'say_to_all',
'content' => $message
)));
}else{
// 如果是向所有客户端发送消息
if($req_data['type'] == 'say_to_all')
{
/*// 获取所有在线的客户端id
$list = Gateway::getAllClientIdList();
var_dump($list);*/
Gateway::sendToAll(json_encode(array(
'client_id' => $client_id,
'content' => $req_data['content']
)));
}elseif($req_data['type'] == 'say_to_single'){
Gateway::sendToClient($client_id, json_encode(array(
'client_id' => $client_id,
'type' => $req_data['type'],
'content' => $req_data['content']
)));
}
}
}
/**
* 当用户断开连接时触发
* @param int $client_id 连接id
*/
public static function onClose($client_id)
{
// 向所有人发送
Gateway::sendToAll(json_encode(array(
'client_id' => $client_id,
'type' => 'say_to_all',
'content' => '离开了聊天室!'
)));
}
}
2、在本地创建html
<!doctype html>
<html>
<head>
<title>Socket Chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="#">
<input id="m" autocomplete="off" /><button id="submit">Send</button>
</form>
</body>
</html>
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
// 注意约定好,前后台发送和接收都用json格式的字符串,以方便解析!!!
var ws = new WebSocket("ws://111.231.194.64:9506"); //WebSocket协议的url使用ws://开头,另外安全的WebSocket协议使用wss://开头
ws.onopen = function(){
//当WebSocket创建成功时,触发onopen事件
console.log("WebSocket连接成功!");
// ws.send("hello"); //将消息发送到服务端
}
// 服务端主动推送消息时会触发这里的onmessage
ws.onmessage = function(e){
// console.log("e.data:", e.data);
if(!isJSON(e.data)){
$('#messages').append(e.data + "<br>");
console.log('请注意,当前从服务器接收的数据不是规定的json格式!!!');
}else{
// json数据转换成js对象
var data = eval("("+e.data+")");
var type = data.type || '';
switch(type){
case 'init':
// 客户端与服务端初始连接的时候
// 利用jquery发起ajax请求,将client_id发给后端进行uid绑定
// $.post('https://xxx/bind.php', {client_id: data.client_id}, function(data){}, 'json');
// ws.send(message); //将消息发送到服务端
$('#messages').append(data.client_id + ',进入了聊天室' + "<br>");
break;
case 'say_to_all':
console.log('say_to_all:',say_to_all);
$('#messages').append(data.client_id + ':' + data.content + "<br>");
break;
default :
// 当mvc框架调用GatewayClient发消息时直接alert出来
// alert(e.data);
$('#messages').append(data.client_id + ':' + data.content + "<br>");
}
}
};
ws.onclose = function(e){
//当客户端收到服务端发送的关闭连接请求时,触发onclose事件
console.log("服务器断开连接!");
$('#messages').append('服务器断开连接!' + "<br>");
}
ws.onerror = function(e){
//如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
console.log(error);
}
$(document).on('click', '#submit', function(){
var message = $("#m").val();
if(message){
//将消息发送到服务端,指定say_to_all类型,服务器端会发送给所有在线客户端
ws.send(JSON.stringify({ type: 'say_to_all', content: message }));
$("#m").val('');
}
})
function isJSON(str) {
if (typeof str == 'string') {
try {
var obj=JSON.parse(str);
if(typeof obj == 'object' && obj ){
return true;
}else{
return false;
}
} catch(e) {
console.log('error:'+str+'!!!'+e);
return false;
}
}
console.log('It is not a string!')
}
</script>
3、把html用多个窗口打开,即可实现,效果如下
二、启动
1、使用 Workerman 作为HttpServer
# 启动
php think worker [start|stop|reload|restart|status]
# 使用telnet访问测试
telnet 11x.xxx.xxx.64 2345
2、除了上面的启动方式,还可以使用SocketServe启动server
php think worker:server start [start|stop|reload|restart|status]
# 使用telnet访问测试
telnet 11x.xxx.xxx.64 2345
三、使用
确保在命令行中使用telnet命令,可以成功启动2345、2346服务后,可以开始下面的使用!
1、编辑 config/worker.php文件,如下
return [
// 扩展自身需要的配置
'host' => '0.0.0.0', // 监听地址
'port' => 2346, // 监听端口
'root' => '', // WEB 根目录 默认会定位public目录
'app_path' => '/www/wwwroot/xxx/xxx', // 应用目录 守护进程模式必须设置(绝对路径)
'file_monitor' => true, // 是否开启PHP文件更改监控(调试模式下自动开启)
'file_monitor_interval' => 2, // 文件监控检测时间间隔(秒)
'file_monitor_path' => [], // 文件监控目录 默认监控application和config目录
// 支持workerman的所有配置参数
'name' => 'thinkphp',
'count' => 4,
'daemonize' => false,
'pidFile' => Env::get('runtime_path') . 'worker.pid',
'onConnect' => function($connection)
{
echo "new connection from ip " . $connection->getRemoteIp() . "\n";
},
// onWorkerReload
'onWorkerReload' => function ($worker) {
echo " message:worker is already reload" . "\n";
},
// onConnect
'onConnect' => function ($connection) {
echo " message:new connection from ip " . $connection->getRemoteIp() . "\n";
},
// onMessage
'onMessage' => function ($connection, $data) {
$connection->send(' message:receive success');
},
// onClose
'onClose' => function ($connection) {
echo " message:worker is already close" . "\n";
},
// onError
'onError' => function ($connection, $code, $msg) {
echo " message:error [ $code ] $msg\n";
},
];
2、在浏览器中访问2346端口,http://xxx.xxx.xxx.xx:2346/
,实际访问的是index/index/index方法,在方法中返回一个字符串,可以在浏览器中被打印出来。此时,访问2346与访问网站默认端口都是可以正常访问application中的方法的。
3、新增一个public/workman.html文件测试,socket连接是否正常,代码如下
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body></body>
</html>
<script type="text/javascript">
var lockReconnect = false; //避免ws重复连接
var ws = null; // 判断当前浏览器是否支持WebSocket
var wsUrl = "ws://xxx.xxx.xx.xx:2346"; // 默认访问index/index/index方法,也可以填写其他参数,注意http请求,使用ws,https请求,需要用wws
createWebSocket(wsUrl); //连接ws
function createWebSocket(url) {
try{
if('WebSocket' in window){
ws = new WebSocket(url);
}
initEventHandle();
}catch(e){
reconnect(url);
console.log(e);
}
}
function initEventHandle() {
ws.onclose = function () {
reconnect(wsUrl);
console.log("llws连接关闭!"+new Date().toLocaleString());
};
ws.onerror = function () {
reconnect(wsUrl);
console.log("llws连接错误!");
};
ws.onopen = function () {
heartCheck.reset().start(); // 心跳检测重置
console.log("llws连接成功!"+new Date().toLocaleString());
};
ws.onmessage = function (event) { // 如果获取到消息,心跳检测重置
heartCheck.reset().start(); // 拿到任何消息都说明当前连接是正常的
console.log("llws收到消息啦:" +event.data);
if(event.data!='pong'){
// let data = jsON.parse(event.data);
let data = event.data;
}
};
}
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
ws.close();
}
function reconnect(url) {
if(lockReconnect) return;
lockReconnect = true;
setTimeout(function () { // 没连接上会一直重连,设置延迟避免请求过多
createWebSocket(url);
lockReconnect = false;
}, 2000);
}
//心跳检测
var heartCheck = {
timeout: 2000, // 2秒钟发一次心跳
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
// 这里发送一个心跳,后端收到后,返回一个心跳消息,
// onmessage拿到返回的心跳就说明连接正常
ws.send("ping");
console.log("ping!")
self.serverTimeoutObj = setTimeout(function(){// 如果超过一定时间还没重置,说明后端主动断开了
ws.close(); // 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
}
}
</script>
访问网址http://xxx.xxx.xx.xxx:2346/workman.html
,控制台可以正常返回http://xxx.xxx.xx.xxx:2346
返回的数据,即可。