「IM系列」WebSocket教程:私聊和群聊实现,数字化转型!
2026-01-10 16:43:19 | 限时活动 | admin | 501°c
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