Appearance
workerman
产品说明
workerman
Workerman 是一款开源高性能异步 PHP socket 框架。支持高并发,超高稳定性,被广泛的用于手机 app、移动通讯,微信小程序,手游服务端、网络游戏、PHP 聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。
手册:workerman 源码:https://github.com/walkor/workerman
GatewayWorker
GatewayWorker 基于 Workerman 开发的一个框架,支持多协议多端口监听,用于快速开发长连接应用,例如移动通讯、物联网、智能家居、游戏服务端、聊天室等等。
手册:GatewayWorker 源码:https://github.com/walkor/GatewayWorker
安装扩展
下面我们来能过几步体验 workerman 与 GatewayWorker
安装 GatewayWorker 时会同时将 workerman,所以不单独安装 workerman 也是可以的。
composer require workerman/workerman
composer require workerman/gateway-worker
服务代码
下面来创建 socket 的业务代码,目录结构如下
socket
├── App
│ ├── Events.php
│ ├── start_businessworker.php
│ ├── start_gateway.php
│ └── start_register.php
├── ssl
└── start.php
文件内容
start.php
该文件用于启动应用使用
<?php
ini_set('display_errors', 'on');
use Workerman\Worker;
if (strpos(strtolower(PHP_OS), 'win') === 0) {
exit("start.php not support windows, please use start_for_win.bat\n");
}
// 检查扩展
if (!extension_loaded('pcntl')) {
exit("Please install pcntl extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
}
if (!extension_loaded('posix')) {
exit("Please install posix extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
}
// 标记是全局启动
define('GLOBAL_START', 1);
require_once __DIR__ . '/../vendor/autoload.php';
// 加载所有Applications/*/start.php,以便启动所有服务
foreach (glob(__DIR__ . '/App/start*.php') as $start_file) {
require_once $start_file;
}
// 运行所有服务
Worker::runAll();
App/Events.php
该文件用于对客户端事件处理
<?php
use \GatewayWorker\Lib\Gateway;
/**
* 主逻辑
* 主要是处理 onConnect onMessage onClose 三个方法
* onConnect 和 onClose 如果不需要可以删除
*/
class Events
{
/**
* 当客户端连接时触发
* @param int $client_id 连接id
*/
public static function onConnect($client_id)
{
Gateway::sendToClient($client_id, json_encode([
'type' => 'init',
'client_id' => $client_id,
]));
}
/**
* 当客户端发来消息时触发
* @param int $client_id 连接id
* @param mixed $message 具体消息
*/
public static function onMessage($client_id, $message)
{
// 向所有人发送
Gateway::sendToAll("$client_id said $message\r\n");
}
/**
* 当用户断开连接时触发
* @param int $client_id 连接id
*/
public static function onClose($client_id)
{
// 向所有人发送
GateWay::sendToAll("$client_id logout\r\n");
}
}
App\start_gateway.php
Gateway 进程是暴露给客户端的让其连接的进程。所有客户端的请求都是由 Gateway 接收然后分发给 BusinessWorker 处理
<?php
use \Workerman\Worker;
use \GatewayWorker\Gateway;
use \Workerman\Autoloader;
require_once __DIR__ . '/../../vendor/autoload.php';
// gateway 进程
$gateway = new Gateway("Websocket://0.0.0.0:7272");
// 设置名称,方便status时查看
$gateway->name = 'ChatGateway';
// 设置进程数,gateway进程数建议与cpu核数相同
$gateway->count = 4;
// 分布式部署时请设置成内网ip(非127.0.0.1)
$gateway->lanIp = '127.0.0.1';
// 内部通讯起始端口。假如$gateway->count=4,起始端口为2300
// 则一般会使用2300 2301 2302 2303 4个端口作为内部通讯端口
$gateway->startPort = 2300;
// 心跳间隔
$gateway->pingInterval = 10;
// 心跳数据
$gateway->pingData = '{"type":"ping"}';
// 服务注册地址
$gateway->registerAddress = '127.0.0.1:1236';
// 如果不是在根目录启动,则运行runAll方法
if (!defined('GLOBAL_START')) {
Worker::runAll();
}
App\start_businessworker.php
BusinessWorker 收到 Gateway 转发来的事件及请求时会默认调用 Events.php 中的 onConnect onMessage
<?php
use \Workerman\Worker;
use \GatewayWorker\BusinessWorker;
use \Workerman\Autoloader;
require_once __DIR__ . '/../../vendor/autoload.php';
// bussinessWorker 进程
$worker = new BusinessWorker();
// worker名称
$worker->name = 'ChatBusinessWorker';
// bussinessWorker进程数量
$worker->count = 4;
// 服务注册地址
$worker->registerAddress = '127.0.0.1:1236';
// 如果不是在根目录启动,则运行runAll方法
if (!defined('GLOBAL_START')) {
Worker::runAll();
}
App\start_register.php
Gateway 进程和 BusinessWorker 进程启动后分别向 Register 进程注册自己的通讯地址,Gateway 进程和 BusinessWorker 通过 Register 进程得到通讯地址后,就可以建立起连接并通讯了。
<?php
use \Workerman\Worker;
use \GatewayWorker\Register;
require_once __DIR__ . '/../../vendor/autoload.php';
// register 服务必须是text协议
$register = new Register('text://0.0.0.0:1236');
// 如果不是在根目录启动,则运行runAll方法
if (!defined('GLOBAL_START')) {
Worker::runAll();
}
启动服务
下面是常用的启动与停止命令
常用命令
以 debug(调试)方式启动
php socket/start.php start
以 daemon(守护进程)方式启动
php socket/start.php start -d
停止
php socket/start.php stop
重启
php socket/start.php restart
平滑重启
php socket/start.php reload
查看状态
php socket/start.php status
查看连接状态(需要 Workerman 版本>=3.5.0)
php socket/start.php connections
前台测试
打开 chrome 浏览器,按 F12 打开调试控制台,在 Console 一栏输入(或者把下面代码放入到 html 页面用 js 运行)
// 假设服务端ip为127.0.0.1
ws = new WebSocket("ws://127.0.0.1:7272");
ws.onopen = function() {
alert("连接成功");
ws.send('tom');
alert("给服务端发送一个字符串:tom");
};
ws.onmessage = function(e) {
alert("收到服务端的消息:" + e.data);
};
聊天室
开发者最关心的是如何与现有 laravel 等框架整合。下面通过 Laravel 与 Workerman 开发聊天室来掌握与 PHP 框架结合的使用。
需要掌握以下知识点
- 现有 mvc 框架项目与 GatewayWorker 独立部署互不干扰
- 所有的业务逻辑都由网站页面 post/get 到 mvc 框架中完成
- GatewayWorker 不接受客户端发来的数据,即 GatewayWorker 不处理任何业务逻辑,GatewayWorker 仅仅当做一个单向的推送通道
- 仅当 mvc 框架需要向浏览器主动推送数据时才在 mvc 框架中调用 Gateway 的 API GatewayClient 完成推送。
扩展包
为了让 Laravel 框架与 GatewayWorker 通信需要安装扩展包 GatewayClient
composer require workerman/gatewayclient
控制器
ChatController 控制器用于处理用户消息并转发到 socket
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use GatewayClient\Gateway;
use Auth;
/**
* 聊天室
* @package App\Http\Controllers
*/
class ChatController extends Controller
{
public function __construct()
{
//SOCKET服务地址
Gateway::$registerAddress = '127.0.0.1:1236';
}
//初次连接时显示欢迎信息
public function init(Request $request)
{
if (Auth::check()) {
Gateway::bindUid($request->client_id, Auth::id());
$this->sendToAll('进入直播间');
}
}
//发送聊天信息
public function send(Request $request)
{
if (Auth::check()) {
$this->sendToAll($request->content);
}
}
//通知所有在线用户
protected function sendToAll($content)
{
$user = Auth::user();
Gateway::sendToAll(json_encode([
'type' => 'chat_message',
'user' => ['id' => $user->id, 'nickname' => $user->nickname, 'icon' => $user->icon],
'content' => $content,
'user_count' => Gateway::getAllClientIdCount()
]));
}
}
路由定义
下面在 routes/api.php 中定义路由
//聊天室
Route::post('chat/init', [ChatController::class, 'init']);
Route::post('chat/send', [ChatController::class, 'send']);
vue 组件
下面是使用 vue3 定义的组件,其中的 axios 用于发送网络请求请自行完善
接口请求文件 chatApi
import axios from 'plugins/axios'
//聊天室
export default {
//初始聊天室用户
async init(data) {
return axios.post('chat/init', data)
},
//发送消息
async send(data) {
return axios.post('chat/send', data)
}
}
聊天室组件 chat.vue
<template>
<div class="border p-3 bg-white flex flex-col justify-between">
<div class="flex flex-col justify-start overflow-auto" id="live-chat-messages">
<div
v-for="(message, index) in messages"
:key="index"
class="bg-gray-100 mb-3 p-3 w-full flex items-center"
:class="{ 'bg-red-50': helper.user().id == message.user.id }"
>
<img :src="message.user.icon" class="w-8 h-8 rounded-full mr-2" />
<div class="text-sm flex flex-col">
<div class="text-xs">{{ message.user.nickname }}</div>
{{ message.content }}
</div>
</div>
</div>
<el-input v-model="form.content" placeholder="请输入聊天内容" clearable @keyup.enter="send"></el-input>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import chatApi from 'api/chatApi'
import helper from 'utils/helper'
const form = reactive({ client_id: '', content: '' })
const messages = ref([])
let socket = new WebSocket('ws://192.168.10.10:7272')
//绑定SOCKET会话处理
socket.onmessage = async response => {
response = JSON.parse(response.data)
console.log(response)
switch (response.type) {
//Events.php中返回的init消息
//数据包括socket的客户client_id
case 'init':
ElMessage.success('你已加入聊天室')
//当前用户的socket的client_id
form.client_id = response.client_id
//初始化用户,向所有用户发送该用户到来的欢迎消息
await chatApi.init({ client_id: form.client_id })
break
case 'chat_message':
//接收用户的聊天消息
messages.value.push(response)
setTimeout(() => {
document.querySelector('#live-chat-messages').scrollTop = 99999
})
messages.value = messages.value.reverse().splice(0, 20).reverse()
}
}
const send = async () => {
if (!form.content) return ElMessage.error('聊天内容不能为空')
await chatApi.send(form)
form.content = ''
}
</script>
SSL 证书
如果网站使用 HTTPS 则需要为 workerman 配置 SSL 证书,下面以在宝塔面板生成的证书为例,其他方式生成证书配置方式是一样。
如果网站使用 http 访问则不需要操作
创建文件
创建目录 socket/ssl
并在其中创建以下两个空文件
- 证书(PEM 格式)文件
server.pem
- 密钥(KEY)文件
server.key
登录宝塔后台并查看 SSL 证书(如果不会设置证书,请查看后盾人文档库中的相应章节)
将密钥(KEY)
的内容保存在server.key
文件中,将证书(PEM格式)
内容保存在server.pem
文件中
端口配置
更改socket/App/start_gateway.php
文件中的协议为websocket
并设置端口为8282
...
$gateway = new Gateway("websocket://0.0.0.0:8282");
...
如果网站为 https 访问则需要配置 SSL 证书
...
$context = array(
'ssl' => array(
// 使用绝对路径
'local_cert' => __DIR__ . '/../ssl/server.pem',
'local_pk' => __DIR__ . '/../ssl/server.key',
'verify_peer' => false,
'allow_self_signed' => true
)
);
$gateway = new Gateway("websocket://0.0.0.0:8282", $context);
$gateway->transport = 'ssl';
...
启动服务
初次启动时会提示,关闭某些函数的禁用操作,按照提示在 php.ini 或宝塔 PHP 管理中删除禁用的函数
在开发时使用调试 模式运行
php socket/start.php start
在生产环境时使用守护进程方式运行
php socket/start.php start -d
停止服务
php socket/start.php stop
启动成功后将看到以下界面
前端测试
下面在前端进行连接测试
- 请将域名更改为你网站的域名
- 如果后台是 https 请使用
wss://
<script>
let socket = new WebSocket("wss://dev.hdcms.com:8282");
socket.onmessage = function(response){
console.log(response);
}
</script>
如果控制台显示类似以下内容即表示安装成功
MessageEvent {isTrusted: true, data: "{"type":"init","client_id":"7f0000010b5400000001"}", origin: "wss://dev.hdcms.com:8282", lastEventId: "", source: null, …}