Alpaca-Laravel 框架(二) --- 集成GateWay實現WebSocket功能-聊天功能示例

概述

本文主要介紹Alpaca-Laravel框架集成GateWayWorker實現WebSocket功能,而且以一個簡單的聊天室做爲示例。Alpaca-Laravel框架是使用Alpaca-spa與Laravel前開端分離開發的一款快速開發框架,集成了用戶管理,權限控制等功能,詳情請閱讀《Alpaca-Laravel 框架(一) --- 概述,先後分離的後臺管理系統》php

圖片名稱

項目相關代碼以及文檔地址

內容 說明 地址
主頁 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

注:後臺管理端登陸帳號是一個測試賬號,權限只有瀏覽功能,沒有編輯等修改功能。html

安裝GateWayWorker

GatewayWorker基於Workerman開發的一個項目框架,用於快速開發TCP長鏈接應用,例如app推送服務端、即時IM服務端、遊戲服務端、物聯網、智能家居等等前端

這裏主要到三個插件: Workerman,GateWayWorkerW, GateWayClientlinux

注:如下示例中經過composer安裝的Workerman,GateWayWorkerW, GateWayClient所有爲linux版本,若是讀者想安裝windows版本,請把名字改成對應windows版本的名字。laravel

安裝前請確認你的環境是否支持GateWayWorker,例如使用如下命令:git

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

詳細說明,請閱讀GateWayWorker的官方文檔。github

安裝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項目環境統一,無縫對接,使用統一的類加載規則,複用代碼。web

建立command
php artisan make:command WsServer

這樣Laravel會在 App\Console\Commands 目錄下面生成一個WsServer.php文件數據庫

若是你修改了Laravel默認的目錄結構,請將他複製到相應的Commands目錄json

稍後再修改這個文件的內容,如今先註冊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

圖片名稱

做者: Sponge 郵箱: 1796512918@qq.com

相關文章
相關標籤/搜索