# WORKERMAN

houdunren.com @ 向军大叔

xj-small

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

手册:http://doc2.workerman.net/

# 安装配置

# 安装扩展

  1. 安装workerman

    composer require workerman/gateway-worker
    
  2. 下载 hdcms 项目中的 socket目录内容到你项目中

  3. 配置composer.json 设置自动加载

    "autoload": {
        "psr-4": {
          ...
          "Socket\\": "socket/"
        },
    ...
    
  4. 然后执行 composer dump-autoload 命令

# SSL证书

如果网站使用HTTPS则需要为workerman配置SSL证书,下面以在宝塔面板生成的证书为例,其他方式生成证书配置方式是一样。

如果网站使用http访问则不需要操作

# 创建文件

创建目录 socket/ssl并在其中创建以下两个空文件

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

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

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, …}

# 聊天室

开发者最关心的是如何与现有mvc框架(ThinkPHP Yii laravel等)整合。下面通过Laravel与Workerman开发聊天室来掌握与PHP框架结合的使用。

img

需要掌握以下知识点

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

# 扩展包

为了让Laravel框架与GatewayWorker通信需要安装扩展包 GatewayClient

composer require workerman/gatewayclient

# 事件处理

socket/Applications/YourApp/events.php 用于配置客户端与SOCKET的通信处理事件

<?php

class Events
{
   //用户初始连接时执行
   public static function onConnect($client_id) {
        Gateway::sendToClient($client_id, json_encode([
            'type'      => 'init',
            'client_id' => $client_id,
        ]));
   }
   
   //我们使用Laravel框架处理用户消息,所以不需要设置
   public static function onMessage($client_id, $message) {
   }
   
   //我们使用Laravel框架处理用户消息,所以不需要设置
   public static function onClose($client_id) {
   }
}

# 路由定义

Route::post('chat/init', 'ChatController@init')->name("chat.init");
Route::post('chat/send', 'ChatController@send')->name("chat.send");

# 控制器

<?php

namespace Modules\Edu\Http\Controllers\Front;

use Auth;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use GatewayClient\Gateway;

class ChatController extends Controller
{
    public function __construct()
    {
        //SOCKET地址
        Gateway::$registerAddress = '127.0.0.1:1238';
    }

    //初次连接时显示欢迎信息
    public function init(Request $request)
    {
        Gateway::joinGroup($request->client_id, 'chat');
        if (Auth::check()) {
            $this->sendToAll('进入直播间');
        }
    }

    //发送聊天信息
    public function send(Request $request)
    {
        if (Auth::check()) {
            $this->sendToAll($request->input('content'));
        }
    }

    //通知所有在线用户
    protected function sendToAll($content)
    {
        Gateway::sendToAll(json_encode([
            'user' => ['id' => user('id'), 'nickname' => user('nickname')],
            'content' => $content,
            'user_count' => Gateway::getClientIdCountByGroup('chat')
        ]));
    }
}

# 前端组件

下面来创建聊天室组件 LiveChat.vue

<template>
  <div class="flex-fill d-flex flex-column bg-light" style="height:380px">
    <div class="flex-fill p-2 chats">
      <div class="mb-2" v-for="(message,index) in messages" :key="index">
        <a href="#" class="text-secondary">{{ message.user.nickname }}</a>
        <div class="d-inline-block pt-1">{{ message.content }}</div>
      </div>
    </div>
    <div class>
      <div class="form-group mb-0" v-if="isLogin">
        <input
          type="text"
          class="form-control rounded-0 bg-light shadow-sm border-left-0 border-right-0"
          name="content"
          placeholder="说点什么吧..."
          v-model="content"
          @keyup.enter="send"
          style="outline: none !important; box-shadow: none;border:solid 3px #333;"
        />
      </div>
      <div class="form-group mb-0 text-center pt-2 pb-2 border-top border-bottom" v-if="!isLogin">
        <a href="/login" class="btn btn-sm btn-info">请登录后操作</a>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      client_id: '',
      messages: [],
      content: '',
      isLogin: !!window.user.id,
    }
  },
  mounted() {
    let socket = new WebSocket('wss://dev.hdcms.com:8282')
    //绑定SOCKET会话处理
    socket.onmessage = this.message
  },
  methods: {
    message(response) {
      let data = JSON.parse(response.data)
      this.client_id = data.client_id
      switch (data.type) {
        //Events.php中返回的init类型的消息,将client_id发给后台进行uid绑定
        case 'init':
          this.axios.post('/Edu/chat/init', this.$data)
          break
        //聊天消息
        default:
          this.messages.push(data)
          this.messages = this.messages.reverse().splice(0, 20).reverse()
          this.$nextTick(() => {
            document.querySelector('.chats').scroll({ top: 9999 })
          })
          break
      }
    },
    send() {
      if (this.content.trim()) this.axios.post('/Edu/chat/send', this.$data).then((_) => (this.content = ''))
    },
  },
}
</script>

<style lang="scss" scoped>
.chats {
  overflow-y: auto;
  height: 200px;
}
</style>