概述

本文主要介绍Alpaca-Laravel框架集成GateWayWorker实现WebSocket功能,并且以一个简单的聊天室作为示例。Alpaca-Laravel框架是使用Alpaca-spa与Laravel前开端分离开发的一款快速开发框架,集成了用户管理,权限控制等功能,详情请阅读《Alpaca-Laravel 框架(一) --- 概述,前后分离的后台管理系统》

Alpaca-Laravel 框架(二) --- 集成GateWay实现WebSocket功能-聊天功能示例-LMLPHP

项目相关代码以及文档地址

主页 Alpaca-Spa http://www.tkc8.com
后台 Alpaca-Spa-Laravel http://full.tkc8.com
手机端sui Alpaca-Spa-Sui http://full.tkc8.com/app
代码 oschina http://git.oschina.net/cc-sponge/Alpaca-Spa-Laravel
代码 github https://github.com/big-sponge/Alpaca-Spa-Laravel

注:后台管理端登录账号是一个测试帐号,权限只有浏览功能,没有编辑等修改功能。

安装GateWayWorker

GatewayWorker基于Workerman开发的一个项目框架,用于快速开发TCP长连接应用,例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等

这里主要到三个插件: Workerman,GateWayWorkerW, GateWayClient

注:以下示例中通过composer安装的Workerman,GateWayWorkerW, GateWayClient全部为linux版本,如果读者想安装windows版本,请把名字改为对应windows版本的名字。

安装前请确认你的环境是否支持GateWayWorker,例如使用以下命令:

curl -Ss http://www.workerman.net/check.php | php

详细说明,请阅读GateWayWorker的官方文档。

安装Workerman
cd your_path/laravel_program
composer require workerman/workerman
安装GateWayWorker
composer require workerman/gateway-worker
安装GatewayClient
composer require workerman/gatewayclient

artisan command实现

因为GateWayWorker服务启动是基于cli命令行模式,所以我们用laravel的artisan实现GateWayWorker的命令,这样做的好处是,你的websocket项目与web项目环境统一,无缝对接,使用统一的类加载规则,复用代码。

创建command
php artisan make:command WsServer

这样Laravel会在 App\Console\Commands 目录下面生成一个WsServer.php文件

如果你修改了Laravel默认的目录结构,请将他复制到相应的Commands目录

稍后再修改这个文件的内容,现在先注册command

注册command

App\Console\Kernel.php文件添加刚才创建的command

protected $commands = [
 Commands\WsServer::class
];
WsServer.php的内容
<?php

namespace Console\Commands;

use App\Modules\WsServer\Router;
use GatewayWorker\BusinessWorker;
use GatewayWorker\Gateway;
use GatewayWorker\Register;
use Illuminate\Console\Command;
use Workerman\Worker;
use GatewayWorker\Lib\Gateway as WsSender;

class WsServer extends Command
{
    protected $webSocket;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ws {action} {--d}';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'workerman server';

    /**
     * Create a new command instance.
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // 检查OS
        if (strpos(strtolower(PHP_OS), 'win') === 0) {
            $this->error("Sorry, not support for windows.\n");
            exit;
        }

        // 检查扩展
        if (!extension_loaded('pcntl')) {
            $this->error("Please install pcntl extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
            exit;
        }
        if (!extension_loaded('posix')) {
            $this->error("Please install posix extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
            exit;
        }

        //因为workerman需要带参数 所以得强制修改
        global $argv;
        $action = $this->argument('action');
        if (!in_array($action, ['start', 'stop', 'status'])) {
            $this->error('Error Arguments');
            exit;
        }
        $argv[0] = 'ws';
        $argv[1] = $action;
        $argv[2] = $this->option('d') ? '-d' : '';

        // BusinessWorker -- 必须是text协议
        new Register('text://0.0.0.0:' . config('gateway.register.port'));

        // BusinessWorker
        $worker                  = new BusinessWorker();
        $worker->name            = config('gateway.worker.name');
        $worker->count           = config('gateway.worker.count');
        $worker->registerAddress = config('gateway.register.host') . ':' . config('gateway.register.port');
        $worker->eventHandler    = 'Console\Commands\WsServer';

        // Gateway
        $gateway                  = new Gateway("websocket://0.0.0.0:" . config('gateway.port'));
        $gateway->name            = config('gateway.gateway.name');
        $gateway->count           = config('gateway.gateway.count');
        $gateway->lanIp           = config('gateway.gateway.lan_ip');
        $gateway->startPort       = config('gateway.gateway.startPort');
        $gateway->registerAddress = config('gateway.register.host') . ':' . config('gateway.register.port');
        $gateway->pingInterval    = 10;
        $gateway->pingData        = '{"action":"sys/ping","data":"0"}';

        Worker::runAll();
    }

    /**
     * 当客户端发来消息时触发
     * @param int   $client_id 连接id
     * @param mixed $message 具体消息
     */
    public static function onMessage($client_id, $message)
    {
        Router::init($client_id, $message);
    }

    /**
     * 当客户端连接时触发
     * 如果业务不需此回调可以删除onConnect
     */
    public static function onConnect()
    {
        $result           = [];
        $result['action'] = "sys/connect";
        $result['msg']    = '连接成功!';
        $result['code']   = 9900;
        WsSender::sendToCurrentClient(json_encode($result, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 进程启动后初始化数据库连接
     */
    public static function onWorkerStart()
    {

    }

    /**
     * 当用户断开连接时触发
     * @param int $client_id 连接id
     */
    public static function onClose($client_id)
    {
        Router::close($client_id);
    }
}

添加配置文件

你可以将IP、端口等参数直接写到程序中,但推荐的做法是写一个配置文件,将这些参数写入配置文件中。config目录下面新建gateway.php文件,内容如下:

<?php

return [

    /*服务端口,对外开放*/
    'port'     => env('WS_SERVER_PORT', '8082'),                   //客户端连接这个端口

    /*注册中心配置*/
    'register' => [
        'host' => env('WS_REGISTER_HOST', '127.0.0.1'),            //地址
        'port' => env('WS_REGISTER_PORT', '1238'),                 //端口
    ],

    /*worker配置*/
    'worker'   => [
        'name'  => env('WS_WORKER_NAME', 'BusinessWorker'),        //名称
        'count' => env('WS_WORKER_COUNT', '1'),                    //进程数量
    ],

    /*gateway配置*/
    'gateway'  => [
        'name'      => env('WS_GATEWAY_NAME', 'gateway'),          //名称
        'count'     => env('WS_GATEWAY_COUNT', '1'),               //进程数量
        'lan_ip'    => env('WS_GATEWAY_LAN_IP', '127.0.0.1'),      //局域网络地址
        'startPort' => env('WS_GATEWAY_START_PORT', '4000'),       //开始端口
    ],
];


运行

#debug运行
php artisan ws start
#常驻后台运行
php artisan ws start --d

创建一个路由类,来处理onMessage事件

    /**
     * 当客户端发来消息时触发
     * @param int   $client_id 连接id
     * @param mixed $message 具体消息
     */
    public static function onMessage($client_id, $message)
    {
        Router::init($client_id, $message);
    }

在app/Modules下面创建 WsServer模块 用来处理所有的WebSocket相关的服务

|--app
|  --Modules
|     |--WsServer             -- WsServer服务模块
|        |--Auth              -- 权限控制功能目录
|        |--Controllers       -- 控制器功能目录
|        |--Service           -- 服务功能目录,
|         --Router.php        -- 路由配置类,用来将onMeaasge事件接收到消息,映射到Controller中的action进行处理

Router.php 内容如下

<?php

namespace App\Modules\WsServer;

use App\Common\Code;
use App\Modules\WsServer\Controllers\Admin\AdminController;
use App\Modules\WsServer\Controllers\ChatController;
use App\Modules\WsServer\Controllers\Server\ServerController;
use GatewayWorker\Lib\Gateway as WsSender;

class Router
{
    //初始化
    static public function init($client_id, $message)
    {
        //格式化输入
        $message = json_decode($message, true);
        $action  = $message['action'];
        $data    = $message['data'];

        //路由
        switch ($action) {

            /* chat 部分 聊天室示例 */
            case 'chat/adminLogin':
                /*登录 - 使用管理员帐号(后台帐号登录)*/
                $result = ChatController::model($client_id, $data)->adminLogin();
                break;
            case 'chat/userLogin':
                /*登录 - 前台用户帐号*/
                $result = ChatController::model($client_id, $data)->userLogin();
                break;
            case 'chat/send':
                /*发送消息*/
                $result = ChatController::model($client_id, $data)->send();
                break;
            case 'chat/online':
                /*获取在线人员*/
                $result = ChatController::model($client_id, $data)->online();
                break;

            /* admin 部分 为管理端提供服务 */
            case 'admin/login':
                /*登录*/
                $result = AdminController::model($client_id, $data)->login();
                break;

            /* server 部分 为用户客户端提供服务 */
            case 'server/login':
                /*结束*/
                $result = ServerController::model($client_id, $data)->login();
                break;

            default:
                $result = ['code' => Code::SYSTEM_ERROR, 'msg' => 'request format error.'];
        }

        $result['action'] = $action;
        //输出结果
        if (!empty($result)) {
            WsSender::sendToCurrentClient(json_encode($result, JSON_UNESCAPED_UNICODE));
        }
    }

    //连接关闭
    static public function close($client_id)
    {
        $group = $_SESSION['ws_client_group'];
        if ($group == ChatController::WS_GROUP_CHAT) {
            $result = ChatController::model($client_id, [])->offline();
        }
    }
}

ChatController.php

编写ChatController类型实现聊天功能,一个简单聊天室成员加入、成员退出,发送消息、接受消息,


<?php

namespace App\Modules\WsServer\Controllers;

use App\Common\Code;
use App\Common\Msg;
use App\Common\Visitor;
use App\Models\AdminMember;
use App\Models\WsToken;
use App\Modules\WsServer\Auth\Auth;
use App\Modules\WsServer\Controllers\Base\BaseController;
use App\Modules\WsServer\Service\TokenService;
use GatewayWorker\Lib\Gateway as WsSender;
use Illuminate\Support\Facades\Cache;

class ChatController extends BaseController
{

    const WS_GROUP_CHAT = 'WS_GROUP_CHAT';

    /**
     * 设置不需要登录的的Action
     * @author Chengcheng
     * @date   2016年10月23日 20:39:25
     * @return array
     */
    protected function noLogin()
    {
        return ['adminLogin', 'userLogin'];
    }

    /**
     * 登录验证
     * @author Chengcheng
     * @date 2016年10月21日 17:04:44
     * @param string $actionID
     * @return bool
     * */
    protected function auth($actionID)
    {
        /* 1 判断Action动作是否需要登录,默认需要登录 */
        $isNeedLogin = true;
        $noLogin     = $this->noLogin();
        $noLogin     = !empty($noLogin) ? $noLogin : [];
        if (in_array($actionID, $noLogin) || $this->isNoLogin) {
            $isNeedLogin = false;
        }

        /* 2 检查用户是否已登录-系统账号登录 */
        $memberResult = Auth::auth()->checkLoginUserMember();
        if ($isNeedLogin == false || $memberResult['code'] == Auth::LOGIN_YES) {
            // 设置框架user信息,默认为unLogin
            Visitor::userMember()->load($memberResult['data']);
            return true;
        }

        /* 3 当前动作需要登录,返回 false,用户未登录,不容许访问 */
        $result["code"] = Code::USER_LOGIN_NULL;
        $result["msg"]  = Msg::USER_LOGIN_NULL;
        return $result;
    }

    /**
     * login - admin
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function adminLoginAction()
    {
        //查询参数
        $param['token'] = $this->requestData['token'];
        $param['type']  = WsToken::MEMBER_TYPE_ADMIN;

        //验证token
        $login = TokenService::wsLogin($param);
        if ($login['code'] != Code::SYSTEM_OK) {
            return $login;
        }

        //保存登录信息
        Auth::auth()->loginUser($login['data']['member']);
        Visitor::userMember()->load($login['data']['member']);
        Visitor::userMember()->type = 'admin';

        //保存登录信息到gateway的session
        $member                = [];
        $member['id']          = Visitor::userMember()->id;
        $member['name']        = Visitor::userMember()->name;
        $member['type']        = Visitor::userMember()->type;
        $member['avatar']      = Visitor::userMember()->avatar;
        $_SESSION['ws_member'] = $member;

        //加入分组
        $_SESSION['ws_client_group'] = static::WS_GROUP_CHAT;
        WsSender::joinGroup($this->clientId, static::WS_GROUP_CHAT);

        //通知上线
        $this->notifyOnline();

        //返回结果
        $result         = [];
        $result['code'] = Code::SYSTEM_OK;
        $result['msg']  = Msg::SYSTEM_OK;
        return $result;
    }

    /**
     * login - user
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function userLoginAction()
    {
        //查询参数
        $param['token'] = $this->requestData['token'];
        $param['type']  = WsToken::MEMBER_TYPE_USER_WX;

        //验证token
        $login = TokenService::wsLogin($param);
        if ($login['code'] != Code::SYSTEM_OK) {
            return $login;
        }

        //保存登录信息
        Auth::auth()->loginUser($login['data']['member']);
        Visitor::userMember()->load($login['data']['member']);
        Visitor::userMember()->type = 'user_wx';

        //保存登录信息到gateway的session
        $member                = [];
        $member['id']          = Visitor::userMember()->id;
        $member['name']        = Visitor::userMember()->name;
        $member['type']        = Visitor::userMember()->type;
        $member['avatar']      = Visitor::userMember()->avatar;
        $_SESSION['ws_member'] = $member;

        //加入分组
        $_SESSION['ws_client_group'] = static::WS_GROUP_CHAT;
        WsSender::joinGroup($this->clientId, static::WS_GROUP_CHAT);

        //通知上线
        $this->notifyOnline();

        //返回结果
        $result         = [];
        $result['code'] = Code::SYSTEM_OK;
        $result['msg']  = Msg::SYSTEM_OK;
        return $result;
    }

    /**
     * 收到客户端发送来的消息 - 发送给所有在线人员
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function sendAction()
    {
        //通知上线
        $this->notifyMsg();

        //返回结果
        $result         = [];
        $result['code'] = Code::SYSTEM_OK;
        $result['msg']  = Msg::SYSTEM_OK;
        return $result;
    }

    /**
     * 获取在线人员
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function onlineAction()
    {
        $sessions       = WsSender::getAllClientSessions(static::WS_GROUP_CHAT);
        $result         = [];
        $result['code'] = Code::SYSTEM_OK;
        $result['msg']  = Msg::SYSTEM_OK;
        $result['data'] = array_column($sessions, 'ws_member');
        return $result;
    }

    /**
     * 人员下线
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function offlineAction()
    {
        //通知上线
        $this->notifyOffline();

        $result         = [];
        $result['code'] = Code::SYSTEM_OK;
        $result['msg']  = Msg::SYSTEM_OK;
        return $result;
    }

    /**
     * 通知上线
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function notifyOnline()
    {
        //上线人信息
        $member           = [];
        $member['id']     = Visitor::userMember()->id;
        $member['name']   = Visitor::userMember()->name;
        $member['type']   = Visitor::userMember()->type;
        $member['avatar'] = Visitor::userMember()->avatar;

        //返回结果
        $data                   = [];
        $data['action']         = 'chat/notifyOnline';
        $data["code"]           = Code::SYSTEM_OK;
        $data["msg"]            = Msg::SYSTEM_OK;
        $data["data"]['member'] = $member;
        WsSender::sendToGroup(static::WS_GROUP_CHAT, json_encode($data, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 通知下线
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function notifyOffline()
    {
        //上线人信息
        $member           = [];
        $member['id']     = Visitor::userMember()->id;
        $member['name']   = Visitor::userMember()->name;
        $member['type']   = Visitor::userMember()->type;
        $member['avatar'] = Visitor::userMember()->avatar;

        //返回结果
        $data                   = [];
        $data['action']         = 'chat/notifyOffline';
        $data["code"]           = Code::SYSTEM_OK;
        $data["msg"]            = Msg::SYSTEM_OK;
        $data["data"]['member'] = $member;
        WsSender::sendToGroup(static::WS_GROUP_CHAT, json_encode($data, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 通知新消息
     * @author Chengcheng
     * @date 2016-10-21 09:00:00
     */
    public function notifyMsg()
    {
        //发送人信息
        $member           = [];
        $member['id']     = Visitor::userMember()->id;
        $member['name']   = Visitor::userMember()->name;
        $member['type']   = Visitor::userMember()->type;
        $member['avatar'] = Visitor::userMember()->avatar;

        //发送内容
        $data                   = [];
        $data['action']         = 'chat/notifyMsg';
        $data["code"]           = Code::SYSTEM_OK;
        $data["msg"]            = Msg::SYSTEM_OK;
        $data["data"]['member'] = $member;
        $data["data"]['msg']    = $this->requestData['msg'];
        $data["data"]['time']   = Visitor::userMember()->time;
        WsSender::sendToGroup(static::WS_GROUP_CHAT, json_encode($data, JSON_UNESCAPED_UNICODE));
    }
}


实现登录权限控制

主要步骤:

  1. 用户打开网页,使用帐号正常登录
  2. 登录成功后,调用接口获取登录WebSocket的token
  3. 发送token到WebSocket服务端
  4. 服务端根据token获取对应用户的信息,登录成功

JS部分代码

前端实现聊天功能

/* 1 定义Metro模块中的WsController*/
Alpaca.MainModule.WsController = {

    //webServer配置
    webServer: {
        ws: null,                                               //* web-socket 连接对象 */
        url: "ws://" + window.location.host + ":8082",          //* web-socket 地址 */
    },

    //onlineList 在线人员数据
    onlineList: {},

    //index-动作
    indexAction: function () {
        var view = new Alpaca.MainModule.pageView();

        view.Layout.ready(function () {
            $('body').addClass('has-detached-right');
        });

        view.ready(function () {

            if (Alpaca.MainModule.WsController.webServer.ws) {
                var onlineList = Alpaca.MainModule.WsController.onlineList;
                for (var i in onlineList) {
                    Alpaca.to('#/main/ws/addOnline', onlineList[i]);
                }
                return;
            }
            AlpacaAjax({
                url: g_url + API['admin_shake_token'],
                data: {},
                success: function (data) {
                    if (data.code != 9900) {
                        return;
                    }

                    //请求正确,开启webSocket
                    var ws_url = Alpaca.MainModule.WsController.webServer.url;
                    var ws     = new WebSocket(ws_url);

                    //onOpen
                    ws.onopen = function () {
                        // 连接成功,登录webSocket
                        var request    = {};
                        request.action = API['ws_chat_admin_login'];
                        request.data   = {token: data.data};
                        ws.send(JSON.stringify(request));
                    };

                    //onMessage
                    ws.onmessage = function (event) {
                        Alpaca.to('#/main/ws/router', event);
                    };

                    //设置ws
                    Alpaca.MainModule.WsController.webServer.ws = ws;
                },
            });

        });
        return view;
    },

    // 处理 ws 路由
    routerAction: function (event) {
        var acceptData = JSON.parse(event.data);
        console.log(acceptData);
        var action = acceptData.action;
        switch (action) {
            case 'chat/adminLogin':
                Alpaca.to('#/main/ws/loginBack', acceptData);
                break;
            case 'chat/notifyOnline':
                Alpaca.to('#/main/ws/notifyOnline', acceptData);
                break;
            case 'chat/notifyOffline':
                Alpaca.to('#/main/ws/notifyOffline', acceptData);
                break;
            case 'chat/online':
                Alpaca.to('#/main/ws/onlineBack', acceptData);
                break;
            case 'chat/notifyMsg':
                Alpaca.to('#/main/ws/notifyMsg', acceptData);
                break;
        }
    },

    // 用户上线
    loginBackAction: function (data) {

        if (data.code != 9900) {
            return;
        }

        //获取在线人员
        var ws         = Alpaca.MainModule.WsController.webServer.ws;
        var request    = {};
        request.action = API['ws_chat_online'];
        request.data   = {msg: data.msg};
        ws.send(JSON.stringify(request));
    },

    // 在线用户
    onlineBackAction: function (data) {

        for (var i in data.data) {
            var uid = data.data[i].type + '_' + data.data[i].id;
            if (Alpaca.MainModule.WsController.onlineList[uid]) {
                continue;
            }
            Alpaca.MainModule.WsController.onlineList[uid] = data.data[i];
            Alpaca.to('#/main/ws/addOnline', data.data[i]);
        }
    },

    // 用户上线
    notifyOnlineAction: function (data) {
        var uid = data.data.member.type + '_' + data.data.member.id;
        if (Alpaca.MainModule.WsController.onlineList[uid]) {
            return;
        }
        Alpaca.MainModule.WsController.onlineList[uid] = data.data.member;
        Alpaca.to('#/main/ws/addOnline', data.data.member);
    },

    // 用户下线
    notifyOfflineAction: function (data) {
        var uid = data.data.member.type + '_' + data.data.member.id;
        delete Alpaca.MainModule.WsController.onlineList[uid];
        var itemClass = ".user-list-item-" + uid;
        $(itemClass).remove();
    },

    // 收到消息
    notifyMsgAction: function (data) {
        Alpaca.to('#/main/ws/addChat', data.data);
    },

    // 发送消息
    sendAction: function (data) {
        var ws         = Alpaca.MainModule.WsController.webServer.ws;
        var request    = {};
        request.action = API['ws_chat_send'];
        request.data   = {msg: data.msg};
        ws.send(JSON.stringify(request));
    },

    // 收到消息
    addOnlineAction: function (data) {

        if (!data.avatar) {
            data.avatar = g_baseUrl + 'main/assets/images/placeholder.jpg"';
        }

        var view  = Alpaca.View({data: data, to: "#online-user-list"});
        view.show = function (to, html) {
            var that = this;
            $(to).append(html);
            that.onLoad();
        };
        view.display();
    },

    // 收到消息
    addChatAction: function (data) {
        if (!data.member.avatar) {
            data.member.avatar = g_baseUrl + 'main/assets/images/placeholder.jpg"';
        }
        var view  = Alpaca.View({data: data, to: "#ws-chat-list"});
        view.show = function (to, html) {
            var that = this;
            $(to).append(html);
            that.onLoad();
        };
        view.display();
    },

};

附录

GatewayWorker手册

Alpaca-Laravel 框架(一) --- 概述,前后分离的后台管理系统

Alpaca-Spa 手册

联系我们

QQ群: 298420174

Alpaca-Laravel 框架(二) --- 集成GateWay实现WebSocket功能-聊天功能示例-LMLPHP

作者: Sponge 邮箱: [email protected]

05-13 19:34