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代码语言:javascript复制{

"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 安装代码语言:javascript复制composer require tinywan/validate

消息验证器自定义消息验证器MessageFormatValidate.php

代码语言:javascript复制

/**

* @desc IM消息格式验证器类,用于验证IM消息格式是否正确,以及消息内容是否合法,比如:消息类型、消息长度等等

* @author Tinywan(ShaoBo Wan)

* @email 756684177@qq.com

* @date 2023/12/10 10:50

*/

declare(strict_types=1);

namespace app\common\validate;

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()代码语言:javascript复制/**

* @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()回调函数代码语言:javascript复制/**

* @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($message, true);

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()回调函数代码语言:javascript复制/**

* @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单聊阿克苏 对 拉姆才让 说话

加入会话阿克苏

代码语言:javascript复制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);

};

拉姆才让

代码语言:javascript复制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);

};

发送消息阿克苏对拉姆才让说:

代码语言:javascript复制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);

};

拉姆才让对阿克苏说:

代码语言:javascript复制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

拉姆才让 console

Part3群聊加入会话阿克苏

代码语言:javascript复制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);

};

拉姆才让

代码语言:javascript复制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);

};

無尘

代码语言:javascript复制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);

};

发送消息

阿克苏对拉姆才让说:

代码语言:javascript复制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);

};

拉姆才让对阿克苏说:

代码语言:javascript复制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教程:安全授权认证详解和简单实现思路

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