# 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),SetConfig(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) DeleteMsg(36),推送 ChatNotify(100) | | Favorite | 6 | 收藏夹 | FavoriteAdd(31) FavoriteList(32) FavoriteGet(33) FavoriteUpdate(34) FavoriteDelete(35) | 与 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 下载 | | 31 | FavoriteAdd | 是 | 消息右键 → 收藏 / 新建笔记 | 收藏会话内容或自定义笔记 | | 32 | FavoriteList | 是 | 左侧收藏夹 | 收藏列表 | | 33 | FavoriteGet | 是 | 点开一条收藏 | 收藏详情 | | 34 | FavoriteUpdate | 是 | 编辑笔记 | 只允许改文本/笔记 | | 35 | FavoriteDelete | 是 | 收藏右键 → 删除 | 删除自己的收藏及 COS 副本 | | 36 | DeleteMsg | 是 | 聊天记录右键 → 删除 / 多选删除 | 仅标记自己侧消息已删除 | | 100 | ChatNotify | 推送 | 聊天区收消息 | 对方在线时推送 | | 101 | Kicked | 推送 | 被挤下线 | 同一账号在别处登录 | | 102 | FriendRequestNotify | 推送 | 好友申请提示 | 收到加好友申请 | | 103 | FriendAcceptedNotify | 推送 | 好友同意提示 | 对方同意申请 | 未登录可发:Heartbeat / SetConfig / Login / Register / SendCode / ResetPassword。 连接建立后客户端应立刻发送 SetConfig,服务端按该连接语言返回 `msg`,并在应答中下发 `[limits]`(账号/密码长度、文件头像上限等)。客户端不再在本地 `config.ini` 保留这些限制。其余请求若未登录,回 `code=2`。 ## 3. 统一响应 请求的应答 body 均为 JSON 对象: | 字段 | 类型 | 说明 | |------|------|------| | `code` | number | `0` 成功,非 0 失败,见 3.1 | | `msg` | string | 文本说明 | | 其它 | — | 按命令扩展 | 响应 `type`、`cmd`、`seq` 与请求相同(推送除外)。 ### 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(服务端不解析)。 ```json {"code":0,"msg":"pong","ts":1788146838644} ``` | 字段 | 类型 | 说明 | |------|------|------| | `ts` | number | 服务端毫秒时间戳 | ### 4.1b SetConfig(cmd=13) Type=`System`。连接成功后立即发送,上报客户端语言并拉取服务端配置。 请求: ```json {"lang":"zh"} ``` `lang`:`zh` 或 `en`(决定本连接回包 `msg` 语言)。 成功: ```json { "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, "max_packet":1048576, "heartbeat_sec":30, "interval_sec":60 }, "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]`、`[protocol] max_packet`、`[client] heartbeat_sec`、`[sms] code_len` / `interval_sec` 一致。客户端用其做输入长度和本地预校验;最终仍以服务端校验为准。 `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` 登录。 请求: ```json {"account":"alice","password":"******"} ``` 成功: ```json {"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` 的验证码。 请求: ```json { "nickname":"Alice", "account":"alice", "phone":"13800138000", "sms_code":"123456", "password":"******" } ``` 约束(与客户端提示一致,长度见 `[limits]`): - 账号:4-16 位字母或数字 - 密码:6-20 位 - 手机号:11 位,以 `1` 开头 成功: ```json {"code":0,"msg":"ok","uid":10002,"account":"alice","nickname":"Alice"} ``` 注册成功后自动加好友 **文件传输助手**(`uid=1`)。 ### 4.5 SendCode(cmd=6) 对应「获取验证码」。 ```json {"phone":"13800138000","scene":"register"} ``` | 字段 | 说明 | |------|------| | `scene` | `register` 注册;`reset` 找回密码。缺省为 `register` | | `reset` | 要求该手机号已注册 | 成功: ```json {"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`。 ```json {"uid":10002,"phone":"13800138000","sms_code":"123456","password":"newpass"} ``` ### 4.7 ChangePassword(cmd=8) 对应设置里的修改密码,须已登录。 ```json {"old_password":"oldpass","password":"newpass"} ``` ### 4.8 FriendList(cmd=9) 对应左侧「好友」列表。请求 body 可空。 ```json { "code":0, "msg":"ok", "pending":1, "list":[ {"uid":1,"account":"filehelper","nickname":"文件传输助手","signature":"文件助手","online":0}, {"uid":10003,"account":"bob","nickname":"Bob","signature":"","online":1} ] } ``` `online`:`1` 在线,`0` 离线。`pending` 为待处理的好友申请数。 ### 4.9 SearchUser(cmd=14) 按账号精确搜索,11 位手机号则按手机号搜。受对方隐私设置约束:不允许被搜到时返回用户不存在。须已登录。 ```json {"keyword":"alice"} ``` 成功: ```json { "code":0, "uid":10003, "account":"alice", "nickname":"Alice", "signature":"", "relation":0, "allow_add":1 } ``` `relation`:`0` 非好友,`1` 已发出申请,`2` 已是好友,`3` 对方已申请加我。 ### 4.10 AddFriend(cmd=12) 按账号或 uid 发起好友申请,须对方同意后才成为好友。对方在线会收到 `FriendRequestNotify` 推送。 ```json {"account":"alice","message":"我是同事,请通过"} ``` `message` 为选填验证信息,最长约 50 字。 成功时 `relation=1` 表示申请已发出。若对方刚好也申请了自己,则直接成为好友(`relation=2`)。 ### 4.11 FriendRequestList(cmd=15) / HandleFriendRequest(cmd=16) 列出待处理申请;`action=1` 同意,`action=2` 拒绝。同意后双向写入好友关系,并向对方推送 `FriendAcceptedNotify`。 ```json {"id":12,"action":1} ``` ### 4.12 GetPrivacy(cmd=17) / SetPrivacy(cmd=18) 设置是否允许通过账号、手机号被搜索,以及是否允许他人添加。缺省字段保持原值。 ```json {"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=2`,`content` 为 `{"pack_id":1,"code":"smile"}`。 GetSticker 成功: ```json {"id":1,"pack_id":1,"code":"smile","name":"微笑","sha256":"...","mime":"image/png","data":""} ``` ### 4.10 ConvList(cmd=10) 对应左侧「消息」会话列表。 ```json { "code":0, "msg":"ok", "list":[ { "peer_uid":10003, "account":"bob", "nickname":"Bob", "signature":"", "last_msg_id":88, "preview":"hello", "ts":1788146838644, "unread":2, "online":1 } ] } ``` `unread` 对应会话头像角标。`online`:`1` 在线,`0` 离线。 ### 4.11 MsgHistory(cmd=11) 点开会话时拉取历史,并将会话未读清零。 ```json {"peer_uid":10003,"last_id":0,"limit":50} ``` | 字段 | 说明 | |------|------| | `last_id` | `0` 表示最新一页;大于 0 时拉比该 id 更早的消息 | | `limit` | 默认与上限见 `[limits] history_default` / `history_max`(50 / 100) | ```json { "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`(连接后经 SetConfig 下发给客户端)。 ```json {"to_uid":10003,"msg_type":1,"content":"hello"} ``` 成功: ```json {"code":0,"msg":"ok","msg_id":88,"ts":1788146838644} ``` 对方在线时,服务端另推 **ChatNotify**(cmd=100,seq=0): ```json { "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) 删除当前用户的会话列表项,并标记该用户侧的消息为已删除(对方聊天记录不受影响)。再次聊天会从空白开始。 ```json {"peer_uid":10003} ``` ### 4.14b DeleteMsg(cmd=36) 删除当前用户聊天记录中的若干条消息(软删除,对方仍可见)。`ids` 最多 100 个,且须属于该 `peer_uid` 会话。 ```json {"peer_uid":10003,"ids":[87,88]} ``` 成功:`peer_uid`、`ids`、以及更新后的会话预览 `last_msg_id`、`preview`、`ts`(无剩余消息时为空/0)。 ### 4.15 SetUnread(cmd=23) ```json {"peer_uid":10003,"unread":0} ``` `unread=0` 标记已读;`unread>=1` 标记未读。点开会话拉历史(MsgHistory)仍会清未读。 ### 4.16 DeleteFriend(cmd=24) ```json {"uid":10003} ``` 删除 `im_friend` 双向关系,并标记当前用户侧消息为已删除、清除自己的会话摘要。对方聊天记录不受影响。不是好友时 `code=4`。 ### 4.17 SetRemark(cmd=25) ```json {"uid":10003,"remark":"老王"} ``` `remark` 最长 32 字节,空字符串表示清除备注。会话列表展示优先用备注。 ### 4.18 GetProfile(cmd=26) ```json {"uid":10003} ``` 成功: ```json {"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`。 ```json {"nickname":"Alice","signature":"hello","avatar":"/9j/..."} ``` 成功返回 `nickname`、`signature`、`avatar_ver`。 ### 4.21 GetAvatar(cmd=28) ```json {"uid":10003} ``` 成功:`uid`、`ver`、`mime`、`data`(JPEG base64)。未设置头像时 `code=4`。 ### 4.22 PutFile(cmd=29) 聊天图片/文件**不再经 TCP 上传**。PutFile 只申请对象键和限时凭证,客户端用返回的 `token` 直接 PUT 到腾讯云 COS,然后再用 Chat `msg_type=3` 发送该 `file_id`。 请求(只传元数据,不要带 `data`): ```json {"name":"a.pdf","mime":"application/pdf","size":1024} ``` | 字段 | 类型 | 说明 | |------|------|------| | `name` | string | 文件名,服务端会去掉路径并截断 | | `mime` | string | 可选,缺省 `application/octet-stream` | | `size` | number | **必填**,字节数,`1 … file_max` | | `sha256` | string | 可选,十六进制,最长 64 | | `purpose` | string | 可选。`favorite` 时对象键走 `{prefix}/favorites/{uid}/{file_id}`,供收藏笔记插附件 | 成功: ```json { "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-Length`、`Content-Type`;有 `session_token` 时必须带 `x-cos-security-token`)。不要改用 chunked,不要增删已签名头。HTTP `200`/`204` 即成功。 2. **COS SDK**:用 `tmp_secret_id` / `tmp_secret_key` / `session_token`,`region`+`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) ```json {"file_id":12} ``` 成功:`file_id`、`name`、`size`、`mime`、`data`(base64)。 ### 4.24 FavoriteAdd(cmd=31,Type=6) 从会话收藏,或新建笔记。图片/文件会 **Copy** 到 COS `{prefix}/favorites/{uid}/{file_id}`,与聊天文件独立。 会话收藏: ```json {"msg_type":1,"content":"一段话","src_peer_uid":10003,"src_msg_id":88} ``` `msg_type`:1 文本、3 图片/文件(content 为 Chat 同款 JSON,含 `file_id`)、4 名片。 新建笔记: ```json {"kind":5,"content":"{\"html\":\"

笔记

\",\"files\":[12]}"} ``` 成功:`id`、`kind`、`title`。`kind`:1文本 2图片 3文件 4名片 5笔记。 ### 4.25 FavoriteList(cmd=32) ```json {"limit":200} ``` 成功:`list` 数组,每项 `id`、`kind`、`title`、`ts`。 ### 4.26 FavoriteGet(cmd=33) ```json {"id":1} ``` 成功:`id`、`kind`、`title`、`content`、`src_msg_id`、`src_peer_uid`、`ts`、`updated_ts`。 ### 4.27 FavoriteUpdate(cmd=34) 仅文本/笔记。笔记 `content` 格式同 FavoriteAdd。 ```json {"id":1,"content":"{\"html\":\"

改过了

\",\"files\":[12]}"} ``` ### 4.28 FavoriteDelete(cmd=35) ```json {"id":1} ``` 只删当前用户这条收藏,以及其 `favorites/` 下的 COS 副本。聊天 `im/files/` 不动。 ### 4.19 Kicked(cmd=101,推送) ```json {"code":0,"msg":"kicked"} ``` 收到后应回到登录窗。随后 TCP 会被服务端断开。 ## 5. 连接行为 - 接入后即可发包,无握手包 - 建议 30s 内发一次心跳,避免中间设备掐连接 - 推送复用同一包格式:`ChatNotify=100`、`Kicked=101`,`seq=0` - 关连接无需发 Logout;Logout 只清登录态 - 密码在服务端以 SHA-256 十六进制存储,传输仍为明文 JSON,生产环境应叠 TLS 或另行加密 ## 6. 配置与库表 客户端只关心 `[server]` 的 `host`/`port` 和 `[protocol]` 的 `magic`/`max_packet`。`magic` 为十六进制字符串(如 `0x494D`)。改 `magic` 后客户端必须同步。 资源下载域名见 `[cdn] domain`,由 SetConfig 原样下发;未配置则为空。COS 对象键前缀见 `[cos] prefix`。聊天图片/文件由客户端持 PutFile 下发的临时 token 直传 COS,字节不经过 IM 服务端。 长度、条数、验证码间隔等见 `[limits]`、`[sms]`。 `[sms] echo_code`:开发联调为 `1` 时,SendCode 响应带回 `sms_code`;上线改为 `0`。 新库执行 [`server/sql/init.sql`](../server/sql/init.sql)。已有 `im_user` / `im_message` 的库执行 [`server/sql/migrate.sql`](../server/sql/migrate.sql)(已存在的列报错可忽略)。 | 表 | 用途 | |----|------| | `im_user` | 用户(含手机号、签名;`status`:1 正常 / 0 禁用 / 2 系统账号) | | `im_sms_code` | 验证码 | | `im_friend` | 双向好友(含 `remark` 备注) | | `im_message` | 消息 | | `im_conversation` | 会话摘要与未读 | | `im_favorite` | 收藏(文本/图片/文件/名片/笔记) | `uid=1` 为文件传输助手(`username=filehelper`,不可登录)。 ## 7. 示例 ### 7.1 Python 心跳 + 登录 ```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 ```