CommIM 第三方接入文档

CommIM 是一套完整的即时通讯微服务系统,支持单聊、群聊、好友管理、文件传输等核心 IM 能力。本文档面向第三方开发者,提供从零接入 CommIM 服务的完整指南。

适用对象:第三方公司开发人员,需将自有客户端接入 CommIM 即时通讯服务。
文档版本:v1.0  |  更新日期:2026-05-24

即时通讯

单聊/群聊,支持文本、图片、语音、视频、文件、自定义消息

用户认证

注册、登录、验证码、密码管理,支持自有账号体系映射

好友系统

好友申请/审批、好友列表、备注、黑名单

群组系统

创建群、邀请入群、群管理、群历史消息

文件上传

服务端签名直传 OSS / STS 临时凭证上传

多端同步

支持多端同时在线,消息实时同步,离线消息自动推送

音视频通话

一对一语音/视频通话,WebRTC P2P + TURN 中继

核心能力

能力说明
用户认证注册、登录、验证码、密码管理
即时消息单聊/群聊,支持文本、图片、语音、视频、文件、自定义消息
好友系统好友申请/审批、好友列表、备注、黑名单
群组系统创建群、邀请入群、申请入群、群管理、群历史消息
文件上传服务端签名直传 OSS / STS 临时凭证上传
离线消息自动推送离线消息,支持消息回执和已读状态
多端同步支持多端同时在线,消息实时同步
音视频通话一对一语音/视频通话,WebRTC P2P + TURN 中继,自建 TURN 服务器

架构与通信协议

整体架构

┌─────────────┐     HTTP/HTTPS      ┌──────────────────┐     gRPC      ┌──────────────┐
│  第三方客户端  │ ──────────────────→ │ service_commim_gate│ ───────────→ │ 后端 gRPC 服务 │
│  (Web/App/PC) │                     │   (API 网关)       │              │ (UAA/Friend/  │
│              │ ←────────────────── │                    │ ←─────────── │ Group/...)    │
└──────┬───────┘     HTTP Response    └──────────────────┘    gRPC Rsp   └──────────────┘
       │
       │ WebSocket (Protobuf 二进制)
       ▼
┌──────────────┐
│ service_gate │  长连接网关,负责消息实时推送、心跳、认证
└──────────────┘

双通道通信模型

CommIM 采用 HTTP + WebSocket 双通道 通信模型:

通道协议用途方向
HTTP HTTP JSON over POST /rpc/:service/:method 请求-响应式操作(登录、注册、查询好友列表等) 客户端主动调用
WS Protobuf 二进制 实时消息推送、好友申请通知、群变更通知等 服务端主动推送 + 客户端发送消息

HTTP 通用请求格式

HTTP
POST http://{gate_host}:11260/rpc/{service}/{method}
Content-Type: application/json
token: {user_token}
x-pcd: {base64_encoded_pbcommdata}
x-app-secret: {app_secret}

通用响应格式

JSON
{
  "code": 0,
  "message": "success",
  "data": { ... }
}
字段类型说明
codeint0 表示成功,非 0 表示错误
messagestring错误描述或 "success"
dataobject业务响应数据

WebSocket Protobuf 协议

WebSocket 通道使用 Protobuf 二进制编码,消息封装在 PBMessage 结构中:

Protobuf
message PBMessage {
  uint32 version = 1;
  uint32 checkCode = 2;
  uint32 errCode = 3;
  string service = 4;       // 目标服务名
  string hashKey = 5;       // 路由键(通常为用户 IM ID)
  PBCommData pbCommData = 6;
  map<string, string> opts = 7;
  string pbName = 8;        // 具体消息的 protobuf 消息名
  bytes pbData = 9;         // 具体消息的 protobuf 编码数据
  string errDesc = 10;
}
接入步骤:
  1. 从服务端获取 client_pb.json(Protobuf 定义文件)
  2. 使用 protobufjs 库加载定义,编解码消息
  3. 连接 WebSocket,设置 binaryType = 'arraybuffer'
  4. 收到二进制数据后,先用 PBMessage 解码外层,再根据 pbName 解码内层消息

快速开始

1. 申请应用凭证

接入 CommIM 前需向平台方申请以下凭证:

凭证说明示例
appId应用唯一标识,由平台分配10001
secret应用密钥,用于接口签名校验dAEmUyk6pouC...

2. 获取服务地址

HTTP
POST http://{gate_host}:11260/rpc/pb_grpc_commim_uaa.UAA/FetchEndPoint
Content-Type: application/json
x-pcd: {base64_pcd}

3. 用户登录获取 Token

HTTP
POST /rpc/pb_grpc_commim_uaa.UAA/Login
Content-Type: application/json

{
  "phone": "13800138000",
  "password": "mypassword123"
}

响应中关键返回值:

字段说明
tokenUAA JWT Token,用于后续 HTTP API 调用的鉴权
imTokenIM Token,用于 WebSocket 长连接认证
imIdIM 用户 ID,系统内唯一标识,后续所有操作均使用此 ID

4. 获取 WebSocket 连接地址

HTTP
GET http://{allocator_host}:11000/Allocater/GetWsGate?userId={imId}

5. 建立 WebSocket 连接并登录

JavaScript
const ws = new WebSocket('ws://xxx.xxx.xxx.xxx:11030/ws');
ws.binaryType = 'arraybuffer';

ws.onopen = () => {
  sendLoginReq(ws, imToken, imId, appId);
};

ws.onmessage = (event) => {
  const head = PBMessage.decode(new Uint8Array(event.data));
  handleMessage(head);
};
完成以上 5 步后,即可开始消息收发。接下来请阅读认证授权和连接管理章节了解详细实现。

UAA 注册/登录

适用于客户端直接使用 CommIM 内置账号体系的场景。

发送验证码

HTTP
POST /rpc/pb_grpc_commim_uaa.UAA/SendPhoneCode
Content-Type: application/json

{
  "PhoneNo": "13800138000"
}

约束:同一手机号 60 秒内只能发送一次,验证码有效期 20 分钟。

HTTP
POST /rpc/pb_grpc_commim_uaa.UAA/SendEmailCode
Content-Type: application/json

{
  "EmailAddr": "user@example.com"
}

验证码有效期 30 分钟。

用户注册

HTTP
POST /rpc/pb_grpc_commim_uaa.UAA/Signup
Content-Type: application/json

{
  "username": "zhangsan",
  "phone": "13800138000",
  "password": "mypassword123",
  "code": 456789,
  "nickname": "张三"
}
字段类型必填说明
usernamestring推荐用户名,6-30 个字符
phonestring三选一手机号码
emailstring三选一邮箱地址
passwordstring密码,至少 6 字符
codeint32验证码(开发环境可用万能码 918666
nicknamestring昵称
avatarstring头像 URL

用户登录

JSON
POST /rpc/pb_grpc_commim_uaa.UAA/Login

{
  "phone": "13800138000",
  "password": "mypassword123"
}
JSON
POST /rpc/pb_grpc_commim_uaa.UAA/Login

{
  "phone": "13800138000",
  "code": 456789
}

登录/注册响应

JSON
{
  "code": 0,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "imToken": "im_token_xxxxxxxx",
    "info": {
      "userId": "10086",
      "username": "zhangsan",
      "imId": 10086,
      "phone": "13800138000",
      "nickname": "张三",
      "avatar": "https://default.example.com/avatar_m.png",
      "gender": 1
    }
  }
}

获取/更新用户信息

HTTP
POST /rpc/pb_grpc_commim_uaa.UAA/UserInfo
Content-Type: application/json
token: {user_token}

{}
JSON
POST /rpc/pb_grpc_commim_uaa.UAA/UpdateUserInfo

{
  "userId": "10086",
  "info": {
    "nickname": "新昵称",
    "sign": "这是我的新签名"
  },
  "keys": ["nickname", "sign"]
}
key说明额外要求
nickname修改昵称
avatar修改头像
gender修改性别
sign修改签名
freeAddMeFriend自由加好友开关
phone修改手机号需要 info.code + info.passWord
email修改邮箱需要 info.code + info.passWord
passWord修改密码需要 info.code 验证码,新密码 6-32 字符

第三方应用集成

适用于第三方已有自己的用户体系,希望将自有用户映射到 CommIM 的场景。

接入流程

1
第三方用户在自有系统登录成功

用户在第三方应用完成身份认证

2
调用 GetIMID 接口

获取该用户对应的 IM ID 和 Token,首次接入系统自动分配 IM ID

3
使用返回的 Token 建立 WebSocket 连接

无需用户重新注册,无缝接入 IM 能力

4
开始消息收发

完整的 IM 功能即刻可用

获取用户 IM ID 和 Token

HTTP
GET http://{inter_api_host}:11190/GetIMID?appId={appId}&secret={secret}&userId={userId}
参数类型必填说明
appIdint64应用 ID(平台分配)
secretstring应用密钥
userIdstring第三方系统的用户 ID
JSON Response
{
  "result": 0,
  "msg": "成功",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "userIds": {
      "appId": 10001,
      "appUserId": "user_238",
      "imUserId": 50001,
      "lastLoginTime": "2026-05-24T10:00:00Z"
    }
  }
}

同步用户信息

HTTP
PUT http://{inter_api_host}:11190/userInfo/{imId}
Content-Type: application/json

{
  "nickname": "张三",
  "avatar": "https://your-cdn.com/avatar/238.jpg",
  "sign": "这是我的签名"
}

完整接入示例

JavaScript
async function integrateCommIM(appId, secret, userId) {
  const imidResp = await fetch(
    `http://inter-api-host:11190/GetIMID?appId=${appId}&secret=${secret}&userId=${userId}`
  );
  const imidData = await imidResp.json();
  const { token, userIds } = imidData.data;
  const imId = userIds.imUserId;

  const wsResp = await fetch(
    `http://allocator-host:11000/Allocater/GetWsGate?userId=${imId}`
  );
  const wsData = await wsResp.json();
  const wsUrl = wsData.data;

  const ws = new WebSocket(wsUrl);
  ws.binaryType = 'arraybuffer';

  ws.onopen = () => {
    sendLoginReq(ws, token, imId, appId);
  };

  ws.onmessage = (event) => {
    handleMessage(new Uint8Array(event.data));
  };
}

x-pcd 请求头

x-pcd 是 CommIM 的上下文传递机制,值为 PBCommData JSON 经 Base64 编码后的字符串。

字段类型说明
srcIdint64发起者 IM ID
aimIdint64目标 IM ID
groupIdint64群 ID(群操作时使用)
appIdint64应用 ID
appUserIdstring应用层用户 ID
srcClientTypeint32客户端类型:0=未知, 1=手机, 2=H5, 3=PC
timeint64当前时间戳(秒)
needReadReceiptbool是否需要已读回执
msgSnint64消息序列号

生成 x-pcd 示例

JavaScript
function makeXPcd(appId, appUserId, imId, aimId, srcClientType = 2) {
  const pcd = {
    srcId: imId || 0,
    aimId: aimId || 0,
    srcClientType,
    appId,
    appUserId: appUserId || '',
    time: Math.floor(Date.now() / 1000),
  };
  return btoa(JSON.stringify(pcd));
}

连接管理

连接流程

1
调用 UAA Login → 获取 token + imToken + imId
2
调用 Allocator GetWsGate → 获取 WebSocket 地址
3
建立 WebSocket 连接
4
发送 LoginReq(携带 imToken)→ 收到 LoginRsp
5
连接建立成功,开始心跳和消息收发

发送登录请求

Protobuf
// pbName: pb_msg_gate.LoginReq
{
  token: imToken,
  token_type: 2,                     // UNI_USER = 2(推荐)
  clientInfo: {
    modelType: 0,
    packageName: 'com.yourapp.name',
    systemVersion: '1.0.0',
    phoneModels: 'Web',
    appVersion: '1.0.0',
    systemName: 'Browser',
    identifier: 'web-xxxxxx',
    xChannel: 'web'
  },
  deviceToken: '',
  forceLogin: true,                  // 首次登录填 true,静默重连填 false
  version: 1
}
Token Type说明
JAVA0旧版 Java 下发的 token
IM1IM 下发的重连 token
UNI_USER2统一认证 token(推荐)

登录响应

Protobuf
// pbName: pb_msg_gate.LoginRsp
{
  result: 0,                         // 0=成功,非0=失败
  reconnectToken: 'xxxx',            // 重连 token
  msg_sn: 12345,
  app_id: 10001,
  app_user_id: '10086',
  deviceToken: 'device_xxx'
}

心跳保活

连接建立后,客户端需每 10 秒发送一次心跳:

Protobuf
// pbName: pb_pub.HeartBeat
// 发送: { type: 0, state: 0 }    PING
// 回复: { type: 0, state: 1 }    PANG
重要:如果超过 30 秒未收到服务端任何消息(包括心跳回复),应判定连接断开,触发重连。

断线重连

1
检测到连接断开
2
重新获取 WebSocket 地址(调用 Allocator GetWsGate)
3
建立新 WebSocket 连接
4
发送 LoginReq,token 使用 reconnectToken,token_type 设为 IM(1),forceLogin 设为 false
5
收到 LoginRsp 后,重连成功,请求离线消息

被踢下线

Protobuf
// pbName: pb_msg_gate.KickOffUser
{
  optUId: 10001,
  aimUId: 10086,
  reason: 21,
  desc: '账号在其他设备登录'
}

客户端收到后应断开连接,提示用户重新登录。

消息收发

消息类型

类型说明data 字段
0TEXT文本消息
1PIC图片消息图片 URL
2VIDEO视频消息视频 URL
3AUDIO语音消息语音 URL
7CUSTOMIZE自定义消息自定义 JSON
8FILE文件消息文件 URL
9RECALL撤回消息原消息信息
10RED_PACKET红包消息红包 ID

发送单聊消息

Protobuf
// PBMessage 封装:
{
  service: '',
  hashKey: String(targetImId),
  pbName: 'pb_msg_gate.ChatText',
  pbCommData: {
    srcId: myImId,
    aimId: targetImId,
    appId: appId,
    appUserId: String(myImId),
    time: 0,
    srcClientType: 2,
    needReadReceipt: true
  }
}

// ChatText 内容:
{
  aim_user_id: targetImId,
  chat_type: 0,
  data: '',
  text: '你好,这是一条测试消息',
  exp: {}
}

发送群聊消息

Protobuf
// PBMessage 封装:
{
  service: 'group',
  hashKey: String(groupId),
  pbName: 'pb_msg_group.GroupChat',
  pbCommData: {
    srcId: myImId,
    groupId: groupId,
    appId: appId,
    time: 0,
    srcClientType: 2,
    atList: [10002, 10003]
  }
}

// GroupChat 内容:
{
  aim_user_id: 0,
  chat_type: 0,
  data: '',
  text: '大家好!',
  exp: {}
}

接收消息

WebSocket 收到消息后,根据 pbName 区分消息类型:

pbName说明
pb_msg_gate.ChatText单聊消息
pb_msg_group.GroupChat群聊消息
pb_pub.MsgReceipt消息回执
pb_msg_gate.KickOffUser被踢下线
pb_pub.HeartBeat心跳回复
pb_msg_friend.ApplyReq好友申请通知
pb_msg_friend.ApplyAnswerRsp好友申请应答通知
pb_msg_offlineMsg.ReadOfflineMsgRsp离线消息

接收消息示例

JavaScript
const head = PBMessage.decode(new Uint8Array(event.data));
const pbName = head.pbName;

if (pbName === 'pb_msg_gate.ChatText') {
  const ChatText = pbRoot.lookupType('pb_msg_gate.ChatText');
  const msg = ChatText.decode(head.pbData);
  const pcd = head.pbCommData;

  console.log('收到消息:', {
    from: pcd.srcId,
    text: msg.text,
    chatType: msg.chat_type,
    data: msg.data,
    time: pcd.time,
    msgSn: pcd.msgSn
  });
}

自定义消息与撤回

JSON
{
  "aim_user_id": 10002,
  "chat_type": 7,
  "data": "{\"type\":\"card\",\"title\":\"名片分享\",\"userId\":10003}",
  "text": "",
  "exp": {}
}
Protobuf
// pbName: pb_pub.MsgRecallReq
{
  msgSn: 12345,
  msgOwnerId: 10086
}

消息回执与状态

消息状态流转

INIT(0) → SEND(2) → RECEIVED(3) → READ(4) → END(5)
   │
   └→ FAULT(1)          发送失败
   └→ IN_BLACK_LIST(6)  被拉黑
   └→ LIMITED(7)        被限制

发送已读回执

Protobuf
// pbName: 'pb_pub.MsgReceipt'
{
  isAtMe: false,
  state: 4,                        // MSG_STATE_READ = 4
  time: 0
}
// pbCommData.aimId = 消息发送者 IM ID
// pbCommData.msgSn = 原消息的 msgSn

离线消息

用户上线(WebSocket 登录成功)后,服务端自动推送离线消息。

Protobuf
// pbName: 'pb_msg_offlineMsg.ReadOfflineMsgRsp'
{
  msgNum: 5,
  msgList: [
    {
      srcUserid: 10002,
      aimUserid: 10086,
      chatType: 0,
      text: '你好',
      data: '',
      sn: 12345,
      time: 1700000000
    }
  ]
}

好友管理

好友相关操作通过 WebSocket 发送请求并等待响应(请求-响应模式)。

申请添加好友

Protobuf
// service: 'friend', hashKey: String(targetImId)
// pbName: 'pb_msg_friend.ApplyReq'
{
  msg: '你好,我是张三'
}
// pbCommData.aimId = 目标用户 IM ID

应答好友申请

Protobuf
// pbName: 'pb_msg_friend.ApplyAnswerReq'
{
  agree: true                  // true=同意, false=拒绝
}

其他好友操作

操作pbName说明
获取好友列表pb_msg_friend.FriendsReq空请求,返回好友列表
删除好友pb_msg_friend.DeleteFriendReqaimId 设为要删除的好友 IM ID
拉黑用户pb_msg_friend.AddBlackListReqaimId 设为目标 IM ID
取消拉黑pb_msg_friend.RemoveBlackListReqaimId 设为目标 IM ID
判断黑名单pb_msg_friend.IsInBlackListReq返回 0=不在, 1=在我黑名单, 2=在对方黑名单

群组管理

创建群聊

Protobuf
// service: 'group', hashKey: String(myImId)
// pbName: 'pb_msg_group.CreateGroupReq'
{
  name: '项目讨论组',
  memberIds: [10086, 10087, 10088],
  memberCountLimit: 200,
  groupType: 0                  // 0=普通群, 1=聊天室, 2=频道, 3=系统通知
}

群操作一览

操作pbName关键参数
邀请入群pb_msg_group.InviteReqinviteeIds, shareMsgCount: -1
应答入群邀请pb_msg_group.InviteAnswerReqagree: true/false
申请入群pb_msg_group.ApplyReqgroupId
审批入群申请pb_msg_group.ApplyAnswerReqagree
获取群列表pb_msg_group.GroupsReq空请求
获取群详情pb_msg_group.GroupDetailReqgroupId
获取群成员pb_msg_group.MembersReqpage, pageSize(最大 100)
退出群聊pb_msg_group.QuitReqgroupId
修改群名称pb_msg_group.EditNameReqname
修改群公告pb_msg_group.EditNoticeReqnotice
设置群禁言pb_msg_group.SetMemberChatBannedStatusReqbannedStatus: 0/1/2
添加群管理员pb_msg_group.AddAdminsReqmemberIds
踢出群成员pb_msg_group.KickoutReqaimId
解散群组pb_msg_group.DisbandGroupsReqgroupIds
群历史消息pb_msg_group.GroupHistoryMsgReqpage, pageSize, filterNew

文件上传

CommIM 支持两种文件上传方式,上传后获取 URL,将 URL 放入消息的 data 字段发送。

方式一:服务端签名直传(推荐)

HTTP
GET http://{oss_host}:11070/uploadToken
Authorization: {token}
UserID: {imId}

响应返回阿里云 OSS PostObject 签名(含 policy、signature、OSSAccessKeyId、key、host),客户端使用签名直传 OSS。

方式二:STS 临时凭证上传

HTTP
GET http://{oss_host}:11070/getStsToken?bucketName=commim
Authorization: {token}
UserID: {imId}

返回 STS 临时凭证(AccessKeyId、AccessKeySecret、SecurityToken、EndPoint、Bucket),使用 STS 凭证初始化 OSS Client SDK 上传。

发送图片/文件消息

JSON
{
  "aim_user_id": 10002,
  "chat_type": 1,
  "data": "https://your-bucket.oss-cn-hangzhou.aliyuncs.com/commim/image_123.jpg",
  "text": "",
  "exp": {"width": "800", "height": "600"}
}

音视频通话

CommIM 提供一对一语音/视频通话能力,基于 WebRTC 实现端到端音视频通信,通过 IM 消息通道传输信令,自建 TURN 服务器实现 NAT 穿透。

能力边界:仅支持一对一通话,不支持多人会议。仅从私聊发起,群聊不支持通话。同一用户同时只能有一路通话。
浏览器要求:Chrome 72+、Firefox 68+、Safari 14.1+、Edge 79+(需 HTTPS 环境)

通话架构

┌──────────────────────────────────────────────────────────────┐
│                    第三方客户端 (Web/App)                      │
│                                                              │
│  ┌────────────┐    ┌───────────────┐    ┌────────────────┐  │
│  │ 通话 UI     │───▶│  通话状态管理  │───▶│ WebRTC Handler │  │
│  └────────────┘    └───────┬───────┘    └───────┬────────┘  │
│                            │                     │           │
│                    ┌───────┴───────┐             │           │
│                    │  信令收发模块  │             │           │
│                    └───────┬───────┘             │           │
└────────────────────────────┼─────────────────────┼───────────┘
                             │                     │
                  IM 消息通道 (WebSocket)      WebRTC 媒体通道
                  ChatType.CUSTOMIZE=7        STUN/TURN + P2P
                             │                     │
┌────────────────────────────┼─────────────────────┼───────────┐
│                     CommIM 服务端                │           │
│                             │                     │           │
│  ┌──────────────┐  ┌───────┴──────────┐  ┌──────┴──────┐   │
│  │ service_gate │  │ service_out_api  │  │   coturn    │   │
│  │  (消息路由)   │  │  (TURN凭证签发)  │  │ (TURN服务)  │   │
│  └──────────────┘  └──────────────────┘  └─────────────┘   │
└──────────────────────────────────────────────────────────────┘

接入流程总览

步骤操作说明
1获取 TURN 凭证调用 /Turn/GetCredentials 获取 ICE 服务器配置
2初始化 WebRTC使用凭证创建 RTCPeerConnection
3发送通话邀请通过 IM 自定义消息发送 call_invite 信令
4SDP 协商交换 SDP Offer/Answer
5ICE 交换双向交换 ICE Candidate
6建立 P2P 连接直连或通过 TURN 中继
7通话结束发送 call_hangup 信令,发送通话记录消息

TURN 凭证签发 API

第三方客户端在发起通话前,需先获取 TURN 临时凭证。凭证基于 HMAC-SHA1 签名,与用户绑定,有效期 24 小时。

GET /Turn/GetCredentials 需登录

获取 TURN 服务器临时凭证,用于 WebRTC ICE 配置。凭证与当前登录用户绑定,有效期由服务端配置(默认 24 小时)。

请求头

Header类型必填说明
Authorizationstring用户登录 Token
UserIDstring用户 IM ID

响应体

JSON
{
  "result": 0,
  "msg": "success",
  "data": {
    "urls": [
      "turn:121.196.153.186:3478?transport=udp",
      "turn:121.196.153.186:3478?transport=tcp",
      "turns:121.196.153.186:5349?transport=tcp",
      "turns:121.196.153.186:443?transport=tcp"
    ],
    "username": "1719583640:10086",
    "credential": "a3N5V2xhRmFqZTg4Nw==",
    "ttl": 86400
  }
}

响应字段说明

字段类型说明
urlsstring[]TURN/STUN 服务器 URL 列表,包含 UDP/TCP/TLS 多种传输方式
usernamestring临时用户名,格式 {expiry_timestamp}:{userId}
credentialstringHMAC-SHA1 签名凭证(Base64 编码)
ttlnumber凭证有效时长(秒),默认 86400(24 小时)

凭证签发算法

伪代码
expiry = current_unix_timestamp + ttl
username = "{expiry}:{userId}"
credential = base64(hmac_sha1(secret, username))
安全提示:凭证与用户绑定,不可跨用户使用。建议客户端缓存凭证至内存(不写入 localStorage),在 TTL 到期前 5 分钟自动刷新。用户登出时清除缓存。
GET /Turn/GetConfig 无需登录

获取 TURN 服务器配置信息(不含凭证),用于展示或诊断。

响应体

JSON
{
  "result": 0,
  "msg": "success",
  "data": {
    "host": "121.196.153.186",
    "port": 3478,
    "tlsPort": 5349,
    "altPort": 443,
    "realm": "commim.turn.fuhui-zhilian.com"
  }
}

信令协议

通话信令通过 IM 自定义消息通道传输,使用 chat_type = 7(CUSTOMIZE),data 字段携带 commim_call: 前缀的 JSON 数据。

信令类型

信令类型方向说明
call_invite主叫 → 被叫发起通话邀请
call_answer被叫 → 主叫接听通话
call_reject被叫 → 主叫拒绝通话
call_cancel主叫 → 被叫取消呼叫
call_hangup双向结束通话
call_sdp_offer主叫 → 被叫WebRTC SDP Offer
call_sdp_answer被叫 → 主叫WebRTC SDP Answer
call_ice_candidate双向ICE 候选交换

信令数据结构

TypeScript
interface CallSignal {
  type: CallSignalType       // 信令类型,见上表
  callId: string             // 通话唯一标识,格式: call_{timestamp}_{random6}
  mediaType: CallMediaType   // 'audio' | 'video'
  sdp?: string               // SDP 内容(仅 sdp_offer/sdp_answer 携带)
  candidate?: RTCIceCandidateInit  // ICE 候选(仅 ice_candidate 携带)
  timestamp: number          // 毫秒时间戳
}

信令编码格式

编码规则
data 字段 = "commim_call:" + JSON.stringify(CallSignal)

// 示例:发起视频通话邀请
data = 'commim_call:{"type":"call_invite","callId":"call_1719500000_a3f2k1","mediaType":"video","timestamp":1719500000000}'

// 发送时 chat_type = 7 (CUSTOMIZE)
// exp 扩展字段: { "_callSignal": "1", "callType": "video", "callId": "call_1719500000_a3f2k1" }

信令消息识别

第三方客户端收到 chat_type = 7 的消息时,检查 data 字段是否以 commim_call: 开头:

JavaScript
function isCallSignal(data) {
  return data && data.startsWith('commim_call:');
}

function decodeCallSignal(data) {
  if (!isCallSignal(data)) return null;
  try {
    return JSON.parse(data.slice('commim_call:'.length));
  } catch {
    return null;
  }
}
重要:通话信令消息不应显示在聊天流中。识别到 commim_call: 前缀的消息后,应拦截并分发给通话模块处理,不进入聊天消息列表。

完整通话流程

正常通话(被叫接听)

主叫方 (Caller)                              被叫方 (Callee)
    │                                            │
    │  1. 获取 TURN 凭证                          │
    │  2. 初始化 RTCPeerConnection                │
    │  3. 获取本地媒体流                           │
    │                                            │
    │────── call_invite ────────────────────────▶│  4. 显示来电弹窗
    │                                            │  5. 播放振铃音
    │                                            │
    │◀────── call_answer ───────────────────────│  6. 用户点击接听
    │                                            │  7. 获取 TURN 凭证
    │                                            │  8. 初始化 RTCPeerConnection
    │  9. 创建 SDP Offer                          │
    │────── call_sdp_offer ─────────────────────▶│  10. 设置远端 Offer
    │                                            │  11. 创建 SDP Answer
    │◀────── call_sdp_answer ───────────────────│
    │  12. 设置远端 Answer                        │
    │                                            │
    │◀────── call_ice_candidate ────────────────│  ICE 交换
    │────── call_ice_candidate ─────────────────▶│  (双向持续)
    │                                            │
    │═══════════ P2P 音视频流 ═══════════════════│  通话中
    │                                            │
    │────── call_hangup ────────────────────────▶│  挂断
    │  发送 VIDEO_INVITE 消息                     │  发送 VIDEO_INVITE 消息

被叫拒绝

主叫方                                        被叫方
    │────── call_invite ──────────────────────▶│
    │◀────── call_reject ──────────────────────│  用户点击拒绝
    │  通话结束                                  │  通话结束

主叫取消

主叫方                                        被叫方
    │────── call_invite ──────────────────────▶│
    │  用户点击取消                               │
    │────── call_cancel ──────────────────────▶│
    │  通话结束                                  │  通话结束

WebRTC 集成指南

1. 获取 TURN 凭证并创建 PeerConnection

JavaScript
// 1. 获取 TURN 凭证
const credResp = await fetch('/api/Turn/GetCredentials', {
  method: 'GET',
  headers: {
    'Authorization': userToken,
    'UserID': String(imId),
  },
});
const credData = await credResp.json();

// 2. 创建 PeerConnection
const pc = new RTCPeerConnection({
  iceServers: [{
    urls: credData.data.urls,
    username: credData.data.username,
    credential: credData.data.credential,
  }],
});

// 3. 获取本地媒体流
const localStream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: mediaType === 'video',  // 'audio' | 'video'
});
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));

2. 监听事件

JavaScript
// 远端媒体流
pc.ontrack = (event) => {
  if (event.streams && event.streams[0]) {
    remoteVideoElement.srcObject = event.streams[0];
  }
};

// ICE 候选 → 通过信令发送给对方
pc.onicecandidate = (event) => {
  if (event.candidate) {
    sendSignal(peerImId, {
      type: 'call_ice_candidate',
      callId: currentCallId,
      mediaType: 'video',
      candidate: event.candidate.toJSON(),
      timestamp: Date.now(),
    });
  }
};

// 连接状态
pc.oniceconnectionstatechange = () => {
  switch (pc.iceConnectionState) {
    case 'connected':
    case 'completed':
      // 通话已建立
      break;
    case 'disconnected':
      // 连接断开,结束通话
      break;
    case 'failed':
      // ICE 连接失败
      break;
  }
};

3. 主叫方:创建 Offer

JavaScript
// 收到 call_answer 后执行
const offer = await pc.createOffer({
  offerToReceiveAudio: true,
  offerToReceiveVideo: true,
});
await pc.setLocalDescription(offer);

// 通过信令发送 SDP Offer
sendSignal(peerImId, {
  type: 'call_sdp_offer',
  callId: currentCallId,
  mediaType: 'video',
  sdp: offer.sdp,
  timestamp: Date.now(),
});

4. 被叫方:创建 Answer

JavaScript
// 收到 call_sdp_offer 后执行
await pc.setRemoteDescription(new RTCSessionDescription({
  type: 'offer',
  sdp: signal.sdp,
}));

const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);

// 通过信令发送 SDP Answer
sendSignal(peerImId, {
  type: 'call_sdp_answer',
  callId: currentCallId,
  mediaType: 'video',
  sdp: answer.sdp,
  timestamp: Date.now(),
});

5. 通话控制

JavaScript
// 静音/取消静音
function toggleAudio(enabled) {
  localStream.getAudioTracks().forEach(track => {
    track.enabled = enabled;
  });
}

// 开关摄像头
function toggleVideo(enabled) {
  localStream.getVideoTracks().forEach(track => {
    track.enabled = enabled;
  });
}

// 挂断
function hangup() {
  sendSignal(peerImId, {
    type: 'call_hangup',
    callId: currentCallId,
    mediaType: 'video',
    timestamp: Date.now(),
  });
  // 释放资源
  localStream.getTracks().forEach(track => track.stop());
  pc.close();
}

通话记录消息

通话结束后,双方应向聊天流发送 VIDEO_INVITE(chat_type=6)类型的消息,作为通话记录落库。

JSON
{
  "aim_user_id": 10002,
  "chat_type": 6,
  "text": "",
  "data": "{\"callType\":\"video\",\"duration\":332,\"result\":\"completed\"}"
}

通话记录 data 字段

字段类型说明
callTypestring通话类型:audio 语音 / video 视频
durationnumber通话时长(秒),未接通为 0
resultstring通话结果:completed 已完成 / missed 未接 / rejected 已拒绝 / cancelled 已取消

通话状态机

         ┌──────────┐
         │   idle   │  初始状态
         └────┬─────┘
              │
    ┌─────────┴──────────┐
    │                    │
    ▼                    ▼
┌──────────┐      ┌──────────┐
│ outgoing │      │ incoming │
│ (呼出中)  │      │ (来电中)  │
└────┬─────┘      └────┬─────┘
     │                  │
     │  call_answer     │  用户接听
     │                  │
     └────────┬─────────┘
              │
              ▼
        ┌──────────┐
        │connecting│  SDP/ICE 协商中
        └────┬─────┘
             │
             │ ICE connected
             │
             ▼
        ┌──────────┐
        │ connected│  通话中
        └────┬─────┘
             │
             │ hangup / disconnect / error
             │
             ▼
        ┌──────────┐
        │  ended   │  已结束(1.5秒后回到 idle)
        └──────────┘

超时与异常处理

场景检测方式处理策略
振铃超时30 秒无应答主叫方:call_cancel + 结束通话;被叫方:自动拒绝
连接超时connecting 阶段 15 秒结束通话,提示"连接超时"
媒体设备不可用getUserMedia 抛出异常结束通话,提示"无法获取媒体设备"
ICE 连接失败iceConnectionState = 'failed'结束通话,提示"连接失败"
网络断开iceConnectionState = 'disconnected'结束通话,提示"连接断开"
对方正在通话收到 call_reject结束通话,提示"对方已拒绝"
TURN 服务不可用凭证 API 返回非 200降级到公共 STUN 服务器

TURN 服务器信息

CommIM 自建 TURN 服务器(coturn),提供 STUN/TURN/TLS 全协议支持。

端口协议用途
3478UDP/TCPSTUN/TURN 主端口
5349TCPTURN over TLS
443TCPTURN over TLS(备用,穿透企业防火墙)
49152-65535UDPTURN Relay 媒体中继端口范围
安全说明:
  • TURN 凭证基于 HMAC-SHA1 签名,24 小时过期,与用户绑定不可跨用户使用
  • WebRTC 内置 DTLS-SRTP 加密,媒体流端到端加密
  • TURN over TLS(端口 5349/443)防止信令窃听
  • 生产环境客户端必须运行在 HTTPS 下(getUserMedia 要求安全上下文)

完整接入示例

JavaScript
class CommIMCall {
  constructor(imSdk, gateUrl) {
    this.imSdk = imSdk;
    this.gateUrl = gateUrl;
    this.pc = null;
    this.localStream = null;
    this.callId = null;
    this.peerImId = null;
    this.mediaType = 'audio';
    this.iceConfig = null;
    this.iceConfigExpiry = 0;
  }

  // 获取 TURN 凭证(带缓存)
  async getIceConfig() {
    const now = Date.now();
    if (this.iceConfig && now < this.iceConfigExpiry) return this.iceConfig;

    const token = localStorage.getItem('im_token') || '';
    const userId = localStorage.getItem('im_user_id') || '';
    const resp = await fetch(`${this.gateUrl}/Turn/GetCredentials`, {
      headers: { 'Authorization': token, 'UserID': userId },
    });
    const json = await resp.json();
    if (json.result !== 0) throw new Error('TURN 凭证获取失败');

    this.iceConfig = {
      iceServers: [{
        urls: json.data.urls,
        username: json.data.username,
        credential: json.data.credential,
      }],
    };
    this.iceConfigExpiry = now + (json.data.ttl - 300) * 1000;
    return this.iceConfig;
  }

  // 发送信令
  sendSignal(signal) {
    const data = 'commim_call:' + JSON.stringify(signal);
    this.imSdk.sendChatText(this.peerImId, '', 7, data, {
      _callSignal: '1',
      callType: signal.mediaType,
      callId: signal.callId,
    });
  }

  // 发起通话
  async startCall(peerImId, mediaType) {
    this.peerImId = peerImId;
    this.mediaType = mediaType;
    this.callId = `call_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;

    // 获取 TURN 凭证
    const iceConfig = await this.getIceConfig();

    // 获取媒体流
    this.localStream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: mediaType === 'video',
    });

    // 创建 PeerConnection
    this.pc = new RTCPeerConnection(iceConfig);
    this.localStream.getTracks().forEach(t => this.pc.addTrack(t, this.localStream));

    // 监听事件
    this.pc.ontrack = (e) => {
      if (e.streams[0]) this.onRemoteStream?.(e.streams[0]);
    };
    this.pc.onicecandidate = (e) => {
      if (e.candidate) {
        this.sendSignal({
          type: 'call_ice_candidate',
          callId: this.callId,
          mediaType: this.mediaType,
          candidate: e.candidate.toJSON(),
          timestamp: Date.now(),
        });
      }
    };

    // 发送邀请
    this.sendSignal({
      type: 'call_invite',
      callId: this.callId,
      mediaType: this.mediaType,
      timestamp: Date.now(),
    });
  }

  // 处理来电信令(在 IM 消息回调中调用)
  async handleSignal(signal, fromImId) {
    if (signal.type === 'call_answer') {
      // 对方接听,创建 Offer
      const offer = await this.pc.createOffer({
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
      });
      await this.pc.setLocalDescription(offer);
      this.sendSignal({
        type: 'call_sdp_offer', callId: this.callId,
        mediaType: this.mediaType, sdp: offer.sdp, timestamp: Date.now(),
      });
    } else if (signal.type === 'call_sdp_offer') {
      await this.pc.setRemoteDescription(new RTCSessionDescription({
        type: 'offer', sdp: signal.sdp,
      }));
      const answer = await this.pc.createAnswer();
      await this.pc.setLocalDescription(answer);
      this.sendSignal({
        type: 'call_sdp_answer', callId: this.callId,
        mediaType: this.mediaType, sdp: answer.sdp, timestamp: Date.now(),
      });
    } else if (signal.type === 'call_sdp_answer') {
      await this.pc.setRemoteDescription(new RTCSessionDescription({
        type: 'answer', sdp: signal.sdp,
      }));
    } else if (signal.type === 'call_ice_candidate') {
      await this.pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
    } else if (signal.type === 'call_hangup') {
      this.endCall();
    }
  }

  // 挂断
  hangup() {
    this.sendSignal({
      type: 'call_hangup', callId: this.callId,
      mediaType: this.mediaType, timestamp: Date.now(),
    });
    this.endCall();
  }

  // 结束通话(内部)
  endCall() {
    this.localStream?.getTracks().forEach(t => t.stop());
    this.pc?.close();
    this.pc = null;
    this.localStream = null;
    this.callId = null;
  }
}

错误码参考

gRPC 状态码 → HTTP 状态码映射

gRPC CodeHTTP Status含义
OK200成功
Canceled408请求取消
Unknown500未知错误
InvalidArgument400参数错误
DeadlineExceeded504超时
NotFound404未找到
AlreadyExists409资源冲突
PermissionDenied403权限不足
Unauthenticated401未认证
ResourceExhausted429请求过于频繁
Internal500内部错误
Unavailable503服务不可用

常见业务错误信息

错误信息触发场景
"操作太频繁,请稍后再试"验证码发送被限流(60 秒 CD)
"手机号码格式不正确"手机号校验未通过
"验证码不正确"验证码与 Redis 值不匹配
"用户名被其他人占用"注册时用户名重复
"该手机号码已有关联账号"手机号已被注册
"密码错误"bcrypt 比对不通过
"该用户不存在"登录时账号未找到
"用户限制登录"用户被封禁或手机在黑名单
"请求过于频繁"IP 限流(每 IP 每秒 10 次)

服务端口清单

服务HTTP 端口gRPC 端口说明
service_commim_gate1126012260API 网关,HTTP 反代 gRPC
service_commim_uaa1125012250用户认证与授权
service_allocator1100012000连接地址分配
service_gate1103012030WebSocket/TCP 长连接网关
service_friend1122012220好友关系
service_group1123012230群组管理
service_oss11070文件上传签名
service_inter_api11190第三方应用接口
service_out_api11120对外 API(TURN 凭证签发等)
coturn (TURN)3478/5349/443STUN/TURN/TLS 音视频 NAT 穿透

常见问题

请联系 CommIM 技术团队获取 client_pb.json 文件。该文件包含所有 Protobuf 消息定义,前端使用 protobufjs 库加载即可进行编解码。

请确认:1) 心跳间隔为 10 秒;2) 超过 30 秒未收到消息触发重连;3) 重连时使用 reconnectTokentoken_type=IM(1);4) 检查网络环境是否稳定。

通过 service_inter_apiGetIMID 接口,传入 appIdsecret 和第三方 userId,系统自动映射并返回 IM ID 和 Token。首次调用时自动创建映射关系。

有。开发环境可使用万能验证码 918666,可跳过短信/邮箱验证。生产环境请务必关闭此功能。

网关层 IP 限流为每 IP 每秒最多 10 次请求,超限返回 "请求过于频繁"。验证码发送另有 60 秒冷却时间限制。

chat_type 设为 7(CUSTOMIZE),data 字段携带自定义 JSON 字符串即可。接收端根据 chat_type 判断为自定义消息后,自行解析 data 字段。

CommIM 支持多端同时在线,消息实时同步。如果同一设备类型重复登录,先登录的设备会收到 KickOffUser 通知被踢下线。不同设备类型(如 Web 和手机)可同时在线。

技术支持

如有接入问题,请联系 CommIM 技术团队获取协助:

邮箱支持

lealli@qq.com

电话咨询

15336533357

接入申请

提交接入申请获取 AppID 和 Secret