聊天室进阶:KV、保活、优先级与禁言
聊天室是在线态会话:成员关系绑定在 WebSocket 会话上,断连即离开;消息只广播给当时在线的成员,不落离线历史、不产生投递 ACK、不能拉取历史。本页覆盖房间级 KV 公告、保活与心跳、低优先级消息配置、白名单/禁言/封禁、销毁策略与 TTL,以及每一步的真实限制与错误码。
客户端 IMLib 只提供 5 个聊天室语义操作:createChatroom / joinChatroom / leaveChatroom / getChatroomInfo / getChatroomMembers。KV、保活、优先级、白名单、禁言、销毁都必须由 App Server 调 Open API(客户端不能持有 AppSecret)。
聊天室消息(conversationType=3)不走离线队列、没有 send_ack、getHistoryMessages(3, …) 返回空页;需要“可回溯的直播弹幕”时,请另建普通群或自行在业务侧归档。
保活只对空闲销毁策略生效;fixed(定时销毁)优先,保活不会顺延销毁时间。
生命周期与 TTL
聊天室从创建开始计时,默认 30 分钟无活动即销毁(服务端可通过 IM_CHATROOM_IDLE_MS 调整,上限由部署决定)。房间内任何有效活动(加入、发言、KV 写入等)都会按策略顺延销毁时间。
| 阶段 | 触发方式 | 服务端行为 | 客户端可观测 |
|---|---|---|---|
| 创建 | Open API chatroom.create / 客户端 createChatroom | 幂等:同 chatroomId 已存在返回现有房间;创建即写入销毁时间 | 回调 chatroom.created;调用方拿到 Chatroom |
| 加入 | WS chatroom_join → chatroom_joined | WS 会话级成员;房内广播 member_changed;10 秒未收到 joined 客户端按 3002 处理 | `Events.CHATROOM_EVENT`(kind=joined/member_changed) |
| 活动 | 发言 / KV / 加入 / 离开 | idle 策略下顺延 destroyAt;fixed 策略不下延 | getChatroomInfo 返回新的 destroyAt |
| 销毁 | 空闲超时 / chatroom.destroy / 管理端销毁 / fixed 到期 | 踢出所有在线成员、广播 destroyed、清理 KV/禁言/封禁/白名单、回调 chatroom.destroyed | `CHATROOM_EVENT`(kind=destroyed),后续调用返回 1012/1013 |
| 策略 | 设置接口 | destroyAt 计算 | 保活是否生效 |
|---|---|---|---|
idle(默认) | chatroom.destroy.policy.set {destroyType:"idle", idleMs?} | 当前时间 + idleMs(缺省 1800000ms),每次活动顺延 | 是:保活期内固定为保活到期时间 |
fixed | chatroom.destroy.policy.set {destroyType:"fixed", idleMs} | 创建时间 + idleMs,活动不再顺延 | 否:定时销毁优先 |
import { AppClient } from '@ni-paas-im/server-sdk';
const app = new AppClient({
appKey: process.env.IM_APP_KEY,
appSecret: process.env.IM_APP_SECRET,
baseUrl: 'http://127.0.0.1:8080',
});
// 1) 创建(chatroomId 可省略,服务端生成)
const room = await app.createChatroom({ chatroomId: 'live_1001', name: '新品发布会', owner: 'anchor_1' });
// 2) 定时销毁:创建后 2 小时销毁,活动不延长
await app.openApi('chatroom.destroy.policy.set', {
chatroomId: room.chatroomId,
destroyType: 'fixed',
idleMs: 2 * 60 * 60 * 1000,
});
// 3) 恢复空闲销毁(默认 30 分钟)
await app.openApi('chatroom.destroy.policy.set', {
chatroomId: room.chatroomId,
destroyType: 'idle',
idleMs: 1800000,
});
// 4) 主动销毁:踢人 + 广播 destroyed + 清理房间级状态
await app.destroyChatroom({ chatroomId: room.chatroomId });KV 公告:房间级键值
房间级 KV 用于公告、置顶、榜单快照等随房间销毁的小数据。值是字符串,支持可选 ttlMs(惰性过期:读取时清理,不保证到点即删)。KV 变更会回调 chatroom.kv_changed,便于 App Server 同步外部缓存。
| 操作 | Open API | 关键参数 | 说明 |
|---|---|---|---|
| 写入 | chatroom.kv.set | {chatroomId, key, value, ttlMs?} | 同 key 覆盖;ttlMs 省略或 ≤0 表示不过期;服务端未设 key/value 长度上限(受请求体与网关约束,建议 key ≤128 字节、value ≤4KB) |
| 读取 | chatroom.kv.get | {chatroomId, keys?} | keys 省略返回全部未过期项 |
| 删除 | chatroom.kv.remove | {chatroomId, key} | 返回删除后的剩余 KV 列表;幂等 |
| 客户端读取 | getChatroomInfo 不含 KV | — | 客户端没有 KV API;公告由 App Server 下发或通过 chatroom.kv_changed 推送到业务通道 |
// 公告写入:30 分钟后自动过期(房间销毁时一并清理)
await app.openApi('chatroom.kv.set', {
chatroomId: 'live_1001',
key: 'announcement',
value: JSON.stringify({ text: '21:00 抽奖,请留在直播间', pinnedAt: Date.now() }),
ttlMs: 30 * 60 * 1000,
});
// 读取单个键 + 读取全部
const one = await app.openApi('chatroom.kv.get', { chatroomId: 'live_1001', keys: ['announcement'] });
const all = await app.openApi('chatroom.kv.get', { chatroomId: 'live_1001' });
// 删除公告
await app.openApi('chatroom.kv.remove', { chatroomId: 'live_1001', key: 'announcement' });订阅 chatroom.kv_changed 时,载荷包含房间与键的变更信息;把公告同步到你自己的直播控制台或推流侧,不要依赖客户端轮询 KV。
KV 不做历史版本,也没有 CAS/事务;并发写入以最后一次为准。需要强一致的计数请用业务库,不要用 KV 做库存扣减。
保活与心跳
聊天室有两套“心跳”,作用完全不同,不要混用:
| 机制 | 参数 / 行为 | 用途 |
|---|---|---|
| WS 心跳 | 客户端每 30s 发送 ping;服务端 90s 无活动判定空闲;要求 pong;连续 60s 未收到 pong 触发重连 | 判断一个 WebSocket 会话是否还活着;断连即离开房间 |
| 聊天室保活 | chatroom.keepalive:把 destroyAt 置为当前时间 + 3 个月(90 天) | 防止空闲销毁;只对 idle 策略生效 |
| 保活查询 / 移除 | chatroom.keepalive.get(返回应用下已保活的房间列表)/ chatroom.keepalive.remove | 运营排查与释放保活 |
| 重连自动补 join | SDK 在重连成功后自动重新加入已加入的聊天室(10 秒超时 3002) | 避免重连后“人在房间但收不到消息” |
// 长期活动房间:保活 3 个月,避免空闲销毁
await app.openApi('chatroom.keepalive', { chatroomId: 'live_1001' });
// 查询应用下当前处于保活状态的聊天室
const { chatroomIds } = await app.openApi('chatroom.keepalive.get', {});
// 释放保活:恢复空闲销毁计时(下次活动起重新计算)
await app.openApi('chatroom.keepalive.remove', { chatroomId: 'live_1001' });3 个月到期后房间仍会被空闲销毁;长期房间应在到期前重新调用 chatroom.keepalive,或在业务侧用定时任务续期。
消息优先级
聊天室支持配置“低优先级消息类型”清单。服务端只存储与查询该配置,当前投递路径不会据此改变发送顺序或丢弃消息;它的用途是让客户端/业务侧按同一份配置做渲染降级(例如高并发时折叠图片、文件、位置等次要消息)。
| 项 | 值 |
|---|---|
| 允许的类型 | text / image / file / voice / location / custom / recall / system / quote(共 9 种,大小写不敏感;未知类型返回 1001) |
| 写入语义 | chatroom.message.priority.set 全量覆盖(不是增量),重复类型自动去重 |
| 查询 | chatroom.message.priority.list 返回 {chatroomId, lowPriorityTypes} |
| 前置条件 | 房间必须存在且未销毁;否则 1012 / 1013 |
// 高并发直播间:图片/文件/位置按低优先级渲染(配置全量覆盖)
await app.openApi('chatroom.message.priority.set', {
chatroomId: 'live_1001',
messageTypes: ['image', 'file', 'location'],
});
const { lowPriorityTypes } = await app.openApi('chatroom.message.priority.list', {
chatroomId: 'live_1001',
});
console.log(lowPriorityTypes); // ['image','file','location']低优先级配置不改变服务端投递、不限频、不丢消息。真正的频率约束是每用户每分钟发送上限(120 条,超出 1008)与消息体 5KB(1007)。
白名单、禁言与封禁
房间级治理有四种状态,优先级从高到低:白名单(豁免)> 封禁(踢出且不可再加入)> 全员禁言 > 单人禁言。白名单用户不受封禁、禁言与全员禁言限制。
| 能力 | Open API | 参数 | 行为 |
|---|---|---|---|
| 单人禁言 / 解禁 | chatroom.mute | {chatroomId, userId, mutedUntil} | mutedUntil 为 epoch ms;0 表示解禁;房主/管理员可操作 |
| 封禁 / 解封 | chatroom.ban | {chatroomId, userId, banned} | 封禁后立即踢出且不可再加入;解封后可重新加入 |
| 封禁列表 | chatroom.ban.list | {chatroomId, offset?, limit?} | 分页;time 为解禁时间(UTC),空串表示未设置时长 |
| 禁言列表 | chatroom.mute.list | {chatroomId, offset?, limit?} | 仅返回未到期记录 |
| 全员禁言 | chatroom.mute-all / chatroom.mute-all.get | {chatroomId, muted, operatorId?} | 房主/管理员可操作;白名单成员不受限 |
| 白名单 | chatroom.whitelist.add/remove/list | {chatroomId, userIds} | 豁免封禁/禁言/全员禁言;随房间销毁清理 |
| 全局禁言 | chatroom.globalMute.set/remove/list | {userIds ≤20, minute 1..43200} | 应用级:约束该用户在所有聊天室的客户端发送 |
| 消息类型白名单 | chatroom.messageWhitelist.add/list/remove | {objectnames ≤20 种,≤32 字符} | 应用级共享;重复添加/移除幂等 |
// 房主/管理员:禁言 10 分钟 + 全员禁言 + 白名单豁免
const mutedUntil = Date.now() + 10 * 60 * 1000;
await app.openApi('chatroom.mute', { chatroomId: 'live_1001', userId: 'spammer_1', mutedUntil });
await app.openApi('chatroom.mute-all', { chatroomId: 'live_1001', muted: true, operatorId: 'anchor_1' });
// 运营号/管理员进白名单:不受禁言/封禁/全员禁言影响
await app.openApi('chatroom.whitelist.add', { chatroomId: 'live_1001', userIds: ['ops_1', 'anchor_1'] });
// 封禁恶意用户(立即踢出,不可再加入)
await app.openApi('chatroom.ban', { chatroomId: 'live_1001', userId: 'spammer_1', banned: true });禁言/封禁/全员禁言要求操作者是房主或管理员,否则 1003;目标用户不在房间返回 1014;房间不存在 1012、已销毁 1013。
被禁言用户发送消息会收到 1009(群/聊天室禁言统一语义);被封禁用户无法加入,返回 1014。
全局禁言只约束客户端发送;服务端 Open API 以用户身份发消息不受此限制(运营需要可控补发时才有意义)。
客户端接入(Web / Android / Flutter)
客户端只负责“进房、收消息、离房、查在线成员”。以下代码使用各端真实绑定方法名与事件名,可直接对照 API 参考核对签名。
Web(浏览器 ES Module)
import { createClient, ConversationType, Events } from '@ni-paas/im-web';
const client = createClient({ appkey: 'YOUR_APP_KEY', serverUrl: 'http://127.0.0.1:8080' });
await client.connect(TOKEN);
client.addEventListener(Events.CHATROOM_EVENT, (event) => {
// kind: joined | left | kicked | destroyed | member_changed
console.log(event.chatroomId, event.kind, event.onlineCount);
});
const room = await client.createChatroom({ chatroomId: 'live_1001', name: '新品发布会', owner: 'anchor_1' });
await client.joinChatroom(room.chatroomId);
client.addEventListener(Events.MESSAGES, ({ messages }) => {
for (const message of messages) {
if (message.conversationType === ConversationType.CHATROOM) renderDanmaku(message);
}
});
// 聊天室消息:仅广播在线成员,无 ack、无历史
await client.sendTextMessage(ConversationType.CHATROOM, 'live_1001', '主播好!');
const { onlineCount, members } = await client.getChatroomInfo('live_1001').then(async (info) => ({
onlineCount: info.onlineCount,
members: await client.getChatroomMembers('live_1001', { offset: 0, limit: 50 }),
}));
await client.leaveChatroom('live_1001');Android(Kotlin,回调式)
val client = IMClient.init(applicationContext, IMClientOptions(appkey = "YOUR_APP_KEY", serverUrl = "http://127.0.0.1:8080"))
client.connect(token, object : ConnectCallback {
override fun onSuccess(userId: String) {
client.addEventListener(IMClientEvent.CHATROOM_EVENT) { event ->
// event.kind: joined / left / kicked / destroyed / member_changed
Log.d("chatroom", "${event.chatroomId} ${event.kind}")
}
client.createChatroom(CreateChatroomOptions(chatroomId = "live_1001", name = "新品发布会", owner = "anchor_1")) { created ->
client.joinChatroom(created.chatroomId, object : ResultCallback<Chatroom> {
override fun onSuccess(room: Chatroom) = Unit
override fun onError(error: IMError) = Log.e("chatroom", "${error.code} ${error.message}")
})
}
}
override fun onError(error: IMError) = Log.e("chatroom", "${error.code} ${error.message}")
})Flutter(Dart,Future)
final engine = await RCIMIWEngine.create(
'YOUR_APP_KEY',
RCIMIWEngineOptions.create(naviServer: 'http://127.0.0.1:8080'),
);
await client.connect(token);
client.addEventListener('CHATROOM_EVENT', (event) {
// event.kind: joined / left / kicked / destroyed / member_changed
debugPrint('${event.chatroomId} ${event.kind}');
});
final room = await client.createChatroom(chatroomId: 'live_1001', name: '新品发布会', owner: 'anchor_1');
await client.joinChatroom(room.chatroomId);
await client.sendTextMessage(3, 'live_1001', '主播好!'); // 3 = ConversationType.CHATROOM
final info = await client.getChatroomInfo('live_1001');
final page = await client.getChatroomMembers('live_1001', offset: 0, limit: 50);
await client.leaveChatroom('live_1001');限制与错误码
| 限制项 | 值 | 来源 |
|---|---|---|
| 单房间在线人数 | 5,000 | protocol.limits.chatroomMaxOnline |
| 消息体 | ≤5KB(5120 字节),文本 ≤5,000 字符 | protocol.limits.messageBytes / textChars |
| 发送频率 | 每用户 120 条/分钟(超出 1008) | protocol.limits.sendRatePerMinute |
| 默认空闲销毁 | 1,800,000ms(30 分钟) | protocol.chatroomSemantics.idleDestroyMs |
| 保活时长 | 90 天(3 个月) | 服务端常量 KEEPALIVE_MS |
| 低优先级消息类型 | 9 种允许值;全量覆盖 | Open API 校验 |
| 全局禁言批量 | ≤20 用户;1–43,200 分钟 | chatroom.globalMute.set |
| 存在性批量查询 | ≤1,000 用户 | chatroom.members.existsBatch |
| 自定义属性批量 | ≤100 对 / ≤100 key | chatroom.entry.batch.* |
| 消息类型白名单 | 应用级 ≤20 种,每种 ≤32 字符 | chatroom.messageWhitelist.* |
| 错误码 | 名称 | 典型场景 |
|---|---|---|
1001 | INVALID_PARAMETER | 参数缺失/类型错误;低优先级类型不在允许清单;批量超限 |
1003 | FORBIDDEN | 非房主/管理员执行禁言、封禁、全员禁言 |
1008 | RATE_LIMITED | 发言超过每分钟 120 条(客户端发送经 WS 同样受限) |
1009 | GROUP_MUTED | 被禁言用户尝试发送 |
1010 | SENSITIVE_WORD | 内容命中敏感词(按控制台处置动作) |
1012 | CHATROOM_NOT_FOUND | 房间不存在或从未创建 |
1013 | CHATROOM_CLOSED | 房间已销毁(含空闲/定时到期) |
1014 | NOT_CHATROOM_MEMBER | 未加入房间就发言 / 封禁后尝试加入 / 禁言目标不在房间 |
1015 | MEMBERSHIP_LIMIT | 创建房间时成员数超限(批量初始化场景) |
1021 | SERVICE_NOT_ACTIVATED | 应用未开通聊天室服务 |
先看错误码:1012/1013 是房间生命周期问题,1014 是成员关系问题,1003 是角色问题。
房间“突然没了”优先检查销毁策略与保活:idle 默认 30 分钟,fixed 到期即销毁;销毁后 KV/白名单不可恢复。
客户端收不到消息但 REST 显示在线:确认 joinChatroom 的 chatroom_joined 是否成功(10 秒超时 3002),以及 CHATROOM_EVENT 监听是否在 connect 之前注册。