IM 平滑迁移方案
更新时间: 2025/05/09 14:09:55
网易云信稳定的 IM 即时通讯服务,在开发者中积累了良好的口碑。部分开发者希望接入网易云信的 IM 服务,但正在使用自研或友商提供的即时通讯服务。针对这一场景,网易云信为客户贴身打造了一套迁移方案,并且成功为多家客户实现了平滑迁移。
基本概念
- 应用服务器:客户方自有,服务于应用层功能的服务器。
- 网易云信 IM 服务器:网易云信提供的服务于 IM 功能的服务器。
- 原 IM 服务器:用户原先实现 IM 功能的服务器,可以是自有服务器或友商提供的云服务。
- 老应用:与原 IM 服务器链接的老版本 App。
- 新应用:迁移后与网易云信 IM 服务器连接的新版本 App。
前期准备
进行 IM 平滑迁移,需要提前进行以下准备:
- 注册并登录网易云信开发者账号,创建应用 并 开通 IM 功能。
- 若原先实现 IM 功能的服务器为友商提供的云服务,则需要提前导出用户资料和用户关系等数据。
迁移方案
强制升级迁移
强制升级迁移的方案,是指在完成网易云信 IM 接入后,新应用上架,强制所有的老应用升级至新应用的迁移方案。此方案下不存在新老应用的兼容问题。

新老兼容迁移
新老兼容迁移的方案,是指在迁移过程中,网易云信 IM 服务器和原 IM 服务器同时提供服务,新应用和旧应用并存,支持新旧应用互通。待用户逐步更新至新应用,旧应用逐步无人使用后,原 IM 服务器停止服务。

新老兼容迁移过程中,会涉及到新老应用的增量消息发送的保障。因此需要原 IM 服务器可以提供消息抄送和服务端发送消息的功能。一条消息由老应用的用户发送到新应用的用户,需要经历以下几个步骤:
- 老应用中的用户发送消息至原 IM 服务器。
- 原 IM 服务器提供消息抄送功能,将消息抄送给应用服务器。
- 应用服务器收到消息抄送后,调用网易云信提供的服务端消息发送 API 发送消息。
- 网易云信服务器将消息发送给新应用的指定用户。
反之,一条消息由新应用发送到老应用,也通过类似的四个步骤实现。
方案对比
不同的迁移方案适应不同的场景,各有优劣,需要依赖的条件也不同。详情如下:
迁移方案 | 优势 | 劣势 |
---|---|---|
强制升级迁移 | 要求老应用强制更新,否则无法使用。 | |
新老兼容迁移 |
迁移流程
flowchart LR
classDef default fill:#337EFF,stroke:#337EFF,stroke-width:0px,color:#FFFFFF;
A("开始") --> B("用户资料迁移") --> C("用户关系迁移")--> D("群组迁移")--> E("历史消息迁移")--> F("(可选)漫游消息迁移")--> G("完成")
迁移流程中接口调用时,请将调用频次控制在 1 秒 100 次以下。以免触发频控导致调用失败。
第一步:迁移用户资料
调用 注册 IM 账号 完成对应用户账号的注册与用户资料的迁移。后续也可以通过 更新用户资料名片 完成用户资料的二次修改。
- 请先完成用户迁移,再进行后续操作。
- 请在自身业务服务器上维护记录好所有用户的账号 ID。
第二步:迁移用户关系
调用 好友管理 和 黑名单管理 相关接口完成好友、黑名单等关系的迁移。
第三步:迁移群组
调用 群组管理 相关接口完成群组的迁移。
请在自身业务服务器上维护记录好所有群组 ID。
第四步:迁移历史消息
若图片、语音等消息中的文件也需要迁移并存储到网易云信服务器,需先使用 文件上传 接口上传,调用成功后将网易云信返回的 url 替换原消息体中的 url。
历史存量消息
历史存量消息(迁移前已产生),一般为某个约定时刻前(约定时刻后的参考下文 增量消息)的历史消息,请按照要求提供以下信息:
-
按照要求构造消息体,存放在文本文件中。
文件后缀建议为
.txt
,每条消息独占一行,请参考 消息体格式章节。单聊与群聊消息请分开,单独提供。建议各自一个文本文件,文件数量尽可能少。 -
迁入对应的网易云信应用 AppKey。
-
根据业务需求确定是否需要 漫游消息 功能。
针对群消息,若新进群用户希望能查阅历史消息,请在 网易云信控制台 打开 新进群用户获取历史消息开关,配置方法:应用 > IM 即时通讯 > 功能配置 > 群聊 > 新进群用户获取历史消息。
增量消息
若应用迁移模式是 新老兼容迁移,则在迁移过程中,会产生增量消息,考虑到历史消息迁移存在实时性的问题,可使用如下方案:
- 提供一个对网易云信开放的 HTTP 接口,由网易云信发起 GET 请求。需提前约定 GET 的频率,如 1 小时一次。
- 收到 GET 后返回
{"code":200, "data": [msg1,msg2,……]}
。data 为 JSON 数组,内容为增量消息,消息格式参考 消息体格式章节。
存量消息与增量消息迁移之后,可通过客户端 SDK 和服务端 API 的 查询云端历史消息接口 拉取验证。
网易云信 IM 支持云端历史消息,历史消息的存储时长在不同的套餐包中不同,各套餐包中的限制具体请参考 增值服务。
第五步:迁移最近联系人列表
最近联系人列表(最近会话列表)的迁移,通过接收云端主动下推的 离线消息与漫游消息,自动触发产生最近会话列表。
由于离线和漫游消息有限,可通过如下方法维护更多最近会话列表:
- 对于迁移前的会话列表,需要自行根据原 IM 服务提供的老数据构造并维护会话列表。
- 对于迁移后的会话列表,可以使用网易云信的消息抄送功能,在自身服务器进行全量会话列表的维护。或者开通 会话服务 功能,可从云端获取最多 3000 个最近会话。
消息体格式
- 不能同时存在两条消息的
msgid
、from
、to/tid
、createTime
这四个字段相同,否则会被覆盖。 ext
字段为消息扩展字段,若原先无该字段,可忽略。createTime
请使用毫秒级别。msgClientId
为客户端消息 ID,不能同时存在msgClientId
相同的多条消息。如果为空,导入时会自动生成一个默认的msgClientId
。
单聊消息
A 发给 B 一条文本消息
JSON{
"data": {
"ext": "ext..",
"clientType": "2",
"createTime": 1513685265481,
"msgid": 1,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B",
"body": "测试"
},
"type": 0
}
A 发给 B 一条图片消息
JSON{
"data": {
"ext": "ext..",
"clientType": "2",
"createTime": 1513685265528,
"msgid": 11,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B",
"attach": {
"h": 780,
"ext": "jpg",
"size": 2589,
"w": 1040,
"name": "haha",
"url": "https://a1.ease5fb40/2c93186d"
}
},
"type": 1
}
A 发给 B 一条语音消息
JSON{
"data": {
"ext": "ext..",
"clientType": "2",
"createTime": 1513685265528,
"msgid": 101,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B",
"attach": {
"size": 5232,
"ext": "amr",
"dur": 8000, //单位为 ms
"url": "https://a1.easemob.files/f3bdaffe174cee99f"
}
},
"type": 2
}
A 发给 B 一条视频消息
JSON{
"data": {
"ext": "ext..",
"clientType": "2",
"createTime": 1513685265528,
"msgid": 1001,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B",
"attach": {
"h": 780,
"w": 1040,
"size": 5232,
"ext": "mp4",
"dur": 8000, //单位为 ms
"url": "https://a1.easeatfiles/f3-ffe174cee99f"
}
},
"type": 3
}
A 发给 B 一条地理位置消息
JSON{
"data": {
"attach": "{\"lng\":\"116.655294\",\"title\":\"新华南路 145 号\",\"lat\":\"39.89611\"}",
"clientType": "1",
"createTime": "1537262375000",
"ext": "",
"msgid": 100561,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B"
},
"type": 4
}
A 发给 B 一条文件消息
JSON{
"data": {
"ext": "ext..",
"clientType": "2",
"createTime": 1513685265528,
"msgid": 10501,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B",
"attach": "{\"size\":5232,\"ext\":\"ttf\",\"url\":\"https://a1.easemob.com/hywtfiles/f3bda-ffe174cee99f\"}"
},
"type": 6
}
A 发给 B 一条自定义消息
JSON{
"data": {
"attach": {
"param": {
"ptId": "f5",
"pe": "http://img.u.com/zmw/upad/2014/92!/0/quty-90/fobp",
"artisanName": "",
"productPrice": "66.0",
"productName": "helase"
},
"type": "1",
"url": "",
"desc": "作息"
},
"clientType": "1",
"createTime": "1539499209000",
"ext": "",
"msgid": 10534501,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"from": "A",
"subType": 100,
"to": "B"
},
"type": 100
}
群聊消息
A 发了一条文本消息
JSON{
"data": {
"ext": "ext..",
"createTime": 1511513944827,
"clientType": 16,
"msgid": 1,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"body": "哈哈",
"tid": 1
},
"type": 0
}
A 发了一条图片消息
JSON{
"data": {
"ext": "ext..",
"createTime": 1511513944827,
"clientType": 16,
"msgid": 1231,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"attach": {
"h": 780,
"ext": "jpg",
"size": 257389,
"w": 1040,
"name": "haha",
"url": "https://a1.easemob.com/hywwjugh/jugh/chatfiles/ba05f0e0- e469-11e7-86e2- 5b402c93186d"
},
"tid": 1
},
"type": 1
}
A 发了一条语音消息
JSON{
"data": {
"ext": "ext..",
"createTime": 1511513944827,
"clientType": 16,
"msgid": 13,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"attach": "{\"size\":5232,\"ext\":\" amr\",\"dur\":8000,\"url\":\"https://a1.easemob.com/hywwjugh/jugh/chatfiles/f3bda300-e469-11e7-9508-ffe174ce e99f\"}", // dur 的单位为 ms
"tid": 1
},
"type": 2
}
群聊的其他类型消息参考 单聊。
群操作通知消息
B 被 A 拉进群
JSON{
"data": {
"accid2": "A",
"createTime": 1511513944871,
"clientType": 16,
"msgid": 3,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "B",
"subType": 100,
"tid": 1
},
"type": 101
}
A 把 B 踢出群
JSON{
"data": {
"accid2": "B",
"createTime": 1511513944871,
"clientType": 16,
"msgid": 4,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 102
}
A 禁言 C
JSON{
"data": {
"accid2": "ccc",
"createTime": 1511513944871,
"clientType": 16,
"msgid": 5,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 103
}
A 取消禁言 C
JSON{
"data": {
"accid2": "ccc",
"createTime": 1511513944871,
"clientType": 16,
"msgid": 6,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 104
}
A 任命 C 为管理员
JSON{
"data": {
"accid2": "ccc",
"createTime": 1511513944871,
"clientType": 16,
"msgid": 7,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 105
}
A 取消 C 为管理员
JSON{
"data": {
"accid2": "ccc",
"createTime": 1511513944871,
"clientType": 16,
"msgid": 8,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 106
}
A 修改了群介绍
JSON{
"data": {
"createTime": 1511513944871,
"clientType": 16,
"tinfo": {
"14": "new",
"tid": 1
},
"msgid": 9,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 107
}
A 修改了加群认证方式
JSON{
"data": {
"createTime": 1511513944872,
"clientType": 16,
"tinfo": {
"16": "0",
"tid": 1
},
"msgid": 10,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 107
}
A 把群转让给了 C
JSON{
"data": {
"accid2": "ccc",
"createTime": 1511513944872,
"clientType": 16,
"msgid": 11,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 108
}
A 退出了群
JSON{
"data": {
"createTime": 1511513944872,
"clientType": 16,
"msgid": 12,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "A",
"subType": 100,
"tid": 1
},
"type": 109
}
C 解散了群
JSON{
"data": {
"createTime": 1511513944872,
"clientType": 16,
"msgid": 13,
"msgClientId": "bec7df36-a8e4****e6abb76d58ee",
"accid": "ccc",
"subType": 100,
"tid": 1
},
"type": 110
}
群相关参数
对于修改群信息这类通知:
tinfo
的子 JSON 串中必须包含 tid
,且需要和父串中的 tid
保持一致。在示例中可以看到,key = 14 表示的是群介绍,key=16 表示的是申请入群的验证方式,其他项如下:
key | 数据类型 | 含义 |
---|---|---|
14 | string | 表示群组简介。 |
15 | string | 表示群组公告。 |
16 | int | 表示申请入群的验证方式。0 表示无需验证,直接入群。1 表示需要群主或管理员验证通过才能入群。2 表示不允许任何人申请入群。 |
17 | int | 表示群组状态,0 表示正常状态。1 表示群组禁言状态。 |
18 | string | 表示第三方扩展信息。 |
19 | string | 表示第三方服务器扩展信息。 |
20 | string | 表示群组头像。 |
21 | int | 表示邀请入群时是否需要被邀请人的同意,0 表示需要同意。1 表示不需要同意。 |
22 | int | 表示谁可以邀请他人入群,0 表示群主和管理员。1 表示所有人。 |
23 | int | 表示谁可以修改群资料,0 表示群主和管理员。1 表示所有人。 |
24 | int | 表示谁可以更新群自定义属性,0 表示群主和管理员。1 表示所有人。 |
客户端类型
对于客户端类型(ClientType
),表示上述操作的来源客户端:
枚举值 | 客户端类型 |
---|---|
1 | 表示 AOS(Android) |
2 | 表示 iOS |
4 | 表示 PC |
8 | 表示 WINPHONE |
16 | 表示 WEB |
32 | 表示 REST(Server) |
64 | 表示 MAC |
65 | 表示 HARMONY |