「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!

1群聊和私聊

群聊: 群聊是指在一个群组中,多个成员可以互相交流和分享信息,多人参与的聊天对话。您可以创建或加入不同的群组,与团队成员、同事或其他人进行群组讨论和协作。可以容纳多个成员,适合用于团队讨论和群体交流。

私聊: 是指一对一的私密对话。在单聊中,您可以与其他用户进行私密交流,分享文件、图片、语音消息等。单聊适合私人对话、个别咨询和私密信息的传递。仅限两个成员参与,提供了私密的交流空间,私聊消息只有发送者和接收者可见,适合私人交流和个人话题讨论。

2约定

约定大于配置原则  这里先约定好客户端和服务端请求数据结构和字段。

字段约定

字段 描述 示例值
event 事件(join:加入连接,speak:发送消息) join
mode 消息模式(1:私聊,2:群聊) 1
group_id 群组ID(私聊:0) 0
from_user_id 来自用户id 10086
from_username 来自用户昵称 开源技术小栈
to_user_id 接受用户id 10000
content 消息内容 你好! Tinywan

请求JOSN

{
    "event""join",
    "mode"1,
    "group_id"0,
    "from_user_id""10086",
    "from_username""开源技术小栈",
    "to_user_id""10000",
    "content""Hi, 开源技术小栈"
}

用户和群组约定

用户名称 用户id(群组ID)
阿克苏 10086
拉姆才让 10000
無尘 2023
开源技术小栈(群) 100

3验证

这里直接复用webman官方验证器插件Validate 验证器插件

composer 安装

composer require tinywan/validate

消息验证器

自定义消息验证器MessageFormatValidate.php

<?php
/**
 * @desc IM消息格式验证器类,用于验证IM消息格式是否正确,以及消息内容是否合法,比如:消息类型、消息长度等等
 * @author Tinywan(ShaoBo Wan)
 * @email 756684177@qq.com
 * @date 2023/12/10 10:50
 */

declare(strict_types=1);

namespace appcommonvalidate;

class MessageFormatValidate extends BaseValidate
{
    /**
     * @var string[]
     */
    protected $rule = [
        'mode' => 'require|in:1,2',
        'group_id' => 'require|number',
        'from_user_id' => 'require',
        'from_username' => 'require',
        'from_avatar' => 'require',
        'content' => 'require|max:128',
    ];

    /**
     * @var string[]
     */
    protected $message = [
        'mode.require' => '消息模式是必须的',
        'mode.in' => '消息模式只能是1或2',
        'group_id.require' => '群组group_id是必须的',
        'from_user_id.require' => '用户id是必须的',
        'from_username.require' => '用户名必须填写',
        'from_avatar.require' => '用户头像是必须的',
        'content.require' => '消息内容是必须的',
        'content.max' => '消息内容最大支持128个字符',
    ];
}

Part1业务Events

回调函数

  • onWorkerStart():当businessWorker进程启动时触发。每个进程生命周期内都只会触发一次。
  • onWebSocketConnect():当客户端连接上gateway完成websocket握手时触发的回调函数。注意:此回调只有gateway为websocket协议并且gateway没有设置onWebSocketConnect时才有效。
  • onMessage():当客户端发来数据(Gateway进程收到数据)后触发的回调函数
  • onClose():客户端与Gateway进程的连接断开时触发。不管是客户端主动断开还是服务端主动断开,都会触发这个回调。一般在这里做一些数据清理工作。

onWebSocketConnect()

/**
 * @desc: 当客户端连接上gateway完成websocket握手时触发
 * @param string $clientId
 * @param array $data
 * @return bool
 * @author Tinywan(ShaoBo Wan)
 */
public static function onWebSocketConnect(string $clientId, array $data): bool
{
    try {
        $_SESSION['client_ip'] = get_client_real_ip($data['server']['HTTP_X_FORWARDED_FOR'] ?? $data['server']['HTTP_REMOTEIP'] ?? '127.0.0.1');
        $_SESSION['browser'] = isset($data['server']['HTTP_USER_AGENT']) ? get_client_browser($data['server']['HTTP_USER_AGENT']) : '未知';
    } catch (Throwable $e) {
        Log::error('[onWebSocketConnect] ' . $e->getMessage() . '|' . $e->getFile() . '|' . $e->getLine());
        return Gateway::sendToCurrentClient(broadcast_json(500, $e->getMessage()));
    }
    return true;
}

onMessage()回调函数

/**
 * @desc 当客户端发来数据后触发的回调函数
 * @param string $clientId
 * @param string $message
 * @return false
 * @author Tinywan(ShaoBo Wan)
 */
public static function onMessage(string $clientId, string $message): bool
{
    try {
        $originMessage = json_decode($messagetrue);
        if (json_last_error() != JSON_ERROR_NONE) {
            Gateway::closeClient($clientId, broadcast_json(400, '无效的json数据'));
            return false;
        }
        if (!is_array($originMessage)) {
            Gateway::closeClient($clientId, broadcast_json(400, '请求数据结构无法被解析'));
            return false;
        }

        $validate = new MessageFormatValidate();
        if (false === $validate->check($originMessage)) {
            Gateway::closeClient($clientId, broadcast_json(400, $validate->getError()));
            return false;
        }
        $groupId = $originMessage['group_id'];
        switch ($originMessage['event']) {
            case 'join':
                /** 群聊 */
                if ($originMessage['mode'] === 2) {
                    $_SESSION['group_id'] = $groupId;
                    Gateway::joinGroup($clientId$groupId);
                /** 私聊 */
                } else {
                    Gateway::bindUid($clientId$originMessage['from_user_id']);
                }
                $_SESSION['mode'] = $originMessage['mode'];
                $_SESSION['event'] = $originMessage['event'];
                $_SESSION['group_id'] = $groupId;
                $_SESSION['from_user_id'] = $originMessage['from_user_id'];
                $_SESSION['from_username'] = $originMessage['from_username'];
                Gateway::sendToCurrentClient(broadcast_json(0, 'success'$originMessage));
                break;
            case 'speak':
                /** 私聊 */
                if ($originMessage['mode'] == 1) {
                    $msg = $originMessage['from_username'] . '[单聊对]'.$originMessage['to_user_id'].'[说]:' . $originMessage['content'];
                    Gateway::sendToUid($originMessage['to_user_id'], broadcast_json(0, $msg$originMessage));
                /** 群聊 */
                } else {
                    $msg = $originMessage['from_username'] . '[群聊说]:' . $originMessage['content'];
                    Gateway::sendToGroup($groupId, broadcast_json(0, $msg$originMessage));
                }
                break;
            default:
                Gateway::sendToCurrentClient(broadcast_json(400, 'default invalid'$originMessage));
        }
    } catch (Throwable $throwable) {
        return Gateway::sendToClient($clientId, broadcast_json(500, $throwable->getMessage()));
    }
    return true;
}

onClose()回调函数

/**
 * @desc: 客户端与Gateway进程的连接断开时触发
 * @param $clientId
 * @return bool
 * @author Tinywan(ShaoBo Wan)
 */
public static function onClose($clientId): bool
{
    try {
        $data = [
            'event' => 'leave',
            'group_id' => $_SESSION['group_id'] ?? '',
            'client_id' => $clientId,
            'content' => 'leaving group',
            'from_user_id' => $_SESSION['from_user_id'] ?? '',
            'from_username' => $_SESSION['from_username'] ?? ''
        ];
        if (isset($data['group_id']) && !empty($data['group_id'])) {
            GateWay::sendToGroup($data['group_id'], broadcast_json(0, 'close'$data));
            return true;
        }
        return Gateway::sendToCurrentClient(broadcast_json(500, 'error'$data));
    } catch (Throwable $e) {
        $data = ['client_id' => $clientId'content' => $e->getMessage()];
        Log::error('[onClose] ' . $e->getMessage() . '|' . $e->getFile() . '|' .
            $e->getLine() . ': clientId = ' . $clientId);
        return Gateway::sendToCurrentClient(broadcast_json(500, 'error'$data));
    }
}  

Part2单聊

阿克苏拉姆才让 说话

加入会话

阿克苏

var ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function(evt
   let $_content = {
      "event""join",
      "mode"1,
      "group_id"0,
      "from_user_id""10086",
      "from_username""阿克苏",
      "to_user_id""10000",
      "content""加入会话",
   };
   ws.send(JSON.stringify($_content));
};

ws.onmessage = function(evt{
  console.log( "【阿克苏】接受消息: " + evt.data);
};

拉姆才让

var ws = new WebSocket("ws://127.0.0.1:8783");

ws.onopen = function(evt) { 
   let $_content = {
      "event""join",
      "mode": 1,
      "group_id": 0,
      "from_user_id""10000",
      "from_username""拉姆才让",
      "to_user_id""10086",
      "content""加入会话"
   };
   ws.send(JSON.stringify($_content));
};

ws.onmessage = function(evt) {
  console.log( "【拉姆才让】接受消息: " + evt.data);
};

发送消息

阿克苏拉姆才让说:

let $_content = {
  "event""speak",
  "mode"1,
  "group_id"0,
  "from_user_id""10086",
  "from_username""阿克苏",
  "to_user_id""10000",
  "content""Hi, 我是阿克苏",
};
ws.send(JSON.stringify($_content));
ws.onmessage = function(evt{
  console.log( "【阿克苏】接受消息: " + evt.data);
};

拉姆才让阿克苏说:

let $_content = {
  "event""speak",
  "mode"1,
  "group_id"0,
  "from_user_id""10000",
  "from_username""拉姆才让",
  "to_user_id""10086",
  "content""Hi, 我是拉姆才让",
};
ws.send(JSON.stringify($_content));
ws.onmessage = function(evt{
  console.log( "【拉姆才让】接受消息: " + evt.data);
};

阿克苏 console

「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!

拉姆才让 console

「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!

Part3群聊

加入会话

阿克苏

var ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function(evt) { 
   let $_content = {
      "event""join",
      "mode": 2,
      "group_id": 100,
      "from_user_id""10086",
      "from_username""阿克苏",
      "to_user_id""10000",
      "content""加入会话",
   };
   ws.send(JSON.stringify($_content));
};

ws.onmessage = function(evt) {
  console.log( "【阿克苏】接受消息: " + evt.data);
};

拉姆才让

var ws = new WebSocket("ws://127.0.0.1:8783");

ws.onopen = function(evt) { 
   let $_content = {
      "event""join",
      "mode": 2,
      "group_id": 100,
      "from_user_id""10000",
      "from_username""拉姆才让",
      "to_user_id""10086",
      "content""加入会话"
   };
   ws.send(JSON.stringify($_content));
};

ws.onmessage = function(evt) {
  console.log( "【拉姆才让】接受消息: " + evt.data);
};

無尘

var ws = new WebSocket("ws://127.0.0.1:8783");
ws.onopen = function(evt) { 
   let $_content = {
      "event""join",
      "mode": 2,
      "group_id": 100,
      "from_user_id""2023",
      "from_username""無尘",
      "to_user_id""10000",
      "content""加入会话",
   };
   ws.send(JSON.stringify($_content));
};

ws.onmessage = function(evt) {
  console.log( "【無尘】接受消息: " + evt.data);
};

发送消息

阿克苏拉姆才让说:

let $_content = {
  "event""speak",
  "mode"2,
  "group_id"100,
  "from_user_id""10086",
  "from_username""阿克苏",
  "to_user_id""10000",
  "content""【群组消息】:我是阿克苏",
};
ws.send(JSON.stringify($_content));
ws.onmessage = function(evt{
  console.log( "【阿克苏】接受消息: " + evt.data);
};

拉姆才让阿克苏说:

let $_content = {
  "event""speak",
  "mode"2,
  "group_id"100,
  "from_user_id""10000",
  "from_username""拉姆才让",
  "to_user_id""10086",
  "content""【群组消息】:拉姆才让",
};
ws.send(JSON.stringify($_content));
ws.onmessage = function(evt{
  console.log( "【拉姆才让】接受消息: " + evt.data);
};

拉姆才让 收到群组100的消息

「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!
「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!

上一章节:「IM系列」WebSocket教程:安全授权认证详解和简单实现思路

Part4源码

文章相关源码地址:https://github.com/Tinywan/webman-admin


原文始发于微信公众号(开源技术小栈):「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247974.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!