protocol.md 21 KB

IM TCP 对接协议

默认监听地址以 server/config/config.ini [server] 为准(当前 0.0.0.0:6000)。传输为 裸 TCP,应用层自行组包,不是 HPSocket PACK 模型。

本文与 Qt 客户端界面一一对应:登录、注册、验证码、找回/修改密码、会话列表、好友列表、发消息、退出登录。

1. 数据包

每个消息 = 12 字节包头 + body。多包可粘在同一 TCP 流里,按 length 切包。

偏移 长度 字段 说明
0 2 magic 固定 0x494D(ASCII IM),配置里写十六进制字符串 0x494D
2 1 type 业务大类,见第 2 节
3 1 cmd 该 Type 下的命令号
4 4 seq 调用方自增序号,响应原样带回;服务端推送为 0
8 4 length 仅 body 字节数,不含包头
12 length body UTF-8 JSON,可为 0 字节
  • 字节序:大端(网络序)
  • length 上限:config.ini [protocol] max_packet,默认 1048576。聊天图片/文件走 COS 直传,不再占用 TCP 包体。
  • magic 不对,或 length > max_packet服务端立即断开
  • 未知 type / cmd:不回包、不断开(仅打日志)

1.1 组包伪代码

out = []
out += be16(0x494D)
out += be8(type)
out += be8(cmd)
out += be32(seq)
out += be32(len(body))
out += body   # UTF-8 bytes

1.2 收包

  1. 至少读满 12 字节
  2. 校验 magic == 0x494D
  3. 再读 length 字节 body
  4. seq 匹配请求;可并发多个未完成请求
  5. 推送包 seq=0,按 type+cmd 处理,不要当请求应答

2. 命令一览

线上为 1 字节 type + 1 字节 cmd(原 16 位 cmd 槽拆开)。Dispatcher 按 type 进业务 Handler,Handler 再解析 cmd

Type Handler Cmd
System 1 系统 Heartbeat(1),SetLang(13)(应答带 limits),推送 Kicked(101)
Account 2 账户 Login(2) Logout(3) Register(5) SendCode(6) ResetPassword(7) ChangePassword(8) GetPrivacy(17) SetPrivacy(18) SetProfile(27) GetAvatar(28)
Friend 3 好友 FriendList(9) AddFriend(12) SearchUser(14) FriendRequestList(15) HandleFriendRequest(16) DeleteFriend(24) SetRemark(25) GetProfile(26),推送 FriendRequestNotify(102) FriendAcceptedNotify(103)
Message 4 消息 Chat(4) ConvList(10) MsgHistory(11) DeleteConv(22) SetUnread(23) PutFile(29) GetFile(30),推送 ChatNotify(100)
Sticker 5 表情包 StickerPackList(19) StickerPack(20) GetSticker(21)

与 Client 界面的对应关系:

cmd 名称 登录 Client 说明
1 Heartbeat 建议 30s 一次 心跳
2 Login 登录窗 账号(username)+ 密码登录
3 Logout 设置 → 退出登录 清会话,不断开 TCP
4 Chat 聊天输入框发送 文本 / 表情 / 文件 / 名片
5 Register 注册账号 注册
6 SendCode 获取验证码 短信验证码
7 ResetPassword 忘记密码 用验证码重置
8 ChangePassword 设置 → 通用 → 修改密码 校验旧密码后改密
9 FriendList 好友页 好友列表
10 ConvList 消息页会话列表 会话摘要 + 未读
11 MsgHistory 点开会话 历史消息,并清未读
12 AddFriend 好友页 + 按账号发起申请,对方同意后才成为好友
14 SearchUser 添加好友对话框 按账号/手机号搜索
15 FriendRequestList 新的朋友 待处理申请
16 HandleFriendRequest 新的朋友 同意/拒绝 1同意 2拒绝
17 GetPrivacy 设置 → 隐私 读取隐私开关
18 SetPrivacy 设置 → 隐私 保存隐私开关
19 StickerPackList 聊天表情面板 表情包列表
20 StickerPack 聊天表情面板 某包下表情元数据
21 GetSticker 表情缓存 下载表情图片
22 DeleteConv 会话右键 → 删除 删除自己的会话摘要,并永久删除双方聊天记录
23 SetUnread 会话右键 → 已读/未读 unread=0 已读,unread=1 未读
24 DeleteFriend 好友右键 → 删除 双向解除好友,并永久删除双方聊天记录
25 SetRemark 好友右键 → 备注 只改自己这一侧的备注
26 GetProfile 好友右键 → 查看名片 按 uid 拉资料
27 SetProfile 设置 → 资料 改昵称、签名、头像
28 GetAvatar 头像缓存 按 uid 下载头像 JPEG
29 PutFile 聊天 → 图片/文件 申请 COS 临时上传凭证,返回 file_id + token
30 GetFile 点击文件气泡 按 file_id 下载
100 ChatNotify 推送 聊天区收消息 对方在线时推送
101 Kicked 推送 被挤下线 同一账号在别处登录
102 FriendRequestNotify 推送 好友申请提示 收到加好友申请
103 FriendAcceptedNotify 推送 好友同意提示 对方同意申请

未登录可发:Heartbeat / SetLang / Login / Register / SendCode / ResetPassword。 连接建立后客户端应立刻发送 SetLang,服务端按该连接语言返回 msg,并在应答中下发 [limits](账号/密码长度、文件头像上限等)。客户端不再在本地 config.ini 保留这些限制。其余请求若未登录,回 code=2

3. 统一响应

请求的应答 body 均为 JSON 对象:

字段 类型 说明
code number 0 成功,非 0 失败,见 3.1
msg string 文本说明
其它 按命令扩展

响应 typecmdseq 与请求相同(推送除外)。

3.1 错误码

code 含义
0 成功
1 参数/JSON 错误
2 未登录
3 账号或密码错误 / 手机号不匹配
4 用户不存在
5 账号已存在
6 手机号已注册
7 验证码错误或过期
8 密码长度不符合 6-20
9 不允许(系统账号、加自己等)
10 数据库不可用或 SQL 失败
11 账号格式不符合 4-16 位字母或数字
12 验证码发送过于频繁
13 账号已禁用

字段:登录用 uid;注册仍用 account 作为用户名(展示/加好友资料,不用于登录)。


4. 命令详情

4.1 Heartbeat(cmd=1)

请求 body:空,或任意 JSON(服务端不解析)。

{"code":0,"msg":"pong","ts":1788146838644}
字段 类型 说明
ts number 服务端毫秒时间戳

4.1b SetLang(cmd=13)

Type=System。连接成功后立即发送,设置本连接回包语言。

请求:

{"lang":"zh"}

langzhen

成功:

{
  "code":0,
  "msg":"成功",
  "lang":"zh",
  "limits":{
    "account_min":4,
    "account_max":16,
    "password_min":6,
    "password_max":20,
    "phone_len":11,
    "nickname_max":32,
    "signature_max":64,
    "content_max":4096,
    "preview_max":120,
    "history_default":50,
    "history_max":100,
    "avatar_max":81920,
    "file_max":524288,
    "code_len":6
  },
    "cdn":{
      "domain":"https://cdn.example.com"
    },
    "update":{
      "version":"1.0.1",
      "url":"https://cdn.example.com/NIM-1.0.1.zip",
      "notes":"修复文件上传"
    }
}

limits 与服务端 config.ini[limits] 以及 [sms] code_len 一致。客户端用其做输入长度和本地预校验;最终仍以服务端校验为准。

cdn.domain 只来自 [cdn] domain,不会按 COS 自动推导。未配置时下发空串,客户端不走 HTTP 下载。

update 来自服务端 [update]version 为最新客户端版本,url 为 ZIP 安装包地址,notes 为说明。客户端用本地 [app] version 比较,更高则提示更新。ZIP 须把安装目录内容放在根(NIM.exe 在压缩包根目录);客户端先下载 ZIP,再复制一份自身到临时目录并以 --apply-update 用自带 3rdparty/7z/x64/7za.exe 解压覆盖原目录(失败再回退系统 tar / Expand-Archive),然后拉起新程序并清理临时副本。url 为空时客户端忽略。

4.2 Login(cmd=2)

对应登录窗:账号(im_user.username)+ 密码。请求字段为 account(兼容 username),不再使用 uid 登录。

请求:

{"account":"alice","password":"******"}

成功:

{"code":0,"msg":"ok","uid":10002,"account":"alice","nickname":"Alice","phone":"13800138000","signature":"","avatar_ver":0}

同一账号在新连接登录成功时,旧连接会收到 Kicked 推送并被断开。系统账号(文件传输助手)不可登录。

4.3 Logout(cmd=3)

请求 body 可空。成功后该连接变为未登录,TCP 不断开。关连接无需发 Logout。

4.4 Register(cmd=5)

对应注册窗:昵称、账号、手机号、验证码、密码。须先对同一手机号发送 scene=register 的验证码。

请求:

{
  "nickname":"Alice",
  "account":"alice",
  "phone":"13800138000",
  "sms_code":"123456",
  "password":"******"
}

约束(与客户端提示一致,长度见 [limits]):

  • 账号:4-16 位字母或数字
  • 密码:6-20 位
  • 手机号:11 位,以 1 开头

成功:

{"code":0,"msg":"ok","uid":10002,"account":"alice","nickname":"Alice"}

注册成功后自动加好友 文件传输助手uid=1)。

4.5 SendCode(cmd=6)

对应「获取验证码」。

{"phone":"13800138000","scene":"register"}
字段 说明
scene register 注册;reset 找回密码。缺省为 register
reset 要求该手机号已注册

成功:

{"code":0,"msg":"ok","ttl":300,"sms_code":"123456"}

sms_code 仅在 config.ini [sms] echo_code=1 时返回,便于客户端联调。生产环境必须改为 0。默认 60 秒内不可对同一手机号+场景重复发送。

4.6 ResetPassword(cmd=7)

对应忘记密码:uid + 绑定手机号 + 验证码 + 新密码。验证码 scene 必须是 reset

{"uid":10002,"phone":"13800138000","sms_code":"123456","password":"newpass"}

4.7 ChangePassword(cmd=8)

对应设置里的修改密码,须已登录。

{"old_password":"oldpass","password":"newpass"}

4.8 FriendList(cmd=9)

对应左侧「好友」列表。请求 body 可空。

{
  "code":0,
  "msg":"ok",
  "pending":1,
  "list":[
    {"uid":1,"account":"filehelper","nickname":"文件传输助手","signature":"文件助手","online":0},
    {"uid":10003,"account":"bob","nickname":"Bob","signature":"","online":1}
  ]
}

online1 在线,0 离线。pending 为待处理的好友申请数。

4.9 SearchUser(cmd=14)

按账号精确搜索,11 位手机号则按手机号搜。受对方隐私设置约束:不允许被搜到时返回用户不存在。须已登录。

{"keyword":"alice"}

成功:

{
  "code":0,
  "uid":10003,
  "account":"alice",
  "nickname":"Alice",
  "signature":"",
  "relation":0,
  "allow_add":1
}

relation0 非好友,1 已发出申请,2 已是好友,3 对方已申请加我。

4.10 AddFriend(cmd=12)

按账号或 uid 发起好友申请,须对方同意后才成为好友。对方在线会收到 FriendRequestNotify 推送。

{"account":"alice","message":"我是同事,请通过"}

message 为选填验证信息,最长约 50 字。

成功时 relation=1 表示申请已发出。若对方刚好也申请了自己,则直接成为好友(relation=2)。

4.11 FriendRequestList(cmd=15) / HandleFriendRequest(cmd=16)

列出待处理申请;action=1 同意,action=2 拒绝。同意后双向写入好友关系,并向对方推送 FriendAcceptedNotify

{"id":12,"action":1}

4.12 GetPrivacy(cmd=17) / SetPrivacy(cmd=18)

设置是否允许通过账号、手机号被搜索,以及是否允许他人添加。缺省字段保持原值。

{"allow_search_account":1,"allow_search_phone":1,"allow_add_friend":1}

4.13 StickerPackList(cmd=19) / StickerPack(cmd=20) / GetSticker(cmd=21)

Type=Sticker(5)。表情包由服务端下发,客户端按 sha256 缓存。发消息时 msg_type=2content{"pack_id":1,"code":"smile"}

GetSticker 成功:

{"id":1,"pack_id":1,"code":"smile","name":"微笑","sha256":"...","mime":"image/png","data":"<base64>"}

4.10 ConvList(cmd=10)

对应左侧「消息」会话列表。

{
  "code":0,
  "msg":"ok",
  "list":[
    {
      "peer_uid":10003,
      "account":"bob",
      "nickname":"Bob",
      "signature":"",
      "last_msg_id":88,
      "preview":"hello",
      "ts":1788146838644,
      "unread":2
    }
  ]
}

unread 对应会话头像角标。

4.11 MsgHistory(cmd=11)

点开会话时拉取历史,并将会话未读清零。

{"peer_uid":10003,"last_id":0,"limit":50}
字段 说明
last_id 0 表示最新一页;大于 0 时拉比该 id 更早的消息
limit 默认与上限见 [limits] history_default / history_max(50 / 100)
{
  "code":0,
  "msg":"ok",
  "list":[
    {"msg_id":87,"from_uid":10003,"to_uid":10002,"msg_type":1,"content":"hi","ts":1788146800000},
    {"msg_id":88,"from_uid":10002,"to_uid":10003,"msg_type":1,"content":"hello","ts":1788146838644}
  ]
}

列表按时间正序(旧 → 新)。from_uid 等于当前用户即自己发的气泡。

4.12 Chat(cmd=4)

对应输入框发送。msg_type=1 文本;msg_type=2 表情,content{"pack_id":1,"code":"smile"}msg_type=3 文件/图片,content{"file_id":12,"name":"a.pdf","size":1024,"mime":"application/pdf"}(须先 PutFile,再用 token 直传 COS,对象落盘后再发 Chat;服务端会 HEAD 校验);msg_type=4 名片,content{"uid":10003,"account":"bob","nickname":"Bob"}。文本最长、文件体积上限见服务端 [limits] content_max / file_max(连接后经 SetLang 下发给客户端)。

{"to_uid":10003,"msg_type":1,"content":"hello"}

成功:

{"code":0,"msg":"ok","msg_id":88,"ts":1788146838644}

对方在线时,服务端另推 ChatNotify(cmd=100,seq=0):

{
  "msg_id":88,
  "from_uid":10002,
  "from_account":"alice",
  "from_nickname":"Alice",
  "to_uid":10003,
  "msg_type":1,
  "content":"hello",
  "ts":1788146838644
}

双方会话预览会更新;接收方 unread+1

4.14 DeleteConv(cmd=22)

删除当前用户的会话列表项,并 永久删除 im_message 中双方之间的全部聊天记录。再次聊天会从空白开始。

{"peer_uid":10003}

4.15 SetUnread(cmd=23)

{"peer_uid":10003,"unread":0}

unread=0 标记已读;unread>=1 标记未读。点开会话拉历史(MsgHistory)仍会清未读。

4.16 DeleteFriend(cmd=24)

{"uid":10003}

删除 im_friend 双向关系,并永久删除双方聊天记录与会话摘要。不是好友时 code=4

4.17 SetRemark(cmd=25)

{"uid":10003,"remark":"老王"}

remark 最长 32 字节,空字符串表示清除备注。会话列表展示优先用备注。

4.18 GetProfile(cmd=26)

{"uid":10003}

成功:

{"code":0,"uid":10003,"account":"bob","nickname":"Bob","signature":"","avatar_ver":1,"relation":2,"remark":"老王"}

4.20 SetProfile(cmd=27)

改自己的昵称、签名;可选带 JPEG 头像(base64)。头像须为 JPEG,体积不超过 avatar_max

{"nickname":"Alice","signature":"hello","avatar":"/9j/..."}

成功返回 nicknamesignatureavatar_ver

4.21 GetAvatar(cmd=28)

{"uid":10003}

成功:uidvermimedata(JPEG base64)。未设置头像时 code=4

4.22 PutFile(cmd=29)

聊天图片/文件不再经 TCP 上传。PutFile 只申请对象键和限时凭证,客户端用返回的 token 直接 PUT 到腾讯云 COS,然后再用 Chat msg_type=3 发送该 file_id

请求(只传元数据,不要带 data):

{"name":"a.pdf","mime":"application/pdf","size":1024}
字段 类型 说明
name string 文件名,服务端会去掉路径并截断
mime string 可选,缺省 application/octet-stream
size number 必填,字节数,1 … file_max
sha256 string 可选,十六进制,最长 64

成功:

{
  "code":0,
  "msg":"ok",
  "file_id":12,
  "name":"a.pdf",
  "size":1024,
  "mime":"application/pdf",
  "url":"im/files/12.pdf",
  "token":{
    "tmp_secret_id":"AKIDxxx",
    "tmp_secret_key":"xxx",
    "session_token":"xxx",
    "start_time":1788146838,
    "expired_time":1788148638,
    "bucket":"examplebucket-1250000000",
    "region":"ap-beijing",
    "key":"im/files/12.pdf",
    "host":"examplebucket-1250000000.cos.ap-beijing.myqcloud.com",
    "upload_url":"https://examplebucket-1250000000.cos.ap-beijing.myqcloud.com/im/files/12.pdf",
    "method":"PUT",
    "authorization":"q-sign-algorithm=sha1&q-ak=...&q-signature=...",
    "headers":{
      "Host":"examplebucket-1250000000.cos.ap-beijing.myqcloud.com",
      "Authorization":"q-sign-algorithm=sha1&...",
      "Content-Type":"application/pdf",
      "Content-Length":"1024",
      "x-cos-security-token":"xxx"
    }
  }
}

客户端上传(二选一,推荐 HTTP PUT):

  1. HTTP PUT(无需 COS SDK):PUT token.upload_url,请求体为原始文件字节,请求头必须与 token.headers 逐项一致(含 Content-LengthContent-Type;有 session_token 时必须带 x-cos-security-token)。不要改用 chunked,不要增删已签名头。HTTP 200/204 即成功。
  2. COS SDK:用 tmp_secret_id / tmp_secret_key / session_tokenregion+bucket,对象键 key,PutObject 上传。

url 仍是对象键,与 CDN 拼接方式不变:cdn.domain + "/" + url。凭证默认约 30 分钟有效。STS 申请失败时服务端会回退为永久密钥签发的限时 PUT 签名,此时 tmp_secret_* / session_token 可能为空,客户端仍可用 headers + upload_url 直传。

对象上传完成前不要发 Chat;服务端对 msg_type=3 会 HEAD COS,对象不存在则 code=4

GetFile 仍可用于无 CDN 时的回退下载;配置了 [cdn] domain 时客户端应走 HTTP 下载,不要再经 TCP 拉文件。

4.23 GetFile(cmd=30)

{"file_id":12}

成功:file_idnamesizemimedata(base64)。

4.19 Kicked(cmd=101,推送)

{"code":0,"msg":"kicked"}

收到后应回到登录窗。随后 TCP 会被服务端断开。

5. 连接行为

  • 接入后即可发包,无握手包
  • 建议 30s 内发一次心跳,避免中间设备掐连接
  • 推送复用同一包格式:ChatNotify=100Kicked=101seq=0
  • 关连接无需发 Logout;Logout 只清登录态
  • 密码在服务端以 SHA-256 十六进制存储,传输仍为明文 JSON,生产环境应叠 TLS 或另行加密

6. 配置与库表

客户端只关心 [server]host/port[protocol]magic/max_packetmagic 为十六进制字符串(如 0x494D)。改 magic 后客户端必须同步。

资源下载域名见 [cdn] domain,由 SetLang 原样下发;未配置则为空。COS 对象键前缀见 [cos] prefix。聊天图片/文件由客户端持 PutFile 下发的临时 token 直传 COS,字节不经过 IM 服务端。

长度、条数、验证码间隔等见 [limits][sms]

[sms] echo_code:开发联调为 1 时,SendCode 响应带回 sms_code;上线改为 0

新库执行 server/sql/init.sql。已有 im_user / im_message 的库执行 server/sql/migrate.sql(已存在的列报错可忽略)。

用途
im_user 用户(含手机号、签名;status:1 正常 / 0 禁用 / 2 系统账号)
im_sms_code 验证码
im_friend 双向好友(含 remark 备注)
im_message 消息
im_conversation 会话摘要与未读

uid=1 为文件传输助手(username=filehelper,不可登录)。

7. 示例

7.1 Python 心跳 + 登录

import socket, struct, json

MAGIC = 0x494D
TYPE_SYSTEM, TYPE_ACCOUNT = 1, 2
CMD_HEARTBEAT, CMD_LOGIN = 1, 2

def pack(typ: int, cmd: int, seq: int, body: bytes = b"") -> bytes:
    return struct.pack(">HBBII", MAGIC, typ, cmd, seq, len(body)) + body

def recv_packet(sock: socket.socket):
    header = sock.recv(12)
    if len(header) < 12:
        raise ConnectionError("closed")
    magic, typ, cmd, seq, length = struct.unpack(">HBBII", header)
    if magic != MAGIC:
        raise ValueError("bad magic")
    body = b""
    while len(body) < length:
        chunk = sock.recv(length - len(body))
        if not chunk:
            raise ConnectionError("closed")
        body += chunk
    return typ, cmd, seq, body

s = socket.create_connection(("127.0.0.1", 6000))
s.sendall(pack(TYPE_SYSTEM, CMD_HEARTBEAT, 1))
print(recv_packet(s)[3])
s.sendall(pack(TYPE_ACCOUNT, CMD_LOGIN, 2, json.dumps(
    {"account": "alice", "password": "123456"}).encode()))
print(json.loads(recv_packet(s)[3]))
s.close()

7.2 十六进制

空 body 心跳(System/Heartbeat,seq=1):

49 4D 01 01 00 00 00 01 00 00 00 00