Appearance
星图消息:scpMessage
说明:主要介绍星图传输层消息协议,包括消息功能、分类、结构体、业务使用、分包处理和消息ID规则等。
消息的功能
安全
校验和 每个包的业务数据校验和,支持不同的校验和算法。
身份验证 使用base58作为身份id,身份id由rsa密钥对中公钥生成,在任何情况下防御身份伪造。
临时密钥加密 首个包交换临时密钥和加密算法。
防重放 使用自增消息ID。
易用
分包重组 支持大消息的分包发送和重组。
丢包重发 支持丢包检测和重发机制。
请求响应模式 通过消息ID,使每个请求都可以等待对应的任务响应。
消息分类
中央消息
特点:无密钥协商,无对端id
终端消息
特点:具有来源id、目标id,可被中继,使用点对点加密
消息结构体
go
type Id uint16
type Type uint8
type HexBytes []byte
// 协议:星图传输层消息01版本
protocolSCP1 = "SC01"
// Req 普通的主动消息, 一般用于指令类消息, 需要对方回复, 即使没有任何回复内容, 也需要回复一个空包
// 如果没有收到Resp, 则应在超时时间后重发Req, 重发次数不限, 但是重发间隔应逐渐增加
// 直到收到Resp后才可以从缓存区域删除Req
Req = Type(1)
// Resp 回复消息, 用于回复Req消息
// 一般用于指令类消息的完成后回复, 如果指令耗时较长也没关系, 例如重发req的超时时间为1秒, 那么我执行指令超过1秒导致请求方重发了,也没事呀.
// 对于超时超长的异步任务,不应该使用默认的Req/Resp模式,而应该在收到请求后即回复确认, 在异步完成任务后再主动发送req告诉发起处任务已完成.
Resp = Type(2)
// RealTimeData 实时数据, 无需回复, 丢包不重发,乱序不重排, 一般用于音视频场景
// 需要 额外的手段来检测在线状态, 否则会无限发送导致变成Dos攻击
// 例如发送方需要每秒发送1个普通的状态确认包(Req)询问接收方是否仍处于等待接收状态, 如果询问 结果为否/超时,则停止发送.
// 这种补充手段是硬性要求, 不能期望接收方主动发消息来停止, 因为接收方可能不是该业务端口, 或者是掉线了.
RealTimeData = Type(3)
// InvalidReq Req/Resp模型中的error Resp,即为告知无效请求,类似HTTP协议的500服务器错误,payload里面是一个utf8的字符串表明错误信息
// 可能的情况举例:1.0的终端收到了1.1版本的新指令;APP终端收到了关闭电源指令(无法处理)
// 对于用户界面,建议的交互为"未知错误"
// 对于程序开发,需要避免出现,消息处理函数将该事件记录到错误日志
InvalidReq = Type(4)
// 星图预定义载荷类型
const (
// PayloadTypeNone 无载荷,或者调试消息类型,不定义的。该类型一定不能用于生产环境
PayloadTypeNone = 0
// PayloadTypeEcho 回声测试
PayloadTypeEcho = 8
// PayloadTypeLoginToRelay 普通节点去中继节点上线
PayloadTypeLoginToRelay = 100
// PayloadTypeRelayHeartbeat 心跳
PayloadTypeRelayHeartbeat = 110
// PayloadTypeQuery UDP协议的SCD发现服务器查询中继信息(/relay/query)
PayloadTypeQuery = 120
)
type ScpMessage struct {
// 协议标识 4字节字符串
ProtocolFlag string `json:"ProtocolFlag"`
// 消息类型 1字节
MessageType Type `json:"MessageType"`
// 包序号 2字节无符号
MessageId Id `json:"MessageId"`
// 分包总数 1字节
SpTotal uint8 `json:"SpTotal"`
// 当前分包序号 1字节
SpIndex uint8 `json:"SpIndex"`
// 发送者ID
FromPeerId peer.ID `json:"FromPeerId"`
// 接收者ID(可空)
ToPeerId peer.ID `json:"ToPeerId"`
// payload包类型(星图预定义1-1024|业务自定义1025-65535) 2字节无符号
PayloadType uint16 `json:"PayloadType"`
// payload校验和 uint16(crc32(Payload) & 0xFFFF)
PayloadCRC uint16 `json:"PayloadCRC"`
// payload长度
PayloadLength uint32 `json:"PayloadLength"`
// Payload []byte
Payload HexBytes `json:"Payload"`
}字段说明
| 字段名 | 类型 | 长度 | 描述 |
|---|---|---|---|
| ProtocolFlag | string | 4字节 | 协议标识,固定为"SC01" |
| MessageType | Type | 1字节 | 消息类型:1-Req, 2-Resp, 3-RealTimeData, 4-InvalidReq |
| MessageId | Id | 2字节 | 包序号,无符号整数 |
| SpTotal | uint8 | 1字节 | 分包总数 |
| SpIndex | uint8 | 1字节 | 当前分包序号 |
| FromPeerId | peer.ID | 44字节 | 发送者ID,定长字段,不足44尾填0x00 |
| ToPeerId | peer.ID | 44字节 | 接收者ID(可空),定长字段,不足44尾填0x00 |
| PayloadType | uint16 | 2字节 | payload包类型(星图预定义1-1024|业务自定义1025-65535) |
| PayloadCRC | uint16 | 2字节 | payload校验和,uint16(crc32(Payload) & 0xFFFF) |
| PayloadLength | uint32 | 4字节 | payload长度 |
| Payload | HexBytes | 变长 | payload数据 |
注意:两个peerId是定长字段,长度44,不足44尾填0x00,解析时也按照此规则,ProtocolFlag定长4字节
示例数据实例
json
{
"ProtocolFlag": "SC01",
"MessageType": 1,
"MessageId": 1,
"SpTotal": 1,
"SpIndex": 1,
"FromPeerId": "BQoVgGKHw51HqUzPbkKcZxFgBewNXmyuqBwV3qTBhuhb",
"ToPeerId": "BQoVgGKHw51HqUzPbkKcZxFgBewNXmyuqBwV3qTBhuhb",
"PayloadType": 1,
"PayloadCRC": 55230,
"PayloadLength": 5,
"Payload": "hello"
}示例数据序列化
长度110字节,220字符:
53433031010100010142516F5667474B487735314871557A50626B4B635A7846674265774E586D797571427756337154426875686242516F5667474B487735314871557A50626B4B635A7846674265774E586D79757142775633715442687568620100BED70500000068656C6C6F业务消息使用
一个ScpMessage的结构体大部分地方是明确定义的,承载业务的只有PayloadType和Payload两处。
例如我们可以定义PayloadType=10012为打开LED灯,结构体为ProtoBuf {ledId: <int>}
请求示例
请求方发出去的包:
json
{
"ProtocolFlag": "SC01",
"MessageType": 1,
"MessageId": 1,
"SpTotal": 1,
"SpIndex": 1,
"FromPeerId": <我的ID>,
"ToPeerId": <对端ID>,
"PayloadType": 10012,
"PayloadCRC": <计算载荷的CRC>,
"PayloadLength": <载荷字节长度>,
"Payload": <ProtoBuf序列化数据为载荷>
}响应示例
对端收到数据包后,返回响应:
json
{
"ProtocolFlag": "SC01",
"MessageType": 2,
"MessageId": 1,
"SpTotal": 1,
"SpIndex": 1,
"FromPeerId": <我的ID>,
"ToPeerId": <对端ID>,
"PayloadType": 10012,
"PayloadCRC": 0,
"PayloadLength": 0,
"Payload": null
}响应包生成规则
注意这里做的事情,使用请求数据生成对应的响应包:
- MessageType 为 Resp (2)
- 反转 FromPeerId、ToPeerId
- 原样返回MessageId(不进行自增)
- 原样返回PayloadType
- 业务规定有响应载荷则返回载荷数据结构,否则默认为null
ScpMessage分包
一个星图message的payload最大为uint32,也就是~4G,一个UDP包为~60KB,大于此值的需要分包发送,分包为星图message层面内部处理,无需上层应用参与,调用处只需传入 data[<4G] 到 sendMesage 函数即可。message函数内部会按照60KB一个包分块进行发送,重组也在message接收层面
发送分包
只是简单的在序列化时分为多个ScpMessage并设置其中的SpTotal和SpIndex即可。
接收重组
应该为每一个PeerId设置单独的缓冲区以隔离缓存 以MessageId为key形成缓冲数组,SpIndex作为数组下标以实现自动去重
直接分包发送(不建议)
如果是TCP,直接序列化一个很大的ScpMessage分段发送其字节即可,无需使用SpIndex分包。 理论上UDP也可以这样,只是要小心分块数据乱序到达。
MessageId
规则说明
- 单个会话递增 "同一个PeerId的一次通信"
- 只在主动消息中递增,例如MessageType:Req和RealTimeData
- uint16,溢出循环从0开始
你不应该
- 将它存入全局的磁盘缓存
- 固定为某个值
- 全局递增
你应该
- 在内存中记录
- 它在每个会话中单独递增
消息测试
开发调试环境(pre)
ECHO服务终端peerID:3phX8Ng2cZHz5NtP8xAf6nYy2z1BYytoejgjoHrWMGhH
你对这个终端发送的任意消息都会被原样返回
