本文主要介绍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