Skip to content

星图消息:scpMessage

说明:主要介绍星图传输层消息协议,包括消息功能、分类、结构体、业务使用、分包处理和消息ID规则等。

消息的功能

安全

  1. 校验和 每个包的业务数据校验和,支持不同的校验和算法。

  2. 身份验证 使用base58作为身份id,身份id由rsa密钥对中公钥生成,在任何情况下防御身份伪造。

  3. 临时密钥加密 首个包交换临时密钥和加密算法。

  4. 防重放 使用自增消息ID。


易用

  1. 分包重组 支持大消息的分包发送和重组。

  2. 丢包重发 支持丢包检测和重发机制。

  3. 请求响应模式 通过消息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"`
}

字段说明

字段名类型长度描述
ProtocolFlagstring4字节协议标识,固定为"SC01"
MessageTypeType1字节消息类型:1-Req, 2-Resp, 3-RealTimeData, 4-InvalidReq
MessageIdId2字节包序号,无符号整数
SpTotaluint81字节分包总数
SpIndexuint81字节当前分包序号
FromPeerIdpeer.ID44字节发送者ID,定长字段,不足44尾填0x00
ToPeerIdpeer.ID44字节接收者ID(可空),定长字段,不足44尾填0x00
PayloadTypeuint162字节payload包类型(星图预定义1-1024|业务自定义1025-65535)
PayloadCRCuint162字节payload校验和,uint16(crc32(Payload) & 0xFFFF)
PayloadLengthuint324字节payload长度
PayloadHexBytes变长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
}

响应包生成规则

注意这里做的事情,使用请求数据生成对应的响应包:

  1. MessageType 为 Resp (2)
  2. 反转 FromPeerId、ToPeerId
  3. 原样返回MessageId(不进行自增)
  4. 原样返回PayloadType
  5. 业务规定有响应载荷则返回载荷数据结构,否则默认为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开始

你不应该

  1. 将它存入全局的磁盘缓存
  2. 固定为某个值
  3. 全局递增

你应该

  1. 在内存中记录
  2. 它在每个会话中单独递增

消息测试

开发调试环境(pre)

ECHO服务终端peerID:3phX8Ng2cZHz5NtP8xAf6nYy2z1BYytoejgjoHrWMGhH

你对这个终端发送的任意消息都会被原样返回

物联网设备通信协议文档