# 产品说明

houdunren.com (opens new window) @ 向军大叔

xj-small

# workerman

Workerman是一款开源高性能异步PHP socket框架。支持高并发,超高稳定性,被广泛的用于手机app、移动通讯,微信小程序,手游服务端、网络游戏、PHP聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。

手册:workerman (opens new window) 源码:https://github.com/walkor/workerman (opens new window)

# GatewayWorker

GatewayWorker基于Workerman开发的一个框架,支持多协议多端口监听,用于快速开发长连接应用,例如移动通讯、物联网、智能家居、游戏服务端、聊天室等等。

手册:GatewayWorker (opens new window) 源码:https://github.com/walkor/GatewayWorker (opens new window)

# 安装扩展

下面我们来能过几步体验 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框架结合的使用。

img

需要掌握以下知识点

  • 现有mvc框架项目与GatewayWorker独立部署互不干扰
  • 所有的业务逻辑都由网站页面post/get到mvc框架中完成
  • GatewayWorker不接受客户端发来的数据,即GatewayWorker不处理任何业务逻辑,GatewayWorker仅仅当做一个单向的推送通道
  • 仅当mvc框架需要向浏览器主动推送数据时才在mvc框架中调用Gateway的API GatewayClient (opens new window) 完成推送。

# 扩展包

为了让Laravel框架与GatewayWorker通信需要安装扩展包 GatewayClient (opens new window)

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并在其中创建以下两个空文件

  1. 证书(PEM格式)文件 server.pem
  2. 密钥(KEY)文件 server.key

登录宝塔后台并查看SSL证书(如果不会设置证书,请查看后盾人文档库 (opens new window)中的相应章节)

image-20200809190720846

密钥(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管理中删除禁用的函数

    image-20200809161419616

在开发时使用调试 模式运行

php socket/start.php start

在生产环境时使用守护进程方式运行

php socket/start.php start -d

停止服务

php socket/start.php stop

启动成功后将看到以下界面

image-20200809191324602

# 前端测试

下面在前端进行连接测试

  • 请将域名更改为你网站的域名
  • 如果后台是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, …}