本文主要介紹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基於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
cd your_path/laravel_program composer require workerman/workerman
composer require workerman/gateway-worker
composer require workerman/gatewayclient
由於GateWayWorker服務啓動是基於cli命令行模式,因此咱們用laravel的artisan實現GateWayWorker的命令,這樣作的好處是,你的websocket項目與web項目環境統一,無縫對接,使用統一的類加載規則,複用代碼。web
php artisan make:command WsServer
這樣Laravel會在 App\Console\Commands 目錄下面生成一個WsServer.php文件數據庫
若是你修改了Laravel默認的目錄結構,請將他複製到相應的Commands目錄json
稍後再修改這個文件的內容,如今先註冊command
App\Console\Kernel.php文件添加剛纔建立的command
protected $commands = [ Commands\WsServer::class ];
<?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
/** * 當客戶端發來消息時觸發 * @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 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 定義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(); }, };
Alpaca-Laravel 框架(一) --- 概述,先後分離的後臺管理系統
QQ羣: 298420174
做者: Sponge 郵箱: 1796512918@qq.com