概述
本文主要介绍Alpaca-Laravel框架集成GateWayWorker实现WebSocket功能,并且以一个简单的聊天室作为示例。Alpaca-Laravel框架是使用Alpaca-spa与Laravel前开端分离开发的一款快速开发框架,集成了用户管理,权限控制等功能,详情请阅读《Alpaca-Laravel 框架(一) --- 概述,前后分离的后台管理系统》。
项目相关代码以及文档地址
主页 | 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));
}
}
实现登录权限控制
主要步骤:
- 用户打开网页,使用帐号正常登录
- 登录成功后,调用接口获取登录WebSocket的token
- 发送token到WebSocket服务端
- 服务端根据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();
},
};
附录
Alpaca-Laravel 框架(一) --- 概述,前后分离的后台管理系统
联系我们
QQ群: 298420174
作者: Sponge 邮箱: [email protected]