前提:安装需要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返回的数据,即可。

最后修改:2024 年 11 月 23 日
如果觉得我的文章对你有用,请随意赞赏