IM 即时通讯云 · 接入文档
多租户 SaaS 版即时通讯服务:每个开发者创建多个「应用(App)」,应用之间数据完全隔离;支持单聊、群聊、好友关系、已读回执、输入状态、上下线通知、离线消息、消息历史与用量统计。
控制台内置可视化测试台:选择应用与 UID 即自动换取 Token、连接 WebSocket,单聊、群聊、好友全流程点一遍就通。
打开测试台 → 我的应用创建应用、查看 AppID、重置 AppSecret、监控用量与套餐配额。
前往我的应用 → 三步接入创建应用拿 AppID → 业务服务器换 IM Token → 小程序 / 网页 / App wss 连接,最快 30 分钟跑通第一条消息。
查看接入流程 →- 多租户隔离:按 AppID 隔离用户、消息、群、好友,不同应用互不可见
- 单聊:文字 / 图片 / 语音 / 视频 / 自定义消息,离线消息上线自动推送、ACK 去重、已读回执、正在输入
- 群聊:建群 / 邀请 / 踢人 / 退群 / 解散 / 禁言 / 群资料修改,群主与管理员角色
- 好友:申请 / 同意 / 拒绝 / 删除,双向好友关系,申请实时推送
- 套餐配额:IM 用户数、好友人数、可加入群数、群成员上限、峰值群组数、历史消息存储时长、同平台多设备在线,按应用生效
- 服务端 API:业务后端可用 HTTP 直接发消息、拉历史、查未读、查在线
一、产品与架构
接入只涉及三个地址:开发者控制台(注册账号、创建应用)、服务端接口(HTTPS,用 AppID + AppSecret 换取 IM Token)与 WebSocket 网关(长连接收发消息):
| 组成 | 地址示例 | 说明 |
|---|---|---|
| 开发者控制台 | https://im.linglankj.cn/console/ | 注册开发者、创建应用、查看 AppID 与 AppSecret、重置密钥、看用量 |
| 服务端接口(HTTPS) | https://im.linglankj.cn/core/* | 你的业务服务器对接(签发 IM Token,与控制台同域名) |
| WebSocket 网关 | wss://swoole.linglankj.cn/ws | 长连接收发消息;另提供 /api/* REST 接口、/health、文档页 /docs 与测试页 /test |
二、接入流程
- 开发者在控制台注册账号、登录,创建应用,获得
AppID与AppSecret(如im1a2b3c...); - 用户登录你的业务系统后,你的业务服务器直接调用
POST /core/open/token(传 IM_APPID + IM_APP_SECRET + 业务用户 uid + 昵称/头像),换回 IM Token 与ws_url; - 客户端(小程序 / 网页 / App)用 Token 建立 WebSocket:
wss://swoole.linglankj.cn/ws?token=xxx,各端代码见第十三章; - 连接成功后服务端推送
auth_ok(含未读汇总)与offline_messages(离线消息); - 收发消息:发
{"type":"message",...},服务端回ack,对方实时收到message; - 客户端每 30 秒发一次
{"type":"ping"}保活;断线自动重连。服务端每 30 秒主动巡检,连接超过 90 秒无任何上行数据将被强制断开并广播user_offline。
wss://,域名需在「微信公众平台 → 开发管理 → 开发设置 → 服务器域名」配置 socket 合法域名,且域名已备案。三、套餐与配额
每个应用在开通/续费套餐后,配额快照保存在应用上,网关实时拦截。配额字段:
| 配额字段 | 含义 | 超限表现 |
|---|---|---|
quota_users | 应用 IM 用户总数(单聊用户规模) | 换取 Token 时新用户被拒:「应用用户数已达套餐上限」 |
quota_groups | 应用峰值群组数(可创建的群总数) | group_create 返回错误码 4112 |
quota_join_groups | 每个用户可加入的群组数 | 建群/受邀入群返回 4114(已达上限的受邀成员自动跳过) |
quota_group_members | 每个群的人数上限 | 建群/邀请返回 4113 / 4134 |
quota_friends | 每个用户的好友数上限 | 同意好友申请时返回 4212 / 4213 |
quota_history_days | 历史消息存储时长(天) | 超期历史消息不再返回(0=永久保存) |
quota_devices | 同平台同时在线设备数 | 超出上限时最早登录的设备被顶下线(0=不限) |
配额值为 0 或 -1 表示不限。应用到期或被禁用后,WebSocket 握手会被拒绝(auth_fail)。
四、服务端接口:换取 IM Token(核心)
服务端接口基础地址为业务站域名(如 https://im.linglankj.cn),统一前缀 /core。统一响应格式:HTTP 状态码始终 200,业务结果看 JSON:
{
"code": 200, // 200=成功,其他=失败
"message": "ok",
"data": { ... } // 业务数据
}
POST/core/open/token
由你的业务服务器在用户登录小程序后调用。业务服务器只需在配置中保存 IM_APPID 与 IM_APP_SECRET(控制台「我的应用」中获取),无需开发者账号登录态。为应用下的业务用户(uid)签发 IM Token;用户不存在时自动创建(受 quota_users 限制)。
// 请求头
Content-Type: application/json
// 请求体
{
"appid": "im1a2b3c4d5e6f708", // 应用 ID(IM_APPID),控制台创建应用获得
"app_secret": "********", // 应用密钥(IM_APP_SECRET),仅服务端使用
"uid": 10001, // ★ 你的业务系统用户 ID,正整数,全局唯一对应该用户
"nickname": "小明", // 可选,会同步到 IM 用户资料
"avatar": "https://..." // 可选,头像 URL
}
// 成功返回
{
"code": 200,
"data": {
"token": "eyJhcHBpZCI6...a1b2c3", // ★ IM Token,小程序拿它连 WebSocket
"expires_in": 604800, // 有效期(秒),默认 7 天
"ws_url": "wss://swoole.linglankj.cn/ws", // WebSocket 连接地址(平台配置)
"appid": "im1a2b3c4d5e6f708",
"uid": 10001
}
}
| code | message 示例 | 原因与处理 |
|---|---|---|
| 400 | 参数 appid、app_secret、uid 必填 | 请求缺参数,检查请求体 |
| 401 | AppID 或 AppSecret 不正确 | 密钥错误或在控制台重置过,到「我的应用」核对 |
| 403 | 应用已被平台封禁,请联系平台方 | 应用违规被封禁 |
| 403 | 应用已停用,请在控制台启用后再接入 | 控制台启用应用 |
| 403 | 应用套餐已到期,请续费后再接入 | 续费套餐 |
| 403 | 应用用户数已达套餐上限(N 人) | 升级套餐后才能接入新用户 |
<?php
// ========= 第一步:通用请求函数(原样复制到你的业务项目里) =========
function httpPost(string $url, array $data): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE),
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode((string) $body, true) ?: [];
}
// ========= 第二步:给你的前端提供「换 Token」接口(如 /api/im-token) =========
// 前置:业务服务器 .env 配置(控制台「我的应用」中获取)
// IM_APPID=im1a2b3c4d5e6f708
// IM_APP_SECRET=********(64 位密钥)
// 1. 只有已登录的业务用户才能拿到 IM Token(换成你自己的登录态判断)
$uid = $_SESSION['user_id'] ?? 0;
if ($uid <= 0) {
header('Content-Type: application/json; charset=utf-8');
http_response_code(401);
exit(json_encode(['code' => 401, 'message' => '未登录']));
}
// 2. 用 IM_APPID + IM_APP_SECRET 为该用户换取 IM Token(无需开发者账号登录态)
$res = httpPost('https://im.linglankj.cn/core/open/token', [
'appid' => getenv('IM_APPID'),
'app_secret' => getenv('IM_APP_SECRET'),
'uid' => (int) $uid, // 业务用户 ID
'nickname' => $_SESSION['nickname'] ?? '', // 可选
'avatar' => $_SESSION['avatar'] ?? '', // 可选,头像 URL
]);
// 3. 把 token 和 ws_url 返回给前端(小程序/网页/App 拿去连 WebSocket)
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['code' => 0, 'data' => [
'token' => $res['data']['token'] ?? '',
'ws_url' => $res['data']['ws_url'] ?? '',
]]);
IM Token 是不透明字符串:由平台业务服务端签发、网关验签,内部包含 appid、uid 与有效期等信息。开发者无需解析、也不要自行构造,拿到后原样用于 WebSocket 连接与 HTTP API 调用即可。
五、WebSocket 连接与鉴权
WS
// 推荐:Token 放在 URL 上
wss://swoole.linglankj.cn/ws?token=<IM_TOKEN>
// 也可以先连接再鉴权(二选一)
new WebSocket('wss://swoole.linglankj.cn/ws')
// 连接建立后立即发送:
{"type":"auth","token":"<IM_TOKEN>"}
鉴权成功服务端推送:
{
"type": "auth_ok",
"app_id": 12,
"appid": "im1a2b3c4d5e6f708",
"uid": 10001,
"server_time": 1693500000000, // 毫秒
"unread": {
"total": 5,
"private": { "total": 2, "conversations": [ {"peer_id": 10002, "count": 2} ] },
"groups": [ { "group_id": 5, "count": 3, "last_read_id": 880 } ]
}
}
若有离线期间的单聊消息,紧接着推送 offline_messages:
{ "type": "offline_messages", "count": 2, "messages": [ /* 消息结构数组,见下节 */ ] }
鉴权失败推送 {"type":"auth_fail","reason":"token 无效或已过期 / 应用已被禁用 / 应用套餐已到期"} 后断开。同账号在别处登录会收到 {"type":"kick","reason":"账号已在其他设备登录"}。
{"type":"ping"},服务端回 {"type":"pong","server_time":...}。
服务端每 30 秒主动巡检一次,连接超过 90 秒没有任何上行数据(ping、消息等)将被强制断开并广播
user_offline——断网、杀进程等「未发关闭帧」的异常离线,最迟约 2 分钟内全网可见其离线。
建议客户端实现断线重连(重连后用新 Token 或原 Token 重新握手);小程序请在 onHide / onUnload 时主动 socketTask.close(),离线更即时。
六、消息统一结构(Envelope)
所有聊天消息(单聊/群聊、实时/离线/历史)都使用同一结构:
{
"msg_id": 12345, // 服务端消息 ID(全局递增)
"client_msg_id": "a1b2c3...", // 客户端去重 ID(发送时可自带,服务端兜底生成)
"conv_type": 1, // 1=单聊 2=群聊
"from_id": 10001, // 发送者 uid
"to_id": 10002, // 单聊=接收者 uid;群聊=group_id
"msg_type": "text", // text | image | voice | video | custom | system
"content": "你好", // 文本内容;图片/语音/视频传【文件 URL】字符串;撤回后为空字符串
"is_read": 0, // 0=未读 1=已读
"recalled": 0, // 0=正常 1=已被撤回(撤回消息 content 不再下发,显示"XX撤回了一条消息")
"created_at": 1693500000000 // 毫秒时间戳
}
content 发送,msg_type 分别为 image / voice / video;
custom 类型的 content 可放自定义 JSON 字符串(点赞、礼物、订单卡片等)。七、单聊协议
7.1 发送消息 客户端→服务端
{
"type": "message",
"to_id": 10002, // 接收方 uid(不能是自己)
"msg_type": "text", // 可选,默认 text
"content": "你好",
"client_msg_id": "uuid-xxx" // 可选,建议传,用于 ACK 去重
}
服务端应答:
{ "type": "ack", "client_msg_id": "uuid-xxx", "msg_id": 12345, "to_id": 10002, "created_at": 1693500000000 }
对方在线时实时收到(服务端→客户端):
{ "type": "message", "msg_id": 12345, "conv_type": 1, "from_id": 10001, "to_id": 10002, "msg_type": "text", "content": "你好", ... }
对方不在线:消息存为离线消息,下次上线通过 offline_messages 推送。
7.2 标记已读 →
{ "type": "read", "peer_id": 10002 }
// 应答:{ "type": "read_ack", "peer_id": 10002, "affected": 3 }
// 对方在线则收到已读回执:
{ "type": "read_receipt", "from_id": 10001, "to_id": 10002, "created_at": 1693500000000 }
7.3 正在输入 →
{ "type": "typing", "to_id": 10002 }
// 对方实时收到(不入库):{ "type": "typing", "from_id": 10001 }
7.4 历史消息 →
{ "type": "history", "peer_id": 10002, "before_id": 0, "limit": 20 }
// before_id=0 表示拉最新一页;翻页时传本页最小 msg_id
// 应答:
{
"type": "history",
"peer_id": 10002,
"messages": [ /* Envelope 数组,按时间倒序 */ ],
"has_more": true
}
7.5 撤回消息 →
单聊、群聊消息撤回使用同一指令 recall:仅消息发送者本人可撤回,需在发送后 2 分钟内操作;重复撤回幂等。
撤回成功后,单聊双方(含自己的其他设备)、群聊全体成员都会实时收到 message_recalled 推送;历史消息 / 离线消息中该条消息 recalled=1 且 content 为空字符串,客户端统一展示「XX 撤回了一条消息」。
// 撤回消息(单聊 / 群聊通用;msg_id 来自发送 ACK 或消息体)
{ "type": "recall", "msg_id": 12345 }
// 应答:{ "type": "recall_ack", "msg_id": 12345, "conv_type": 1 }
// 单聊:对方与自己的其他在线设备收到:
{ "type": "message_recalled", "conv_type": 1, "msg_id": 12345, "peer_id": 10001, "from_id": 10001 }
// 群聊:全体群成员(含自己其他设备)收到:
{ "type": "message_recalled", "conv_type": 2, "msg_id": 12399, "group_id": 5, "from_id": 10001 }
// 客户端按 msg_id 定位本地消息,替换为"XX撤回了一条消息"
八、群聊协议
群角色 role:1=群主,2=管理员,3=普通成员;非成员为 0。群消息结构中单聊 Envelope 的 conv_type=2、to_id=group_id。
8.1 群消息
// 发送(仅群成员;被禁言成员发送会被拒)
{ "type": "group_message", "group_id": 5, "msg_type": "text", "content": "大家好", "client_msg_id": "uuid" }
// 应答:{ "type": "group_ack", "client_msg_id": "uuid", "msg_id": 12399, "group_id": 5, "created_at": ... }
// 其他在线成员收到:
{ "type": "group_message", "group_id": 5, "msg_id": 12399, "conv_type": 2, "from_id": 10001, "to_id": 5, ... }
8.2 群操作指令一览 客户端→服务端
| type | 参数 | 权限 / 说明 | 应答 / 推送 |
|---|---|---|---|
| group_create | {name, avatar?, notice?, member_uids?:[uid...]} | 任何人可建群;创建者为群主;受群数/每群人数配额限制 | 应答 group_created;被拉成员收 group_event(invited) |
| group_update | {group_id, name?, avatar?, notice?} | 群主/管理员 | 应答 group_updated;全员收 group_event(updated) |
| group_invite | {group_id, uids:[10004,...]} | 群成员可邀请;受每群人数配额限制 | 应答 group_invite_result(added/skipped);新成员收 group_event(invited) |
| group_kick | {group_id, uid} | 群主/管理员;管理员不能踢管理员和群主 | 应答 group_kick_result;被踢者收 group_event(kicked) |
| group_quit | {group_id} | 成员主动退群;群主需先解散 | 应答 group_quit_result;管理员收 group_event(quit) |
| group_dismiss | {group_id} | 仅群主 | 应答 group_dismiss_result;全员收 group_event(dismissed) |
| group_mute | {group_id, uid, mute:1|0} | 群主/管理员;不能禁言群主 | 应答 group_mute_result;被操作者收 group_event(muted/unmuted) |
| group_read | {group_id} | 群成员;把群未读清零 | {type:"group_read_ack", group_id, last_read_id} |
| group_typing | {group_id} | 群成员;实时透传不入库 | 其他成员收 {type:"group_typing", group_id, from_id} |
| group_history | {group_id, before_id?, limit?} | 群成员 | {type:"group_history", group_id, messages:[...], has_more} |
| group_list | 无 | 我加入的群列表 | {type:"group_list", groups:[...]} |
| group_members | {group_id} | 群成员 | {type:"group_members", group_id, members:[...]} |
群对象结构
{
"group_id": 5,
"name": "技术交流群",
"avatar": "https://.../g.png",
"notice": "群公告内容",
"owner_id": 10001,
"member_count": 8,
"role": 1, // 当前请求者在该群的角色(group_list 中返回)
"mute": 0, // 当前请求者是否被禁言(group_list 中返回)
"created_at": 1693500000000
}
群成员对象
{ "uid": 10002, "nickname": "小红", "avatar": "https://...", "role": 3, "mute": 0, "join_time": 1693500000 }
群事件推送 服务端→客户端
{
"type": "group_event",
"event": "invited", // invited | updated | kicked | quit | dismissed | muted | unmuted
"group_id": 5,
"inviter": 10001, // 或 operator / uid,视事件而定
"group": { /* 完整群对象,部分事件携带 */ }
}
九、好友协议
| 方向 | type | 参数 / 说明 |
|---|---|---|
| → | friend_apply | {to_uid, message?} 发起好友申请;已是好友/已有待处理申请会返回错误 |
| → | friend_handle | {request_id, action:"agree"|"refuse"} 处理收到的申请;同意时校验双方好友配额 |
| → | friend_list | 无参数,返回我的好友列表 |
| → | friend_requests | 无参数,返回收到的(inbox)与发出的(outbox)申请 |
| → | friend_delete | {friend_uid} 删除好友(双向解除) |
应答与推送示例
// 申请发出:{ "type": "friend_apply_sent", "request_id": 3, "to_uid": 10002 }
// 被申请方实时收到:
{
"type": "friend_event",
"event": "request_new", // request_new | request_agreed | request_refused | deleted
"request": {
"id": 3, "from_uid": 10001, "message": "我是小明",
"nickname": "小明", "avatar": "https://...", "created_at": 1693500000000
}
}
// 同意/拒绝后申请人收到:friend_event(event=request_agreed/request_refused, request_id, friend_uid, ...)
// 好友列表:
{ "type": "friend_list", "friends": [ {"uid":10002,"nickname":"小红","avatar":"...","remark":"","created_at":...} ] }
// 申请列表:
{ "type": "friend_requests",
"inbox": [ {"id":3,"from_uid":10003,"nickname":"小刚","avatar":"...","message":"...","status":0,"created_at":...} ],
"outbox": [ {"id":2,"to_uid":10004,"nickname":"小丽","avatar":"...","message":"...","status":1,"created_at":...} ] }
// status: 0=待处理 1=已同意 2=已拒绝
// 删除好友:应答 { "type":"friend_deleted", "friend_uid":10002 };对方收 friend_event(event=deleted)
十、在线状态与用户资料
// 批量查在线状态(限同应用)
{ "type": "presence_query", "uids": [10002, 10003] }
// 应答:{ "type": "presence", "presence": { "10002": 1, "10003": 0 } } // 1=在线 0=离线
// 批量查用户资料
{ "type": "user_profile", "uids": [10002, 10003] }
// 应答:{ "type": "user_profile", "users": [ {"uid":10002,"nickname":"小红","avatar":"https://..."} ] }
好友/有过会话的用户上下线时会自动推送(限同应用):
{ "type": "user_online", "uid": 10002 }
{ "type": "user_offline", "uid": 10002 }
十一、服务端推送事件汇总
| type | 触发时机 | 关键字段 |
|---|---|---|
| pong | 心跳应答 | server_time |
| auth_ok / auth_fail | 鉴权结果 | unread / reason |
| kick | 同账号在别处登录 | reason |
| offline_messages | 上线时推送离线单聊消息 | count, messages[] |
| message / group_message | 收到单聊/群聊消息 | Envelope |
| ack / group_ack | 消息发送成功应答 | client_msg_id, msg_id |
| read_ack / read_receipt | 已读应答 / 对方已读回执 | peer_id / from_id |
| typing / group_typing | 对方正在输入 | from_id, group_id? |
| history / group_history | 历史消息应答 | messages[], has_more |
| recall_ack | 撤回消息成功应答 | msg_id, conv_type |
| message_recalled | 消息被撤回(单聊双方 / 群聊全员实时推送) | msg_id, conv_type, from_id, peer_id?/group_id? |
| group_created / group_updated / group_invite_result / group_kick_result / group_quit_result / group_dismiss_result / group_mute_result | 群操作应答 | group / added / skipped 等 |
| group_event | 群成员被动接收群变更 | event: invited/updated/kicked/quit/dismissed/muted/unmuted |
| group_list / group_members / group_read_ack | 群列表/成员/已读应答 | groups[] / members[] / last_read_id |
| friend_apply_sent / friend_handle_result / friend_list / friend_requests / friend_deleted | 好友操作应答 | — |
| friend_event | 好友申请/同意/拒绝/被删推送 | event: request_new/request_agreed/request_refused/deleted |
| presence / user_profile / user_online / user_offline | 状态与资料 | — |
| error | 任何错误 | {code, message} |
十二、网关 HTTP API(业务服务器调用)
网关同时提供 REST 接口,用于业务服务器主动发通知、拉历史等。鉴权使用 IM Token(三种方式任选:Authorization: Bearer <token> 请求头 / ?token= / body 中 token 字段)。响应格式为 {"code":0,"msg":"ok","data":{...}}(code=0 成功)。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /health | 健康检查(无需鉴权),返回在线连接数 |
| POST | /api/messages/send | HTTP 发单聊消息 {to_id, msg_type?, content, client_msg_id?},返回 {message, delivered}(delivered=false 表示已存离线) |
| GET | /api/messages/history?peer_id=&before_id=&limit= | 单聊历史消息 |
| GET | /api/messages/unread | 未读汇总(单聊 + 群聊) |
| POST | /api/messages/read | 标记单聊已读 {peer_id},并给对方推 read_receipt |
| GET | /api/users?uids=10001,10002 | 批量查用户资料(限同应用,最多 200 个) |
| GET | /api/users/online?uids=10001,10002 | 批量查在线状态(POST 传 JSON 数组也可) |
// 例:业务服务器给用户推送系统通知
curl -X POST https://swoole.linglankj.cn/api/messages/send \
-H "Authorization: Bearer <某用户的 IM_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"to_id":10002,"msg_type":"system","content":"您的订单已发货"}'
// 返回(HTTP 200,code=0 成功):
{
"code": 0,
"msg": "ok",
"data": {
"message": { "msg_id": 12400, "conv_type": 1, "from_id": 10001, "to_id": 10002, "msg_type": "system", "content": "您的订单已发货", ... },
"delivered": true // true=对方在线已实时投递;false=已存离线,上线后自动推送
}
}
uid 作为发送方;服务端通知场景可给系统账号(如 uid=99999)换 Token 后调用。十三、多端接入代码(小程序 / 网页 / App)
三端使用完全相同的协议:Token 由业务服务器调 /core/open/token 换取,客户端建立 WebSocket 后收发的 JSON 指令、事件名、消息结构全部一致。区别只在各平台的 WebSocket API 写法与域名配置方式。
13.1 微信小程序
新建 utils/im.js:
// utils/im.js —— 微信小程序 IM 接入封装(单聊 + 群聊 + 好友 + 自动重连心跳)
let socket = null, connected = false, pingTimer = null, reconnectTimer = null;
let listeners = {}; // 事件监听
const HEARTBEAT = 30000;
function on(type, fn) { (listeners[type] = listeners[type] || []).push(fn); }
function emit(type, data) { (listeners[type] || []).forEach(fn => fn(data)); }
function connect(wsUrl, token) {
disconnect();
socket = wx.connectSocket({
url: wsUrl + '?token=' + token,
success() {}
});
socket.onOpen(() => {
connected = true;
emit('open');
clearInterval(pingTimer);
pingTimer = setInterval(() => send({ type: 'ping' }), HEARTBEAT);
});
socket.onMessage((res) => {
const msg = JSON.parse(res.data);
emit(msg.type, msg); // auth_ok / message / group_message / group_event / friend_event ...
emit('message', msg); // 兜底:所有消息都监听
});
socket.onClose(() => {
connected = false;
clearInterval(pingTimer);
emit('close');
// 断线自动重连(Token 未过期可直接重连,生产环境建议向业务后端重新取 Token)
reconnectTimer = setTimeout(() => connect(wsUrl, token), 3000);
});
socket.onError(() => { emit('error'); });
}
function send(data) {
if (!connected) return false;
socket.send({ data: JSON.stringify(data) });
return true;
}
function disconnect() {
clearInterval(pingTimer);
clearTimeout(reconnectTimer);
connected = false;
if (socket) { try { socket.close({}); } catch (e) {} socket = null; }
}
// ===== 业务快捷方法 =====
function sendPrivate(toUid, content, msgType = 'text') {
return send({
type: 'message', to_id: toUid, msg_type: msgType, content,
client_msg_id: Date.now() + '' + Math.floor(Math.random() * 1000)
});
}
function sendGroup(groupId, content, msgType = 'text') {
return send({
type: 'group_message', group_id: groupId, msg_type: msgType, content,
client_msg_id: Date.now() + '' + Math.floor(Math.random() * 1000)
});
}
module.exports = { connect, disconnect, send, sendPrivate, sendGroup, on };
页面中使用:
// pages/chat/chat.js
const im = require('../../utils/im');
Page({
onLoad() {
// 1. 先向你的业务后端换取 IM Token(服务端调 /core/open/token)
wx.request({
url: 'https://你的业务域名/api/im-token', // 你自己的接口
method: 'POST',
data: {},
success: (res) => {
const { token, ws_url } = res.data.data;
im.connect(ws_url, token);
}
});
// 2. 监听事件
im.on('auth_ok', (d) => console.log('登录成功,未读', d.unread));
im.on('message', (d) => {
if (d.type === 'message') console.log('收到单聊', d);
});
im.on('group_message', (d) => console.log('收到群消息', d));
im.on('group_event', (d) => console.log('群事件', d.event, d));
im.on('friend_event', (d) => console.log('好友事件', d.event, d));
im.on('kick', () => wx.showModal({ title: '提示', content: '账号在其他设备登录' }));
},
// 发送文本
onSend() {
im.sendPrivate(10002, '你好,这是一条小程序消息');
// 群聊:im.sendGroup(5, '大家好');
},
onUnload() { /* 全局单例建议在 App 级别管理,不在页面卸载时断开 */ }
});
wss://,需在「微信公众平台 → 开发管理 → 服务器域名」添加 socket 合法域名(网关域名);上传/下载文件还要配置 uploadFile / downloadFile 域名。
13.2 网页端(Web / H5 / PC 网站)
浏览器原生 WebSocket 即可接入,无需任何 SDK。新建 utils/im.js:
// utils/im.js —— 网页端 IM 接入封装(浏览器原生 WebSocket,零依赖)
let socket = null, connected = false, pingTimer = null, reconnectTimer = null;
let listeners = {};
const HEARTBEAT = 30000;
export function on(type, fn) { (listeners[type] = listeners[type] || []).push(fn); }
function emit(type, data) { (listeners[type] || []).forEach(fn => fn(data)); }
export function connect(wsUrl, token) {
disconnect();
socket = new WebSocket(wsUrl + '?token=' + token);
socket.onopen = () => {
connected = true;
emit('open');
clearInterval(pingTimer);
pingTimer = setInterval(() => send({ type: 'ping' }), HEARTBEAT);
};
socket.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
emit(msg.type, msg);
emit('message', msg);
};
socket.onclose = () => {
connected = false;
clearInterval(pingTimer);
emit('close');
// 断线自动重连(生产环境建议先向业务后端重新取 Token)
reconnectTimer = setTimeout(() => connect(wsUrl, token), 3000);
};
socket.onerror = () => emit('error');
}
export function send(data) {
if (!connected || socket.readyState !== WebSocket.OPEN) return false;
socket.send(JSON.stringify(data));
return true;
}
export function disconnect() {
clearInterval(pingTimer);
clearTimeout(reconnectTimer);
connected = false;
if (socket) { try { socket.close(); } catch (e) {} socket = null; }
}
// 业务快捷方法(与小程序版完全一致)
export function sendPrivate(toUid, content, msgType = 'text') {
return send({ type: 'message', to_id: toUid, msg_type: msgType, content,
client_msg_id: Date.now() + '' + Math.floor(Math.random() * 1000) });
}
export function sendGroup(groupId, content, msgType = 'text') {
return send({ type: 'group_message', group_id: groupId, msg_type: msgType, content,
client_msg_id: Date.now() + '' + Math.floor(Math.random() * 1000) });
}
Vue / React 页面中使用:
// Chat.vue(Vue3 示例,React 同理)
import { onMounted, onUnmounted } from 'vue'
import { connect, disconnect, on, sendPrivate } from '@/utils/im'
onMounted(async () => {
// 1. 向你的业务后端换取 IM Token(服务端调 /core/open/token)
const res = await fetch('/api/im-token', { method: 'POST' }).then(r => r.json())
const { token, ws_url } = res.data
connect(ws_url, token)
// 2. 监听事件(事件名与小程序完全一致)
on('auth_ok', (d) => console.log('登录成功,未读', d.unread))
on('message', (d) => { if (d.type === 'message') console.log('收到单聊', d) })
on('group_message', (d) => console.log('收到群消息', d))
on('kick', () => alert('账号在其他设备登录'))
})
onUnmounted(() => { /* 建议在全局状态/Pinia 中管理连接,组件卸载不断开 */ })
function sendText() {
sendPrivate(10002, '你好,这是一条网页消息')
}
readyState 并自动重连。
13.3 移动 App(iOS / Android / uni-app)
推荐使用 uni-app(一套代码同时出小程序 / H5 / iOS / Android)。下面是完整可用的封装,与网页版的协议、事件名、快捷方法完全一致,直接复制到项目即可:
// utils/im.js —— uni-app 版(一套代码发布 iOS / Android / 小程序 / H5)
// 与小程序版唯一区别:wx.* 换成 uni.*,其余协议、事件名、快捷方法全部一致
let socketTask = null, connected = false, pingTimer = null;
let listeners = {};
const HEARTBEAT = 30000;
export function on(type, fn) { (listeners[type] = listeners[type] || []).push(fn); }
function emit(type, data) { (listeners[type] || []).forEach(fn => fn(data)); }
export function connect(wsUrl, token) {
socketTask = uni.connectSocket({ url: wsUrl + '?token=' + token, complete: () => {} });
socketTask.onOpen(() => {
connected = true; emit('open');
clearInterval(pingTimer);
pingTimer = setInterval(() => send({ type: 'ping' }), HEARTBEAT);
});
socketTask.onMessage((res) => {
const msg = JSON.parse(res.data);
emit(msg.type, msg);
emit('message', msg);
});
socketTask.onClose(() => {
connected = false;
clearInterval(pingTimer);
emit('close');
setTimeout(() => connect(wsUrl, token), 3000); // 断线重连
});
}
export function send(data) {
if (!connected) return false;
socketTask.send({ data: JSON.stringify(data) });
return true;
}
export function sendPrivate(toUid, content, msgType = 'text') {
return send({ type: 'message', to_id: toUid, msg_type: msgType, content,
client_msg_id: Date.now() + '' + Math.floor(Math.random() * 1000) });
}
export function sendGroup(groupId, content, msgType = 'text') {
return send({ type: 'group_message', group_id: groupId, msg_type: msgType, content,
client_msg_id: Date.now() + '' + Math.floor(Math.random() * 1000) });
}
// App.vue 中全局初始化:
// uni.login / 自有账号体系登录后 → 调业务后端换 Token → connect(ws_url, token)
// 原生 iOS(Swift/OC)、Android(Kotlin/Java)、Flutter、React Native:
// 使用各自 WebSocket 库连接同样的 wss 地址,收发同样的 JSON 即可,无额外适配层。
原生 iOS / Android 或 Flutter / React Native 等跨端框架,接入要点相同:
- 使用平台原生 WebSocket 库(iOS 可 Starscream / URLSessionWebSocketTask;Android 可 OkHttp WebSocket;Flutter 用
web_socket_channel;React Native 用WebSocket全局对象); - 连接地址
wss://网关域名/ws?token=IM_TOKEN,或先连接再发{"type":"auth","token":"..."}; - 收发消息按第六章至第十一章的 JSON 协议解析;每 30 秒发
{"type":"ping"}心跳; - App 端建议在应用级单例管理连接,前后台切换时做断线重连;网络从 Wi-Fi 切到 4G/5G 后也要重连;
- 图片/语音/视频先走 App 文件上传到业务后端/对象存储拿 URL,再以
image/voice/video类型发 URL; - 业务服务器的通知类消息(如订单状态、系统通知)可直接调网关 REST API
POST /api/messages/send,无需客户端在线。
im.sendPrivate(uid, url, 'image') / im.sendGroup(gid, url, 'video')。十四、错误码
WebSocket 错误统一推送 {"type":"error","code":xxx,"message":"..."}:
| code | 含义 |
|---|---|
| 400 | 消息格式错误 / 参数不合法 / content 为空等 |
| 401 | 未鉴权或 Token 无效 |
| 403 | 应用不存在/被禁用/已到期 |
| 404 | 未知消息类型 |
| 4004 / 4005 / 4006 | 单聊:peer_id 不合法 / uids 为空 |
| 4040 ~ 4043 | 撤回:msg_id 不合法 / 消息不存在或已过期 / 只能撤回自己的消息 / 超过 2 分钟不可撤回 |
| 4101 ~ 4181 | 群聊:群不存在、非群成员、被禁言、非群主/管理员、群名非法、群数/群成员超配额等(message 有具体说明) |
| 4201 ~ 4220 | 好友:已是好友、申请已存在、申请不存在、好友数超配额、friend_uid 不合法等 |
| 4301 | 查询资料 uids 为空 |
| 500 | 服务端内部错误(可重试,同时请反馈平台方排查) |
业务接口(/core/*)错误:HTTP 200,code 非 200,message 为中文原因;401 鉴权失败(AppID/AppSecret 不正确或 Token 无效)、403 无权限/配额上限、404 资源不存在。
十五、在线测试
控制台自带可视化测试台(无需写代码即可验证全部协议):登录 开发者控制台,左侧菜单「应用接入 → 在线测试」即可打开。
测试台已内置控制台登录态,无需再填 API 地址与开发者账号:选择应用、填写 UID,即自动换取 IM Token 并连接 WebSocket。支持:单聊互发、建群/群消息/群管理、好友申请全流程、在线状态查询。开两个浏览器窗口(或一个普通窗口 + 一个无痕窗口)用不同 UID 连接即可互测。