DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,89 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"longheng.io/server/pkg/serializer"
|
||||
)
|
||||
|
||||
var topicPayloadMakers = map[string]func() any{
|
||||
TopicServiceStarted: func() any { return &ServiceStarted{} },
|
||||
TopicClusterServiceStarted: func() any { return &ClusterServiceEvent{} },
|
||||
TopicClusterServiceState: func() any { return &ClusterServiceEvent{} },
|
||||
TopicClusterServiceBeat: func() any { return &ClusterServiceEvent{} },
|
||||
TopicClusterServiceStopped: func() any { return &ClusterServiceEvent{} },
|
||||
TopicGatewayClientPacket: func() any { return &GatewayClientPacket{} },
|
||||
TopicGatewayPush: func() any { return &GatewayPush{} },
|
||||
TopicLogicPlayerResponse: func() any { return &LogicPlayerResponse{} },
|
||||
TopicSessionConnected: func() any { return &SessionConnected{} },
|
||||
TopicSessionDisconnected: func() any { return &SessionDisconnected{} },
|
||||
TopicPlayerStateChange: func() any { return &PlayerStateChanged{} },
|
||||
TopicChatBroadcast: func() any { return &ChatBroadcast{} },
|
||||
TopicNoticePublished: func() any { return &NoticePublished{} },
|
||||
TopicRoomAllocated: func() any { return &RoomAllocated{} },
|
||||
}
|
||||
|
||||
// MarshalTopicPayload 使用框架默认 JSON 编码器序列化 topic 负载。
|
||||
func MarshalTopicPayload(payload any) ([]byte, error) {
|
||||
codec, err := serializer.DefaultRegistry.MustGet("json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return codec.Marshal(payload)
|
||||
}
|
||||
|
||||
// UnmarshalTopicPayload 按 topic 反序列化为已知契约结构,未知 topic 返回 map。
|
||||
func UnmarshalTopicPayload(topic string, data []byte) (any, error) {
|
||||
factory := topicPayloadFactory(topic)
|
||||
if factory == nil {
|
||||
var payload map[string]any
|
||||
if err := unmarshalJSON(data, &payload); err != nil {
|
||||
return nil, fmt.Errorf("decode topic %q: %w", topic, err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
target := factory()
|
||||
if err := unmarshalJSON(data, target); err != nil {
|
||||
return nil, fmt.Errorf("decode topic %q: %w", topic, err)
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(target)
|
||||
if value.Kind() == reflect.Pointer && !value.IsNil() {
|
||||
return value.Elem().Interface(), nil
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func topicPayloadFactory(topic string) func() any {
|
||||
if factory, ok := topicPayloadMakers[topic]; ok {
|
||||
return factory
|
||||
}
|
||||
switch {
|
||||
case IsLogicNodePacketTopic(topic):
|
||||
return func() any { return &GatewayClientPacket{} }
|
||||
case IsLogicSessConnTopic(topic):
|
||||
return func() any { return &SessionConnected{} }
|
||||
case IsLogicSessDiscTopic(topic):
|
||||
return func() any { return &SessionDisconnected{} }
|
||||
case IsGatewayNodePushTopic(topic):
|
||||
return func() any { return &GatewayPush{} }
|
||||
case IsGatewayNodeRespTopic(topic):
|
||||
return func() any { return &LogicPlayerResponse{} }
|
||||
case IsRPCNodeRequestTopic(topic):
|
||||
return func() any { return &RPCRequest{} }
|
||||
case IsRPCResponseTopic(topic):
|
||||
return func() any { return &RPCResponse{} }
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalJSON(data []byte, target any) error {
|
||||
codec, err := serializer.DefaultRegistry.MustGet("json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return codec.Unmarshal(data, target)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Package contracts 定义服务和 SDK 相关包共享的稳定消息、事件、指标和 RPC 契约。
|
||||
//
|
||||
// 这里的类型应该谨慎变更;它们是 gateway、logic、runtime 模块、生成客户端和运维工具之间的兼容层。
|
||||
package contracts
|
||||
@@ -0,0 +1,141 @@
|
||||
package contracts
|
||||
|
||||
import "time"
|
||||
|
||||
// ServiceStarted 描述单节点服务启动事件。
|
||||
type ServiceStarted struct {
|
||||
Service string `json:"service"`
|
||||
NodeID string `json:"node_id"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
// ClusterServiceEvent 描述服务在集群拓扑中的状态、地址和版本信息。
|
||||
type ClusterServiceEvent struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
GID int64 `json:"gid,omitempty"`
|
||||
VirtualGID int64 `json:"virtual_gid,omitempty"`
|
||||
ShardID int64 `json:"shard_id,omitempty"`
|
||||
Service string `json:"service"`
|
||||
NodeID string `json:"node_id"`
|
||||
Environment string `json:"environment"`
|
||||
Zone string `json:"zone"`
|
||||
PublicAddr string `json:"public_addr,omitempty"`
|
||||
PrivateAddr string `json:"private_addr,omitempty"`
|
||||
AdminAddr string `json:"admin_addr,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
DataVersion string `json:"data_version,omitempty"`
|
||||
State string `json:"state"`
|
||||
Maintaining bool `json:"maintaining"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// GatewayClientPacket 是 gateway 转发到 logic 的客户端请求包。
|
||||
type GatewayClientPacket struct {
|
||||
GatewayNodeID string `json:"gateway_node_id"`
|
||||
TargetLogicNodeID string `json:"target_logic_node_id,omitempty"`
|
||||
SessionID string `json:"session_id"`
|
||||
AccountID string `json:"account_id"`
|
||||
PacketID int32 `json:"packet_id"`
|
||||
MsgID uint32 `json:"msg_id"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
ClientRequestID string `json:"client_request_id,omitempty"`
|
||||
WireFormat string `json:"wire_format,omitempty"`
|
||||
Compressed bool `json:"compressed"`
|
||||
Body []byte `json:"body,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
ReceivedAt time.Time `json:"received_at"`
|
||||
}
|
||||
|
||||
// LogicPlayerResponse 是 logic 返回给 gateway 的玩家响应包。
|
||||
type LogicPlayerResponse struct {
|
||||
TargetGatewayNodeID string `json:"target_gateway_node_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
AccountID string `json:"account_id"`
|
||||
PacketID int32 `json:"packet_id"`
|
||||
MsgID uint32 `json:"msg_id"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
WireFormat string `json:"wire_format,omitempty"`
|
||||
Compressed bool `json:"compressed"`
|
||||
Body []byte `json:"body,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// GatewayPush 是服务侧推送到 gateway 的单人或广播消息。
|
||||
type GatewayPush struct {
|
||||
TargetGatewayNodeID string `json:"target_gateway_node_id,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
AccountID string `json:"account_id,omitempty"`
|
||||
Broadcast bool `json:"broadcast,omitempty"`
|
||||
PacketID int32 `json:"packet_id"`
|
||||
MsgID uint32 `json:"msg_id"`
|
||||
Sequence uint64 `json:"sequence,omitempty"`
|
||||
WireFormat string `json:"wire_format,omitempty"`
|
||||
Compressed bool `json:"compressed"`
|
||||
Body []byte `json:"body,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// SessionConnected 描述会话绑定到账号后的上线事件。
|
||||
type SessionConnected struct {
|
||||
TargetLogicNodeID string `json:"target_logic_node_id,omitempty"`
|
||||
SessionID string `json:"session_id"`
|
||||
AccountID string `json:"account_id"`
|
||||
ConnectedAt time.Time `json:"connected_at"`
|
||||
}
|
||||
|
||||
// SessionDisconnected 描述会话断开和下线原因。
|
||||
type SessionDisconnected struct {
|
||||
TargetLogicNodeID string `json:"target_logic_node_id,omitempty"`
|
||||
SessionID string `json:"session_id"`
|
||||
AccountID string `json:"account_id"`
|
||||
Reason string `json:"reason"`
|
||||
DisconnectedAt time.Time `json:"disconnected_at"`
|
||||
}
|
||||
|
||||
// PlayerStateChanged 描述玩家状态变化事件。
|
||||
type PlayerStateChanged struct {
|
||||
AccountID string `json:"account_id"`
|
||||
State string `json:"state"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ChatBroadcast 描述社交聊天广播事件。
|
||||
type ChatBroadcast struct {
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
Seq uint64 `json:"seq,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
AccountID string `json:"account_id"`
|
||||
To string `json:"to,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
}
|
||||
|
||||
// NoticePublished 描述公告发布后的事件负载。
|
||||
type NoticePublished struct {
|
||||
NoticeID string `json:"notice_id"`
|
||||
Kind string `json:"kind"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
ShardID string `json:"shard_id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
AttachmentRef string `json:"attachment_ref,omitempty"`
|
||||
Recipients []string `json:"recipients,omitempty"`
|
||||
DeliveryIDs []string `json:"delivery_ids,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
PublishedAt time.Time `json:"published_at"`
|
||||
}
|
||||
|
||||
// RoomAllocated 描述场景房间分配结果。
|
||||
type RoomAllocated struct {
|
||||
RoomID string `json:"room_id"`
|
||||
AccountID string `json:"account_id"`
|
||||
AllocatedAt time.Time `json:"allocated_at"`
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
// MetricPrefixLongHeng 是所有框架指标名的统一前缀。
|
||||
MetricPrefixLongHeng = "longheng"
|
||||
|
||||
// MetricLabelService 标记服务名。
|
||||
MetricLabelService = "service"
|
||||
|
||||
// MetricLabelNode 标记节点 ID。
|
||||
MetricLabelNode = "node"
|
||||
|
||||
// MetricLabelRoute 标记协议或 RPC 路由。
|
||||
MetricLabelRoute = "route"
|
||||
|
||||
// MetricLabelResult 标记操作结果。
|
||||
MetricLabelResult = "result"
|
||||
|
||||
// MetricLabelStatus 标记状态值。
|
||||
MetricLabelStatus = "status"
|
||||
|
||||
// MetricLabelComponent 标记平台或运行时组件。
|
||||
MetricLabelComponent = "component"
|
||||
|
||||
// MetricLabelKind 标记对象类型。
|
||||
MetricLabelKind = "kind"
|
||||
|
||||
// MetricLabelGID 标记游戏区或战区 ID。
|
||||
MetricLabelGID = "gid"
|
||||
|
||||
// MetricLabelSID 标记分片 ID。
|
||||
MetricLabelSID = "sid"
|
||||
)
|
||||
|
||||
const (
|
||||
// MetricResultOK 表示操作成功。
|
||||
MetricResultOK = "ok"
|
||||
|
||||
// MetricResultError 表示操作失败。
|
||||
MetricResultError = "error"
|
||||
|
||||
// MetricResultDropped 表示请求或事件被丢弃。
|
||||
MetricResultDropped = "dropped"
|
||||
|
||||
// MetricResultTimeout 表示操作超时。
|
||||
MetricResultTimeout = "timeout"
|
||||
)
|
||||
|
||||
// MetricName 生成跨代码、告警和仪表盘共用的稳定指标名。
|
||||
func MetricName(parts ...string) string {
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
out = append(out, metricNameFields(part)...)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(out, "_")
|
||||
}
|
||||
|
||||
// PlatformMetric 生成平台核心能力指标名,例如 longheng_platform_bus_publish_total。
|
||||
func PlatformMetric(component string, parts ...string) string {
|
||||
return MetricName(append([]string{MetricPrefixLongHeng, "platform", component}, parts...)...)
|
||||
}
|
||||
|
||||
// RuntimeMetric 生成可选系统模块指标名,例如 longheng_runtime_matchmaker_ticket_total。
|
||||
func RuntimeMetric(component string, parts ...string) string {
|
||||
return MetricName(append([]string{MetricPrefixLongHeng, "runtime", component}, parts...)...)
|
||||
}
|
||||
|
||||
// ServiceMetric 生成服务层指标名,例如 longheng_service_gateway_connections_total。
|
||||
func ServiceMetric(service string, parts ...string) string {
|
||||
return MetricName(append([]string{MetricPrefixLongHeng, "service", service}, parts...)...)
|
||||
}
|
||||
|
||||
// MetricLabels 构造指标标签并跳过空 key,避免调用侧反复写样板代码。
|
||||
func MetricLabels(values ...string) map[string]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
labels := make(map[string]string, len(values)/2)
|
||||
for idx := 0; idx+1 < len(values); idx += 2 {
|
||||
key := strings.TrimSpace(values[idx])
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
labels[key] = strings.TrimSpace(values[idx+1])
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
return nil
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
// ShardLabels 是区服/分片指标的通用标签构造器。
|
||||
func ShardLabels(gid, sid int64) map[string]string {
|
||||
return MetricLabels(
|
||||
MetricLabelGID, strconv.FormatInt(gid, 10),
|
||||
MetricLabelSID, strconv.FormatInt(sid, 10),
|
||||
)
|
||||
}
|
||||
|
||||
func metricNameFields(part string) []string {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.FieldsFunc(part, func(r rune) bool {
|
||||
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
||||
})
|
||||
out := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
field = strings.ToLower(strings.Trim(field, "_"))
|
||||
if field != "" {
|
||||
out = append(out, field)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package contracts
|
||||
|
||||
import "time"
|
||||
|
||||
// RPCRequest 是跨节点 RPC 请求的总线契约。
|
||||
type RPCRequest struct {
|
||||
RequestID string `json:"request_id"`
|
||||
SourceService string `json:"source_service,omitempty"`
|
||||
SourceNodeID string `json:"source_node_id"`
|
||||
TargetNodeID string `json:"target_node_id"`
|
||||
Route string `json:"route"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Payload []byte `json:"payload,omitempty"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
}
|
||||
|
||||
// RPCResponse 是跨节点 RPC 响应的总线契约。
|
||||
type RPCResponse struct {
|
||||
RequestID string `json:"request_id"`
|
||||
SourceNodeID string `json:"source_node_id,omitempty"`
|
||||
TargetNodeID string `json:"target_node_id"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Payload []byte `json:"payload,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TopicScope 表示带作用域 topic 的层级类型。
|
||||
type TopicScope string
|
||||
|
||||
const (
|
||||
// TopicScopeService 表示服务作用域 topic。
|
||||
TopicScopeService TopicScope = "service"
|
||||
|
||||
// TopicScopeZone 表示区服作用域 topic。
|
||||
TopicScopeZone TopicScope = "zone"
|
||||
|
||||
// TopicScopeShard 表示分片作用域 topic。
|
||||
TopicScopeShard TopicScope = "shard"
|
||||
|
||||
// TopicServiceStarted 是单节点服务启动事件 topic。
|
||||
TopicServiceStarted = "service.started"
|
||||
|
||||
// TopicClusterServiceStarted 是集群服务启动事件 topic。
|
||||
TopicClusterServiceStarted = "cluster.service.started"
|
||||
|
||||
// TopicClusterServiceState 是集群服务状态变化事件 topic。
|
||||
TopicClusterServiceState = "cluster.service.state"
|
||||
|
||||
// TopicClusterServiceBeat 是集群服务心跳事件 topic。
|
||||
TopicClusterServiceBeat = "cluster.service.heartbeat"
|
||||
|
||||
// TopicClusterServiceStopped 是集群服务停止事件 topic。
|
||||
TopicClusterServiceStopped = "cluster.service.stopped"
|
||||
|
||||
// TopicGatewayClientPacket 是 gateway 收到客户端包后的事件 topic。
|
||||
TopicGatewayClientPacket = "gateway.client.packet"
|
||||
|
||||
// TopicGatewayPush 是推送到 gateway 的消息 topic。
|
||||
TopicGatewayPush = "gateway.push"
|
||||
|
||||
// TopicLogicPlayerResponse 是 logic 返回玩家响应的事件 topic。
|
||||
TopicLogicPlayerResponse = "logic.player.response"
|
||||
|
||||
// TopicSessionConnected 是会话上线事件 topic。
|
||||
TopicSessionConnected = "gateway.session.connected"
|
||||
|
||||
// TopicSessionDisconnected 是会话下线事件 topic。
|
||||
TopicSessionDisconnected = "gateway.session.disconnected"
|
||||
|
||||
// TopicPlayerStateChange 是玩家状态变化事件 topic。
|
||||
TopicPlayerStateChange = "logic.player.state.changed"
|
||||
|
||||
// TopicChatBroadcast 是聊天广播事件 topic。
|
||||
TopicChatBroadcast = "social.chat.broadcast"
|
||||
|
||||
// TopicNoticePublished 是公告发布事件 topic。
|
||||
TopicNoticePublished = "notice.published"
|
||||
|
||||
// TopicRoomAllocated 是场景房间分配事件 topic。
|
||||
TopicRoomAllocated = "scene.room.allocated"
|
||||
|
||||
topicLogicPrefix = "logic.node."
|
||||
logicPacketSuffix = ".packet"
|
||||
sessionOnSuffix = ".session.connected"
|
||||
sessionOffSuffix = ".session.disconnected"
|
||||
topicGwNodePrefix = "gateway.node."
|
||||
topicGwPushSuffix = ".push"
|
||||
topicGwRespSuffix = ".response"
|
||||
topicRPCNodePrefix = "rpc.node."
|
||||
topicRPCReqSuffix = ".request"
|
||||
topicRPCRespPrefix = "rpc.response."
|
||||
topicRPCRespSuffix = ".reply"
|
||||
)
|
||||
|
||||
// ScopedTopic 是解析后的作用域 topic 结构。
|
||||
type ScopedTopic struct {
|
||||
Scope TopicScope `json:"scope"`
|
||||
Service string `json:"service,omitempty"`
|
||||
ZoneID string `json:"zone_id,omitempty"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
ShardID string `json:"shard_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// LogicNodePacketTopic 返回指定 logic 节点接收客户端包的 topic。
|
||||
func LogicNodePacketTopic(nodeID string) string {
|
||||
return logicNodeTopic(nodeID, logicPacketSuffix)
|
||||
}
|
||||
|
||||
// LogicSessConnTopic 返回指定 logic 节点接收会话上线事件的 topic。
|
||||
func LogicSessConnTopic(nodeID string) string {
|
||||
return logicNodeTopic(nodeID, sessionOnSuffix)
|
||||
}
|
||||
|
||||
// LogicSessDiscTopic 返回指定 logic 节点接收会话下线事件的 topic。
|
||||
func LogicSessDiscTopic(nodeID string) string {
|
||||
return logicNodeTopic(nodeID, sessionOffSuffix)
|
||||
}
|
||||
|
||||
// IsLogicNodePacketTopic 判断 topic 是否为 logic 节点客户端包 topic。
|
||||
func IsLogicNodePacketTopic(topic string) bool {
|
||||
return isLogicNodeTopic(topic, logicPacketSuffix)
|
||||
}
|
||||
|
||||
// IsLogicSessConnTopic 判断 topic 是否为 logic 节点会话上线 topic。
|
||||
func IsLogicSessConnTopic(topic string) bool {
|
||||
return isLogicNodeTopic(topic, sessionOnSuffix)
|
||||
}
|
||||
|
||||
// IsLogicSessDiscTopic 判断 topic 是否为 logic 节点会话下线 topic。
|
||||
func IsLogicSessDiscTopic(topic string) bool {
|
||||
return isLogicNodeTopic(topic, sessionOffSuffix)
|
||||
}
|
||||
|
||||
// GatewayNodePushTopic 返回指定 gateway 节点接收推送的 topic。
|
||||
func GatewayNodePushTopic(nodeID string) string {
|
||||
return nodeTopic(topicGwNodePrefix, nodeID, topicGwPushSuffix)
|
||||
}
|
||||
|
||||
// IsGatewayNodePushTopic 判断 topic 是否为 gateway 节点推送 topic。
|
||||
func IsGatewayNodePushTopic(topic string) bool {
|
||||
return isNodeTopic(topic, topicGwNodePrefix, topicGwPushSuffix)
|
||||
}
|
||||
|
||||
// GatewayNodeRespTopic 返回指定 gateway 节点接收 logic 玩家响应的 topic。
|
||||
func GatewayNodeRespTopic(nodeID string) string {
|
||||
return nodeTopic(topicGwNodePrefix, nodeID, topicGwRespSuffix)
|
||||
}
|
||||
|
||||
// IsGatewayNodeRespTopic 判断 topic 是否为 gateway 节点玩家响应 topic。
|
||||
func IsGatewayNodeRespTopic(topic string) bool {
|
||||
return isNodeTopic(topic, topicGwNodePrefix, topicGwRespSuffix)
|
||||
}
|
||||
|
||||
// RPCNodeRequestTopic 返回指定节点接收 RPC 请求的 topic。
|
||||
func RPCNodeRequestTopic(nodeID string) string {
|
||||
return nodeTopic(topicRPCNodePrefix, nodeID, topicRPCReqSuffix)
|
||||
}
|
||||
|
||||
// RPCResponseTopic 返回指定节点接收 RPC 响应的 topic。
|
||||
func RPCResponseTopic(nodeID string) string {
|
||||
return nodeTopic(topicRPCRespPrefix, nodeID, topicRPCRespSuffix)
|
||||
}
|
||||
|
||||
// IsRPCNodeRequestTopic 判断 topic 是否为节点 RPC 请求 topic。
|
||||
func IsRPCNodeRequestTopic(topic string) bool {
|
||||
return isNodeTopic(topic, topicRPCNodePrefix, topicRPCReqSuffix)
|
||||
}
|
||||
|
||||
// IsRPCResponseTopic 判断 topic 是否为节点 RPC 响应 topic。
|
||||
func IsRPCResponseTopic(topic string) bool {
|
||||
return isNodeTopic(topic, topicRPCRespPrefix, topicRPCRespSuffix)
|
||||
}
|
||||
|
||||
// ServiceScopedTopic 构造服务作用域 topic。
|
||||
func ServiceScopedTopic(service string, groupID string, shardID string, name string) string {
|
||||
return scopedTopic("service", service, groupID, shardID, name)
|
||||
}
|
||||
|
||||
// ZoneScopedTopic 构造区服作用域 topic。
|
||||
func ZoneScopedTopic(zoneID string, name string) string {
|
||||
return scopedTopic("zone", zoneID, "", "", name)
|
||||
}
|
||||
|
||||
// ShardScopedTopic 构造分片作用域 topic。
|
||||
func ShardScopedTopic(shardID string, name string) string {
|
||||
return scopedTopic("shard", shardID, "", "", name)
|
||||
}
|
||||
|
||||
// ParseScopedTopic 解析服务、区服或分片作用域 topic。
|
||||
func ParseScopedTopic(topic string) (ScopedTopic, bool) {
|
||||
parts := strings.Split(strings.TrimSpace(topic), ".")
|
||||
if len(parts) < 2 || hasEmptyPart(parts) {
|
||||
return ScopedTopic{}, false
|
||||
}
|
||||
switch TopicScope(parts[0]) {
|
||||
case TopicScopeService:
|
||||
return parseScopedTopic(parts)
|
||||
case TopicScopeZone:
|
||||
return parseOneScopedTopic(TopicScopeZone, parts)
|
||||
case TopicScopeShard:
|
||||
return parseOneScopedTopic(TopicScopeShard, parts)
|
||||
default:
|
||||
return ScopedTopic{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func logicNodeTopic(nodeID, suffix string) string {
|
||||
return nodeTopic(topicLogicPrefix, nodeID, suffix)
|
||||
}
|
||||
|
||||
func nodeTopic(prefix, nodeID, suffix string) string {
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
if nodeID == "" {
|
||||
return ""
|
||||
}
|
||||
return prefix + nodeID + suffix
|
||||
}
|
||||
|
||||
func isLogicNodeTopic(topic, suffix string) bool {
|
||||
return isNodeTopic(topic, topicLogicPrefix, suffix)
|
||||
}
|
||||
|
||||
func isNodeTopic(topic, prefix, suffix string) bool {
|
||||
if prefix == "" || suffix == "" {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(topic, prefix) || !strings.HasSuffix(topic, suffix) {
|
||||
return false
|
||||
}
|
||||
nodeID := strings.TrimSuffix(strings.TrimPrefix(topic, prefix), suffix)
|
||||
return strings.TrimSpace(nodeID) != ""
|
||||
}
|
||||
|
||||
func scopedTopic(scope string, first string, groupID string, shardID string, name string) string {
|
||||
parts := []string{strings.TrimSpace(scope), strings.TrimSpace(first)}
|
||||
if groupID = strings.TrimSpace(groupID); groupID != "" {
|
||||
parts = append(parts, "gid", groupID)
|
||||
}
|
||||
if shardID = strings.TrimSpace(shardID); shardID != "" {
|
||||
parts = append(parts, "sid", shardID)
|
||||
}
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
parts = append(parts, name)
|
||||
}
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ".")
|
||||
}
|
||||
|
||||
func parseScopedTopic(parts []string) (ScopedTopic, bool) {
|
||||
if len(parts) < 2 {
|
||||
return ScopedTopic{}, false
|
||||
}
|
||||
out := ScopedTopic{Scope: TopicScopeService, Service: parts[1]}
|
||||
idx := 2
|
||||
for idx < len(parts) {
|
||||
switch parts[idx] {
|
||||
case "gid":
|
||||
if idx+1 >= len(parts) || out.GroupID != "" {
|
||||
return ScopedTopic{}, false
|
||||
}
|
||||
out.GroupID = parts[idx+1]
|
||||
idx += 2
|
||||
case "sid":
|
||||
if idx+1 >= len(parts) || out.ShardID != "" {
|
||||
return ScopedTopic{}, false
|
||||
}
|
||||
out.ShardID = parts[idx+1]
|
||||
idx += 2
|
||||
default:
|
||||
out.Name = strings.Join(parts[idx:], ".")
|
||||
return out, true
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func parseOneScopedTopic(scope TopicScope, parts []string) (ScopedTopic, bool) {
|
||||
if len(parts) < 2 {
|
||||
return ScopedTopic{}, false
|
||||
}
|
||||
out := ScopedTopic{Scope: scope}
|
||||
if scope == TopicScopeZone {
|
||||
out.ZoneID = parts[1]
|
||||
} else {
|
||||
out.ShardID = parts[1]
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
out.Name = strings.Join(parts[2:], ".")
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func hasEmptyPart(parts []string) bool {
|
||||
for _, part := range parts {
|
||||
if strings.TrimSpace(part) == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package protocol
|
||||
|
||||
import "time"
|
||||
|
||||
// AuthResponse 是认证成功后返回给客户端的登录令牌响应。
|
||||
type AuthResponse struct {
|
||||
Result string `json:"result"`
|
||||
Authtoken string `json:"authtoken"`
|
||||
ShardRole []ShardRole `json:"shardrole"`
|
||||
}
|
||||
|
||||
// ShardRole 描述账号在单个分区里的已有角色。
|
||||
type ShardRole struct {
|
||||
Shard string `json:"shard"`
|
||||
Name string `json:"name"`
|
||||
LLT int64 `json:"llt"`
|
||||
}
|
||||
|
||||
// ShardListResponse 是分区列表接口响应。
|
||||
type ShardListResponse struct {
|
||||
Shards []ShardDescriptor `json:"shards"`
|
||||
}
|
||||
|
||||
// ShardDescriptor 描述客户端选区页展示的单个分区。
|
||||
type ShardDescriptor struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"dn"`
|
||||
ShowState int32 `json:"ss"`
|
||||
Recommend int32 `json:"recmd"`
|
||||
RecommendLang string `json:"recmd_lang"`
|
||||
VGID int32 `json:"v_gid"`
|
||||
MainlineLevel string `json:"mainline_level"`
|
||||
}
|
||||
|
||||
// GetGateResponse 是网关寻址接口响应。
|
||||
type GetGateResponse struct {
|
||||
Result string `json:"result"`
|
||||
IP string `json:"ip"`
|
||||
LoginToken string `json:"logintoken"`
|
||||
}
|
||||
|
||||
// NoticeResponse 是登录前公告和端点发现响应。
|
||||
type NoticeResponse struct {
|
||||
Endpoint map[string]string `json:"Endpoint"`
|
||||
ServerTime string `json:"ServerTime"`
|
||||
TimeoutTime string `json:"TimeoutTime"`
|
||||
TokenTime string `json:"TokenTime"`
|
||||
Publics []any `json:"Publics"`
|
||||
Chatserver string `json:"Chatserver"`
|
||||
ChatIp string `json:"chatIp"`
|
||||
NoticeCustomer string `json:"NoticeCustomer"`
|
||||
WinScanSwitch int `json:"WinScanSwitch"`
|
||||
AndroidPrivacySwitch string `json:"AndroidPrivacySwitch"`
|
||||
IosPrivacySwitch string `json:"IosPrivacySwitch"`
|
||||
ClientExpireTime string `json:"clientExpireTime"`
|
||||
}
|
||||
|
||||
// BuildAuthOK 构造认证成功响应。
|
||||
func BuildAuthOK(authToken string) AuthResponse {
|
||||
return AuthResponse{
|
||||
Result: "ok",
|
||||
Authtoken: authToken,
|
||||
ShardRole: []ShardRole{},
|
||||
}
|
||||
}
|
||||
|
||||
// BuildShardList 构造分区列表响应并隔离调用方切片。
|
||||
func BuildShardList(shards []ShardDescriptor) ShardListResponse {
|
||||
return ShardListResponse{Shards: append([]ShardDescriptor(nil), shards...)}
|
||||
}
|
||||
|
||||
// BuildGetGate 构造网关寻址响应。
|
||||
func BuildGetGate(ip, loginToken string) GetGateResponse {
|
||||
return GetGateResponse{
|
||||
Result: "ok",
|
||||
IP: ip,
|
||||
LoginToken: loginToken,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildNotice 构造公告响应并隔离端点表。
|
||||
func BuildNotice(endpoints map[string]string, chatListen string, now time.Time) NoticeResponse {
|
||||
var copiedEndpoints map[string]string
|
||||
if endpoints != nil {
|
||||
copiedEndpoints = make(map[string]string, len(endpoints))
|
||||
for key, value := range endpoints {
|
||||
copiedEndpoints[key] = value
|
||||
}
|
||||
}
|
||||
return NoticeResponse{
|
||||
Endpoint: copiedEndpoints,
|
||||
ServerTime: formatUnix(now),
|
||||
TimeoutTime: "30",
|
||||
TokenTime: "12900",
|
||||
Publics: []any{},
|
||||
Chatserver: chatListen,
|
||||
ChatIp: chatListen,
|
||||
NoticeCustomer: "",
|
||||
WinScanSwitch: 0,
|
||||
AndroidPrivacySwitch: "false",
|
||||
IosPrivacySwitch: "false",
|
||||
ClientExpireTime: "0",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package protocol 定义 LongHeng 公开 gateway wire format。
|
||||
//
|
||||
// 这里包含 packet envelope、frame 编码、登录契约、校验和/加密辅助逻辑以及生成的玩法 schema。
|
||||
// 任何变更都应视为 SDK 和向后兼容性决策。
|
||||
package protocol
|
||||
@@ -0,0 +1,194 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
// ErrorCode 是协议层向客户端暴露的稳定错误码。
|
||||
type ErrorCode int32
|
||||
|
||||
// 协议错误码分组保留 HTTP 语义,并扩展框架内部错误。
|
||||
const (
|
||||
CodeOK ErrorCode = 0
|
||||
CodeBadRequest ErrorCode = 400
|
||||
CodeUnauthorized ErrorCode = 401
|
||||
CodeNotFound ErrorCode = 404
|
||||
CodeConflict ErrorCode = 409
|
||||
CodeInternal ErrorCode = 500
|
||||
CodeUnavailable ErrorCode = 503
|
||||
CodeNotImplemented ErrorCode = 1001
|
||||
)
|
||||
|
||||
// AppError 是服务内部携带错误码、公开消息和调试明细的错误。
|
||||
type AppError struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse 是发送给客户端的错误响应体。
|
||||
type ErrorResponse struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
ServerTime int64 `json:"server_time"`
|
||||
}
|
||||
|
||||
// NewError 构造带协议错误码的应用错误。
|
||||
func NewError(code ErrorCode, message string) *AppError {
|
||||
return &AppError{Code: code, Message: message}
|
||||
}
|
||||
|
||||
// Errorf 使用格式化消息构造应用错误。
|
||||
func Errorf(code ErrorCode, format string, args ...any) *AppError {
|
||||
return NewError(code, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// WrapError 把底层错误明细保存在应用错误里。
|
||||
func WrapError(code ErrorCode, message string, err error) *AppError {
|
||||
if err == nil {
|
||||
return NewError(code, message)
|
||||
}
|
||||
return &AppError{Code: code, Message: message, Detail: err.Error()}
|
||||
}
|
||||
|
||||
// ErrorFrom 把任意 error 归一成 AppError。
|
||||
func ErrorFrom(err error) AppError {
|
||||
if err == nil {
|
||||
return AppError{Code: CodeOK}
|
||||
}
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) && appErr != nil {
|
||||
return *appErr
|
||||
}
|
||||
return AppError{
|
||||
Code: CodeInternal,
|
||||
Message: "internal server error",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// PublicErrorResponse 生成可安全返回给客户端的错误响应。
|
||||
func PublicErrorResponse(err AppError, serverTime int64) ErrorResponse {
|
||||
resp := ErrorResponse{
|
||||
Code: err.Code,
|
||||
Message: err.Message,
|
||||
Detail: err.Detail,
|
||||
ServerTime: serverTime,
|
||||
}
|
||||
if resp.Message == "" {
|
||||
resp.Message = err.Code.String()
|
||||
}
|
||||
switch err.Code {
|
||||
case CodeInternal:
|
||||
resp.Message = "internal server error"
|
||||
case CodeUnavailable:
|
||||
resp.Message = "service unavailable"
|
||||
case CodeUnauthorized:
|
||||
resp.Message = "unauthorized"
|
||||
}
|
||||
switch err.Code {
|
||||
case CodeBadRequest:
|
||||
default:
|
||||
resp.Detail = ""
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// Error 返回应用错误的日志友好文本。
|
||||
func (e *AppError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Detail == "" {
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s: %s", e.Code, e.Message, e.Detail)
|
||||
}
|
||||
|
||||
// String 返回错误码的协议文本。
|
||||
func (c ErrorCode) String() string {
|
||||
switch c {
|
||||
case CodeOK:
|
||||
return "OK"
|
||||
case CodeBadRequest:
|
||||
return "BAD_REQUEST"
|
||||
case CodeUnauthorized:
|
||||
return "UNAUTHORIZED"
|
||||
case CodeNotFound:
|
||||
return "NOT_FOUND"
|
||||
case CodeConflict:
|
||||
return "CONFLICT"
|
||||
case CodeInternal:
|
||||
return "INTERNAL"
|
||||
case CodeUnavailable:
|
||||
return "UNAVAILABLE"
|
||||
case CodeNotImplemented:
|
||||
return "NOT_IMPLEMENTED"
|
||||
default:
|
||||
return fmt.Sprintf("ERROR_%d", c)
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeErrorResponse 把错误响应编码为轻量 protobuf wire body。
|
||||
func EncodeErrorResponse(resp ErrorResponse) []byte {
|
||||
body := make([]byte, 0, 96)
|
||||
body = appendI32VarintField(body, 1, int32(resp.Code))
|
||||
body = appendStringField(body, 2, resp.Message)
|
||||
body = appendStringField(body, 3, resp.Detail)
|
||||
body = appendInt64Varint(body, 4, resp.ServerTime)
|
||||
return body
|
||||
}
|
||||
|
||||
// DecodeErrorResponse 从 protobuf wire body 解码错误响应。
|
||||
func DecodeErrorResponse(body []byte) ErrorResponse {
|
||||
var resp ErrorResponse
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return resp
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.VarintType:
|
||||
value, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return resp
|
||||
}
|
||||
resp.Code = ErrorCode(int32FromVarint(value))
|
||||
body = body[n:]
|
||||
case fieldNumber == 2 && typ == protowire.BytesType:
|
||||
value, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return resp
|
||||
}
|
||||
resp.Message = value
|
||||
body = body[n:]
|
||||
case fieldNumber == 3 && typ == protowire.BytesType:
|
||||
value, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return resp
|
||||
}
|
||||
resp.Detail = value
|
||||
body = body[n:]
|
||||
case fieldNumber == 4 && typ == protowire.VarintType:
|
||||
value, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return resp
|
||||
}
|
||||
resp.ServerTime = int64FromVarint(value)
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return resp
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 帧协议基础常量定义固定头、版本和默认 body 上限。
|
||||
const (
|
||||
FrameMagic uint16 = 0x5047 // 固定帧协议探测魔数:"GP",不是品牌缩写。
|
||||
FrameVersion1 uint8 = 1
|
||||
FrameHeaderSizeV1 = 32
|
||||
MaxFrameBodySize uint32 = 4 * 1024 * 1024
|
||||
)
|
||||
|
||||
// 标准帧 metadata 键名;链路字段兼容 W3C trace context。
|
||||
const (
|
||||
FrameMetadataTraceParent = "traceparent"
|
||||
FrameMetadataTraceState = "tracestate"
|
||||
FrameMetadataBaggage = "baggage"
|
||||
// FrameMetadataIdempotencyKey 由客户端为一次业务请求生成,重试和重连时保持不变。
|
||||
FrameMetadataIdempotencyKey = "idempotency-key"
|
||||
|
||||
frameMetadataMagic = "MD"
|
||||
frameMetadataVersion = 1
|
||||
frameMaxHeaderSize = 255
|
||||
)
|
||||
|
||||
// 帧标志位描述 body 的压缩、加密和校验状态。
|
||||
const (
|
||||
FrameFlagCompressed uint16 = 1 << iota
|
||||
FrameFlagEncrypted
|
||||
FrameFlagChecksum
|
||||
)
|
||||
|
||||
// 帧 codec 标识 body 的序列化格式。
|
||||
const (
|
||||
FrameCodecRaw uint16 = iota
|
||||
FrameCodecProtobuf
|
||||
FrameCodecJSON
|
||||
)
|
||||
|
||||
// 帧解析和校验错误。
|
||||
var (
|
||||
ErrInvalidFrameMagic = errors.New("protocol frame magic is invalid")
|
||||
ErrInvalidFrameVersion = errors.New("protocol frame version is invalid")
|
||||
ErrInvalidFrameHeader = errors.New("protocol frame header is invalid")
|
||||
ErrFrameBodyTooLarge = errors.New("protocol frame body is too large")
|
||||
ErrFrameChecksumInvalid = errors.New("protocol frame checksum is invalid")
|
||||
ErrFrameMetadataInvalid = errors.New("protocol frame metadata is invalid")
|
||||
ErrFrameMetadataTooLarge = errors.New("protocol frame metadata is too large")
|
||||
)
|
||||
|
||||
// Frame 是 gateway frame_v1 线协议的内存表示。
|
||||
type Frame struct {
|
||||
Version uint8
|
||||
Flags uint16
|
||||
Codec uint16
|
||||
PacketID int32
|
||||
MsgID uint32
|
||||
Sequence uint64
|
||||
Checksum uint32
|
||||
Body []byte
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
// FrameFromPacket 把旧 packet 包装为 frame_v1。
|
||||
func FrameFromPacket(pkt Packet, sequence uint64) Frame {
|
||||
flags := uint16(0)
|
||||
if pkt.Compressed {
|
||||
flags |= FrameFlagCompressed
|
||||
}
|
||||
return Frame{
|
||||
Version: FrameVersion1,
|
||||
Flags: flags,
|
||||
Codec: FrameCodecRaw,
|
||||
PacketID: pkt.PacketID,
|
||||
MsgID: pkt.MsgID,
|
||||
Sequence: sequence,
|
||||
Body: append([]byte(nil), pkt.Body...),
|
||||
}
|
||||
}
|
||||
|
||||
// Packet 把 frame 还原成旧 packet 视图并拷贝 body。
|
||||
func (f Frame) Packet() Packet {
|
||||
return Packet{
|
||||
PacketID: f.PacketID,
|
||||
MsgID: f.MsgID,
|
||||
Compressed: f.Flags&FrameFlagCompressed != 0,
|
||||
Body: append([]byte(nil), f.Body...),
|
||||
}
|
||||
}
|
||||
|
||||
func packetFromFrame(frame Frame) Packet {
|
||||
return Packet{
|
||||
PacketID: frame.PacketID,
|
||||
MsgID: frame.MsgID,
|
||||
Compressed: frame.Flags&FrameFlagCompressed != 0,
|
||||
Body: frame.Body,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFrame 从 reader 读取并校验一个 frame_v1 帧。
|
||||
func ReadFrame(r io.Reader, maxBodySize uint32) (Frame, error) {
|
||||
maxBodySize = normMaxFrameBody(maxBodySize)
|
||||
var header [FrameHeaderSizeV1]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
if binary.LittleEndian.Uint16(header[0:2]) != FrameMagic {
|
||||
return Frame{}, ErrInvalidFrameMagic
|
||||
}
|
||||
version := header[2]
|
||||
if version != FrameVersion1 {
|
||||
return Frame{}, fmt.Errorf("%w: %d", ErrInvalidFrameVersion, version)
|
||||
}
|
||||
headerSize := int(header[3])
|
||||
if headerSize < FrameHeaderSizeV1 {
|
||||
return Frame{}, fmt.Errorf("%w: size=%d", ErrInvalidFrameHeader, headerSize)
|
||||
}
|
||||
var metadataHeader []byte
|
||||
if headerSize > FrameHeaderSizeV1 {
|
||||
metadataHeader = make([]byte, headerSize-FrameHeaderSizeV1)
|
||||
if _, err := io.ReadFull(r, metadataHeader); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
}
|
||||
metadata, err := decodeFrameMeta(metadataHeader)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
|
||||
bodySize := binary.LittleEndian.Uint32(header[24:28])
|
||||
if bodySize > maxBodySize {
|
||||
return Frame{}, fmt.Errorf("%w: %d", ErrFrameBodyTooLarge, bodySize)
|
||||
}
|
||||
body := make([]byte, bodySize)
|
||||
if bodySize > 0 {
|
||||
if _, err := io.ReadFull(r, body); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
}
|
||||
|
||||
frame := Frame{
|
||||
Version: version,
|
||||
Flags: binary.LittleEndian.Uint16(header[4:6]),
|
||||
Codec: binary.LittleEndian.Uint16(header[6:8]),
|
||||
PacketID: packetIDFromWire(binary.LittleEndian.Uint32(header[8:12])),
|
||||
MsgID: binary.LittleEndian.Uint32(header[12:16]),
|
||||
Sequence: binary.LittleEndian.Uint64(header[16:24]),
|
||||
Checksum: binary.LittleEndian.Uint32(header[28:32]),
|
||||
Body: body,
|
||||
Metadata: metadata,
|
||||
}
|
||||
if frame.Flags&FrameFlagChecksum != 0 && crc32.ChecksumIEEE(body) != frame.Checksum {
|
||||
return Frame{}, ErrFrameChecksumInvalid
|
||||
}
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
// WriteFrame 把 frame_v1 帧写入 writer。
|
||||
func WriteFrame(w io.Writer, frame Frame) error {
|
||||
version := frame.Version
|
||||
if version == 0 {
|
||||
version = FrameVersion1
|
||||
}
|
||||
if version != FrameVersion1 {
|
||||
return fmt.Errorf("%w: %d", ErrInvalidFrameVersion, version)
|
||||
}
|
||||
bodySize := len(frame.Body)
|
||||
if bodySize > int(MaxFrameBodySize) || uint64(bodySize) > uint64(^uint32(0)) {
|
||||
return fmt.Errorf("%w: %d", ErrFrameBodyTooLarge, bodySize)
|
||||
}
|
||||
metadataHeader, err := encodeFrameMeta(frame.Metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headerSize := FrameHeaderSizeV1 + len(metadataHeader)
|
||||
if headerSize > frameMaxHeaderSize {
|
||||
return fmt.Errorf("%w: header_size=%d", ErrFrameMetadataTooLarge, headerSize)
|
||||
}
|
||||
if frame.Flags&FrameFlagChecksum != 0 {
|
||||
frame.Checksum = crc32.ChecksumIEEE(frame.Body)
|
||||
} else {
|
||||
frame.Checksum = 0
|
||||
}
|
||||
|
||||
var header [FrameHeaderSizeV1]byte
|
||||
binary.LittleEndian.PutUint16(header[0:2], FrameMagic)
|
||||
header[2] = version
|
||||
header[3] = uint8(headerSize) //nolint:gosec // G115:headerSize 已校验不超过 frameMaxHeaderSize。
|
||||
binary.LittleEndian.PutUint16(header[4:6], frame.Flags)
|
||||
binary.LittleEndian.PutUint16(header[6:8], frame.Codec)
|
||||
binary.LittleEndian.PutUint32(header[8:12], packetIDToWire(frame.PacketID))
|
||||
binary.LittleEndian.PutUint32(header[12:16], frame.MsgID)
|
||||
binary.LittleEndian.PutUint64(header[16:24], frame.Sequence)
|
||||
binary.LittleEndian.PutUint32(header[24:28], uint32(bodySize))
|
||||
binary.LittleEndian.PutUint32(header[28:32], frame.Checksum)
|
||||
|
||||
if bodySize == 0 {
|
||||
return writeFullBuffers(w, header[:], metadataHeader)
|
||||
}
|
||||
return writeFullBuffers(w, header[:], metadataHeader, frame.Body)
|
||||
}
|
||||
|
||||
func normMaxFrameBody(maxBodySize uint32) uint32 {
|
||||
if maxBodySize == 0 {
|
||||
return MaxFrameBodySize
|
||||
}
|
||||
return maxBodySize
|
||||
}
|
||||
|
||||
func packetIDFromWire(value uint32) int32 {
|
||||
return int32(value) //nolint:gosec // G115:协议线格式用 uint32 保存 int32 的补码位模式。
|
||||
}
|
||||
|
||||
func packetIDToWire(value int32) uint32 {
|
||||
return uint32(value) //nolint:gosec // G115:协议线格式用 uint32 保存 int32 的补码位模式。
|
||||
}
|
||||
|
||||
// TraceContextMetadata 构造 frame metadata 中的链路追踪字段。
|
||||
func TraceContextMetadata(traceParent, traceState string) map[string]string {
|
||||
out := make(map[string]string, 2)
|
||||
if traceParent = strings.TrimSpace(traceParent); traceParent != "" {
|
||||
out[FrameMetadataTraceParent] = traceParent
|
||||
}
|
||||
if traceState = strings.TrimSpace(traceState); traceState != "" {
|
||||
out[FrameMetadataTraceState] = traceState
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CloneFrameMetadata 规范化并拷贝 frame metadata。
|
||||
func CloneFrameMetadata(metadata map[string]string) map[string]string {
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(metadata))
|
||||
for key, value := range metadata {
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
value = strings.TrimSpace(value)
|
||||
if key == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func encodeFrameMeta(metadata map[string]string) ([]byte, error) {
|
||||
metadata = CloneFrameMetadata(metadata)
|
||||
if len(metadata) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(metadata) > 255 {
|
||||
return nil, fmt.Errorf("%w: entries=%d", ErrFrameMetadataTooLarge, len(metadata))
|
||||
}
|
||||
keys := make([]string, 0, len(metadata))
|
||||
for key := range metadata {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
out := []byte{frameMetadataMagic[0], frameMetadataMagic[1], frameMetadataVersion, byte(len(keys))} //nolint:gosec // G115:metadata 条目数已限制在 255 以内。
|
||||
for _, key := range keys {
|
||||
value := metadata[key]
|
||||
if len(key) > 255 || len(value) > 255 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrFrameMetadataTooLarge, key)
|
||||
}
|
||||
out = append(out, byte(len(key)), byte(len(value))) //nolint:gosec // G115:key/value 长度已限制在 255 以内。
|
||||
out = append(out, key...)
|
||||
out = append(out, value...)
|
||||
if FrameHeaderSizeV1+len(out) > frameMaxHeaderSize {
|
||||
return nil, fmt.Errorf("%w: header_size=%d", ErrFrameMetadataTooLarge, FrameHeaderSizeV1+len(out))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func decodeFrameMeta(extra []byte) (map[string]string, error) {
|
||||
if len(extra) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(extra) < 4 || string(extra[:2]) != frameMetadataMagic {
|
||||
return nil, ErrFrameMetadataInvalid
|
||||
}
|
||||
if extra[2] != frameMetadataVersion {
|
||||
return nil, fmt.Errorf("%w: version=%d", ErrFrameMetadataInvalid, extra[2])
|
||||
}
|
||||
count := int(extra[3])
|
||||
pos := 4
|
||||
out := make(map[string]string, count)
|
||||
for idx := 0; idx < count; idx++ {
|
||||
if pos+2 > len(extra) {
|
||||
return nil, ErrFrameMetadataInvalid
|
||||
}
|
||||
keyLen := int(extra[pos])
|
||||
valueLen := int(extra[pos+1])
|
||||
pos += 2
|
||||
if keyLen == 0 || valueLen == 0 || pos+keyLen+valueLen > len(extra) {
|
||||
return nil, ErrFrameMetadataInvalid
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(string(extra[pos : pos+keyLen])))
|
||||
pos += keyLen
|
||||
value := strings.TrimSpace(string(extra[pos : pos+valueLen]))
|
||||
pos += valueLen
|
||||
if key == "" || value == "" {
|
||||
return nil, ErrFrameMetadataInvalid
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
if pos != len(extra) {
|
||||
return nil, ErrFrameMetadataInvalid
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hkdf"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// frame 加密参数使用 AES-GCM 推荐的 32 字节 key 和 12 字节 nonce。
|
||||
const (
|
||||
FrameSessionKeySize = 32
|
||||
FrameNonceSize = 12
|
||||
)
|
||||
|
||||
// frame 加密和重放保护错误。
|
||||
var (
|
||||
ErrFrameEncryptionKeyInvalid = errors.New("protocol frame encryption key is invalid")
|
||||
ErrFrameEncryptedPayload = errors.New("protocol frame encrypted payload is invalid")
|
||||
ErrFrameNonceReplay = errors.New("protocol frame nonce replay detected")
|
||||
ErrFrameReplayGuardFull = errors.New("protocol frame replay guard is full")
|
||||
)
|
||||
|
||||
// DeriveFrameSessionKey 从主密钥和会话 ID 派生 frame 会话密钥。
|
||||
func DeriveFrameSessionKey(masterSecret, sessionID string) ([]byte, error) {
|
||||
if masterSecret == "" || sessionID == "" {
|
||||
return nil, ErrFrameEncryptionKeyInvalid
|
||||
}
|
||||
key, err := hkdf.Key(sha256.New, []byte(masterSecret), []byte(sessionID), "longheng:frame-session-key:v1", FrameSessionKeySize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptFrame 加密 frame body,并把 nonce 放入密文前缀。
|
||||
func EncryptFrame(frame Frame, key []byte, random io.Reader) (Frame, error) {
|
||||
if frame.Version == 0 {
|
||||
// 加密 AAD 必须与最终写出的帧头一致;WriteFrame 会把 0 规范成 FrameVersion1。
|
||||
frame.Version = FrameVersion1
|
||||
}
|
||||
if frame.Version != FrameVersion1 {
|
||||
return Frame{}, fmt.Errorf("%w: %d", ErrInvalidFrameVersion, frame.Version)
|
||||
}
|
||||
aead, err := frameAEAD(key)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
nonce := make([]byte, FrameNonceSize)
|
||||
binary.BigEndian.PutUint64(nonce[:8], frame.Sequence)
|
||||
if _, err := io.ReadFull(random, nonce[8:]); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
out := frame
|
||||
out.Flags |= FrameFlagEncrypted
|
||||
aad, err := frameAAD(frame)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
out.Body = aead.Seal(nonce, nonce, frame.Body, aad)
|
||||
out.Checksum = 0
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WriteEncryptedFrame 加密后写出 frame_v1。
|
||||
func WriteEncryptedFrame(w io.Writer, frame Frame, key []byte, random io.Reader) error {
|
||||
encrypted, err := EncryptFrame(frame, key, random)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteFrame(w, encrypted)
|
||||
}
|
||||
|
||||
// DecryptFrame 解密 frame body 并执行可选 nonce 重放检查。
|
||||
func DecryptFrame(frame Frame, key []byte, replay *FrameReplayGuard) (Frame, error) {
|
||||
if frame.Flags&FrameFlagEncrypted == 0 {
|
||||
return frame, nil
|
||||
}
|
||||
aead, err := frameAEAD(key)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
if len(frame.Body) < FrameNonceSize+aead.Overhead() {
|
||||
return Frame{}, ErrFrameEncryptedPayload
|
||||
}
|
||||
nonce := append([]byte(nil), frame.Body[:FrameNonceSize]...)
|
||||
aad, err := frameAAD(frame)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
plain, err := aead.Open(nil, nonce, frame.Body[FrameNonceSize:], aad)
|
||||
if err != nil {
|
||||
return Frame{}, fmt.Errorf("%w: %w", ErrFrameEncryptedPayload, err)
|
||||
}
|
||||
if replay != nil {
|
||||
if err := replay.Seen(nonce); err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
}
|
||||
out := frame
|
||||
out.Flags &^= FrameFlagEncrypted
|
||||
out.Body = plain
|
||||
out.Checksum = 0
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReadEncryptedFrame 读取 frame_v1 后立即解密。
|
||||
func ReadEncryptedFrame(r io.Reader, maxBodySize uint32, key []byte, replay *FrameReplayGuard) (Frame, error) {
|
||||
frame, err := ReadFrame(r, maxBodySize)
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
return DecryptFrame(frame, key, replay)
|
||||
}
|
||||
|
||||
// EncryptEnvelope 把 envelope 按 frame_v1 规则加密。
|
||||
func EncryptEnvelope(envelope Envelope, key []byte, random io.Reader) (Envelope, error) {
|
||||
if NormalizeWireFormat(envelope.Format) != WireFormatFrameV1 {
|
||||
return Envelope{}, ErrInvalidWireFormat
|
||||
}
|
||||
frame := Frame{
|
||||
Version: envelope.Frame.Version,
|
||||
Flags: envelope.Frame.Flags,
|
||||
Codec: envelope.Frame.Codec,
|
||||
PacketID: envelope.Packet.PacketID,
|
||||
MsgID: envelope.Packet.MsgID,
|
||||
Sequence: envelope.Frame.Sequence,
|
||||
Body: append([]byte(nil), envelope.Packet.Body...),
|
||||
Metadata: CloneFrameMetadata(envelope.Frame.Metadata),
|
||||
}
|
||||
if envelope.Packet.Compressed {
|
||||
frame.Flags |= FrameFlagCompressed
|
||||
}
|
||||
encrypted, err := EncryptFrame(frame, key, random)
|
||||
if err != nil {
|
||||
return Envelope{}, err
|
||||
}
|
||||
return Envelope{
|
||||
Format: WireFormatFrameV1,
|
||||
Packet: packetFromFrame(encrypted),
|
||||
Frame: encrypted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecryptEnvelope 解密 frame_v1 envelope,旧 packet envelope 直接透传。
|
||||
func DecryptEnvelope(envelope Envelope, key []byte, replay *FrameReplayGuard) (Envelope, error) {
|
||||
switch NormalizeWireFormat(envelope.Format) {
|
||||
case WireFormatPacket:
|
||||
return envelope, nil
|
||||
case WireFormatFrameV1:
|
||||
default:
|
||||
return Envelope{}, ErrInvalidWireFormat
|
||||
}
|
||||
if envelope.Frame.Flags&FrameFlagEncrypted == 0 {
|
||||
return envelope, nil
|
||||
}
|
||||
decrypted, err := DecryptFrame(envelope.Frame, key, replay)
|
||||
if err != nil {
|
||||
return Envelope{}, err
|
||||
}
|
||||
return Envelope{
|
||||
Format: WireFormatFrameV1,
|
||||
Packet: packetFromFrame(decrypted),
|
||||
Frame: decrypted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FrameReplayGuard 在有界序号窗口内精确记录 nonce,防止同一会话内加密帧重放。
|
||||
//
|
||||
// nonce 前 8 字节是大端 frame sequence,后 4 字节是随机后缀。同一 sequence
|
||||
// 可能因响应和 push 编号空间重叠而出现不同 nonce,因此窗口内按完整 nonce
|
||||
// 去重;一旦 sequence 滑出窗口,其任何 nonce 都永久按重放拒绝。
|
||||
type FrameReplayGuard struct {
|
||||
mu sync.Mutex
|
||||
max int
|
||||
initialized bool
|
||||
highest uint64
|
||||
total int
|
||||
seen map[[FrameNonceSize]byte]struct{}
|
||||
slots []frameReplaySlot
|
||||
}
|
||||
|
||||
type frameReplaySlot struct {
|
||||
sequence uint64
|
||||
active bool
|
||||
nonces [][FrameNonceSize]byte
|
||||
}
|
||||
|
||||
// NewFrameReplayGuard 创建固定序号窗口和 nonce 容量的重放保护器。
|
||||
func NewFrameReplayGuard(maxEntries int) *FrameReplayGuard {
|
||||
if maxEntries <= 0 {
|
||||
maxEntries = 4096
|
||||
}
|
||||
return &FrameReplayGuard{
|
||||
max: maxEntries,
|
||||
seen: make(map[[FrameNonceSize]byte]struct{}, maxEntries),
|
||||
slots: make([]frameReplaySlot, maxEntries),
|
||||
}
|
||||
}
|
||||
|
||||
// Seen 记录 nonce;重复 nonce 或已滑出窗口的旧序号会返回重放错误。
|
||||
func (g *FrameReplayGuard) Seen(nonce []byte) error {
|
||||
if g == nil {
|
||||
return ErrFrameEncryptedPayload
|
||||
}
|
||||
if len(nonce) != FrameNonceSize {
|
||||
return ErrFrameEncryptedPayload
|
||||
}
|
||||
var key [FrameNonceSize]byte
|
||||
copy(key[:], nonce)
|
||||
sequence := binary.BigEndian.Uint64(key[:8])
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.ensureReady()
|
||||
window := replayWindow(g.max)
|
||||
|
||||
if g.initialized && sequence <= g.highest && g.highest-sequence >= window {
|
||||
return ErrFrameNonceReplay
|
||||
}
|
||||
if !g.initialized {
|
||||
g.initialized = true
|
||||
g.highest = sequence
|
||||
} else if sequence > g.highest {
|
||||
g.advance(sequence)
|
||||
}
|
||||
if _, ok := g.seen[key]; ok {
|
||||
return ErrFrameNonceReplay
|
||||
}
|
||||
if g.total >= g.max {
|
||||
return ErrFrameReplayGuardFull
|
||||
}
|
||||
|
||||
slot := &g.slots[sequence%window]
|
||||
if slot.active && slot.sequence != sequence {
|
||||
g.clearSlot(slot)
|
||||
}
|
||||
if !slot.active {
|
||||
slot.sequence = sequence
|
||||
slot.active = true
|
||||
}
|
||||
slot.nonces = append(slot.nonces, key)
|
||||
g.seen[key] = struct{}{}
|
||||
g.total++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *FrameReplayGuard) ensureReady() {
|
||||
if g.max <= 0 {
|
||||
g.max = 4096
|
||||
}
|
||||
if g.seen == nil {
|
||||
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
|
||||
}
|
||||
if len(g.slots) != g.max {
|
||||
g.slots = make([]frameReplaySlot, g.max)
|
||||
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
|
||||
g.initialized = false
|
||||
g.highest = 0
|
||||
g.total = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (g *FrameReplayGuard) advance(sequence uint64) {
|
||||
delta := sequence - g.highest
|
||||
window := replayWindow(g.max)
|
||||
if delta >= window {
|
||||
g.slots = make([]frameReplaySlot, g.max)
|
||||
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
|
||||
g.total = 0
|
||||
g.highest = sequence
|
||||
return
|
||||
}
|
||||
start := g.highest % window
|
||||
for step := uint64(1); step <= delta; step++ {
|
||||
g.clearSlot(&g.slots[(start+step)%window])
|
||||
}
|
||||
g.highest = sequence
|
||||
}
|
||||
|
||||
func replayWindow(maxEntries int) uint64 {
|
||||
if maxEntries <= 0 {
|
||||
return 4096
|
||||
}
|
||||
// int 的正值范围始终可由 uint64 表示;这里集中转换,避免窗口运算散落窄化风险。
|
||||
return uint64(maxEntries) // #nosec G115 -- maxEntries 已验证为正数,转换不会溢出。
|
||||
}
|
||||
|
||||
func (g *FrameReplayGuard) clearSlot(slot *frameReplaySlot) {
|
||||
if slot == nil || !slot.active {
|
||||
return
|
||||
}
|
||||
for _, nonce := range slot.nonces {
|
||||
delete(g.seen, nonce)
|
||||
g.total--
|
||||
}
|
||||
if g.total < 0 {
|
||||
g.total = 0
|
||||
}
|
||||
*slot = frameReplaySlot{}
|
||||
}
|
||||
|
||||
func frameAEAD(key []byte) (cipher.AEAD, error) {
|
||||
if len(key) != FrameSessionKeySize {
|
||||
return nil, ErrFrameEncryptionKeyInvalid
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewGCM(block)
|
||||
}
|
||||
|
||||
func frameAAD(frame Frame) ([]byte, error) {
|
||||
var aad [21]byte
|
||||
aad[0] = frame.Version
|
||||
binary.BigEndian.PutUint16(aad[1:3], frame.Flags&^FrameFlagEncrypted)
|
||||
binary.BigEndian.PutUint16(aad[3:5], frame.Codec)
|
||||
binary.BigEndian.PutUint32(aad[5:9], packetIDToWire(frame.PacketID))
|
||||
binary.BigEndian.PutUint32(aad[9:13], frame.MsgID)
|
||||
binary.BigEndian.PutUint64(aad[13:21], frame.Sequence)
|
||||
metadata, err := encodeFrameMeta(frame.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(metadata) == 0 {
|
||||
return aad[:], nil
|
||||
}
|
||||
out := make([]byte, 0, len(aad)+len(metadata))
|
||||
out = append(out, aad[:]...)
|
||||
out = append(out, metadata...)
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
// 示例游戏协议用于在通用蓝图中保留示例消息 ID 和响应流程,
|
||||
// 避免继承旧项目生成的 protobuf 类型。真实游戏应替换 configs/protocol/game.json 并重新生成。
|
||||
|
||||
//go:generate go run ../../cmd/protogen -schema ../../configs/protocol/game.json -out game_schema_gen.go
|
||||
|
||||
// 示例 packet id 保留最小登录、请求响应、心跳和聊天流程。
|
||||
const (
|
||||
PacketIDContent = int32(0)
|
||||
PacketIDReqResp = int32(1)
|
||||
PacketIDPingPong = int32(2)
|
||||
PacketIDChat = int32(5)
|
||||
)
|
||||
|
||||
// OnConnectionRequest 是示例 on_connection 请求的轻量解码结果。
|
||||
type OnConnectionRequest struct {
|
||||
RawBody []byte
|
||||
PassthroughID string
|
||||
}
|
||||
|
||||
// OnConnectionResponse 是示例 on_connection 响应。
|
||||
type OnConnectionResponse struct {
|
||||
PassthroughID string
|
||||
HasRole bool
|
||||
ShardID int32
|
||||
ServerTime int64
|
||||
CurBundleVer string
|
||||
}
|
||||
|
||||
// DecodeOnConnectionRequest 从示例请求体提取 passthrough id。
|
||||
func DecodeOnConnectionRequest(body []byte) OnConnectionRequest {
|
||||
passthroughID := extractPassID(body)
|
||||
return OnConnectionRequest{
|
||||
RawBody: append([]byte(nil), body...),
|
||||
PassthroughID: passthroughID,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeOnConnectionResponse 编码示例 on_connection 响应。
|
||||
func EncodeOnConnectionResponse(resp OnConnectionResponse) []byte {
|
||||
respBody := make([]byte, 0, 64)
|
||||
respBody = appendStringField(respBody, 1, resp.PassthroughID)
|
||||
respBody = appendVarintField(respBody, 3, 0)
|
||||
respBody = appendVarintField(respBody, 7, 0)
|
||||
respBody = appendInt64Varint(respBody, 12, resp.ServerTime)
|
||||
|
||||
body := make([]byte, 0, 96)
|
||||
body = appendBytesField(body, 1, respBody)
|
||||
body = appendBoolField(body, 2, resp.HasRole)
|
||||
body = appendI32VarintField(body, 3, resp.ShardID)
|
||||
body = appendStringField(body, 4, resp.CurBundleVer)
|
||||
return body
|
||||
}
|
||||
|
||||
func appendTag(dst []byte, fieldNumber int, typ protowire.Type) []byte {
|
||||
if fieldNumber <= 0 {
|
||||
return dst
|
||||
}
|
||||
return protowire.AppendTag(dst, protowire.Number(fieldNumber), typ) //nolint:gosec // G115:协议字段号由 schema/generator 固定为正数。
|
||||
}
|
||||
|
||||
func appendVarintField(dst []byte, fieldNumber int, value uint64) []byte {
|
||||
dst = appendTag(dst, fieldNumber, protowire.VarintType)
|
||||
return protowire.AppendVarint(dst, value)
|
||||
}
|
||||
|
||||
func appendIntVarintField(dst []byte, fieldNumber int, value int) []byte {
|
||||
return appendInt64Varint(dst, fieldNumber, int64(value))
|
||||
}
|
||||
|
||||
func appendI32VarintField(dst []byte, fieldNumber int, value int32) []byte {
|
||||
return appendInt64Varint(dst, fieldNumber, int64(value))
|
||||
}
|
||||
|
||||
func appendInt64Varint(dst []byte, fieldNumber int, value int64) []byte {
|
||||
// protobuf varint 对 signed int 兼容旧协议的补码写法,不能改成 zigzag。
|
||||
return appendVarintField(dst, fieldNumber, uint64(value)) //nolint:gosec // G115:协议线格式要求保留 int64 的补码位模式。
|
||||
}
|
||||
|
||||
func intFromVarint(value uint64) int {
|
||||
signed := int64FromVarint(value)
|
||||
if signed > int64(math.MaxInt) {
|
||||
return math.MaxInt
|
||||
}
|
||||
if signed < int64(math.MinInt) {
|
||||
return math.MinInt
|
||||
}
|
||||
return int(signed) //nolint:gosec // G115:前面已按当前平台 int 范围钳制。
|
||||
}
|
||||
|
||||
func int32FromVarint(value uint64) int32 {
|
||||
signed := int64FromVarint(value)
|
||||
if signed > math.MaxInt32 {
|
||||
return math.MaxInt32
|
||||
}
|
||||
if signed < math.MinInt32 {
|
||||
return math.MinInt32
|
||||
}
|
||||
return int32(signed) //nolint:gosec // G115:前面已按 int32 范围钳制。
|
||||
}
|
||||
|
||||
func int64FromVarint(value uint64) int64 {
|
||||
return int64(value) //nolint:gosec // G115:协议线格式要求按补码恢复 int64。
|
||||
}
|
||||
|
||||
func appendBoolField(dst []byte, fieldNumber int, value bool) []byte {
|
||||
if value {
|
||||
return appendVarintField(dst, fieldNumber, 1)
|
||||
}
|
||||
return appendVarintField(dst, fieldNumber, 0)
|
||||
}
|
||||
|
||||
func appendStringField(dst []byte, fieldNumber int, value string) []byte {
|
||||
dst = appendTag(dst, fieldNumber, protowire.BytesType)
|
||||
return protowire.AppendString(dst, value)
|
||||
}
|
||||
|
||||
func appendBytesField(dst []byte, fieldNumber int, value []byte) []byte {
|
||||
dst = appendTag(dst, fieldNumber, protowire.BytesType)
|
||||
return protowire.AppendBytes(dst, value)
|
||||
}
|
||||
|
||||
func extractPassID(body []byte) string {
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return ""
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
reqBytes, n := protowire.ConsumeBytes(body)
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
return extractPassthroughID(reqBytes)
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractPassthroughID(body []byte) string {
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return ""
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
value, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
package protocol
|
||||
|
||||
// Code generated by protogen; DO NOT EDIT.
|
||||
|
||||
import "google.golang.org/protobuf/encoding/protowire"
|
||||
|
||||
const (
|
||||
MessageOnConnection = uint32(1)
|
||||
MessageEnterWorld = uint32(2)
|
||||
MessageAddCurrency = uint32(10001)
|
||||
MessageReadMail = uint32(10002)
|
||||
MessagePushCreateProfile = uint32(30141)
|
||||
MessageNoticePush = uint32(30142)
|
||||
)
|
||||
|
||||
type MessageDescriptor struct {
|
||||
MsgID uint32 `json:"msg_id"`
|
||||
Name string `json:"name"`
|
||||
Constant string `json:"constant"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
var genMsgDescs = []MessageDescriptor{
|
||||
{MsgID: MessageOnConnection, Name: "OnConnection", Constant: "MessageOnConnection", Description: ""},
|
||||
{MsgID: MessageEnterWorld, Name: "EnterWorld", Constant: "MessageEnterWorld", Description: ""},
|
||||
{MsgID: MessageAddCurrency, Name: "AddCurrency", Constant: "MessageAddCurrency", Description: ""},
|
||||
{MsgID: MessageReadMail, Name: "ReadMail", Constant: "MessageReadMail", Description: ""},
|
||||
{MsgID: MessagePushCreateProfile, Name: "PushCreateProfile", Constant: "MessagePushCreateProfile", Description: ""},
|
||||
{MsgID: MessageNoticePush, Name: "NoticePush", Constant: "MessageNoticePush", Description: ""},
|
||||
}
|
||||
|
||||
func MessageDescriptors() []MessageDescriptor {
|
||||
return append([]MessageDescriptor(nil), genMsgDescs...)
|
||||
}
|
||||
|
||||
func MessageName(msgID uint32) (string, bool) {
|
||||
switch msgID {
|
||||
case MessageOnConnection:
|
||||
return "OnConnection", true
|
||||
case MessageEnterWorld:
|
||||
return "EnterWorld", true
|
||||
case MessageAddCurrency:
|
||||
return "AddCurrency", true
|
||||
case MessageReadMail:
|
||||
return "ReadMail", true
|
||||
case MessagePushCreateProfile:
|
||||
return "PushCreateProfile", true
|
||||
case MessageNoticePush:
|
||||
return "NoticePush", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
type CreateProfilePush struct {
|
||||
}
|
||||
|
||||
type NoticePush struct {
|
||||
NoticeID string
|
||||
DeliveryID string
|
||||
Title string
|
||||
Body string
|
||||
AttachmentRef string
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
type EnterWorldResponse struct {
|
||||
AccountID string
|
||||
RoleID string
|
||||
Name string
|
||||
Level int
|
||||
ServerTime int64
|
||||
Gold int64
|
||||
Diamond int64
|
||||
InventoryCount int
|
||||
QuestCount int
|
||||
UnreadMail int
|
||||
}
|
||||
|
||||
type AddCurrencyRequest struct {
|
||||
Currency string
|
||||
Delta int64
|
||||
}
|
||||
|
||||
type AddCurrencyResponse struct {
|
||||
AccountID string
|
||||
Currency string
|
||||
Balance int64
|
||||
ServerTime int64
|
||||
}
|
||||
|
||||
type ReadMailRequest struct {
|
||||
MailID string
|
||||
}
|
||||
|
||||
type ReadMailResponse struct {
|
||||
AccountID string
|
||||
MailID string
|
||||
Changed bool
|
||||
UnreadMail int
|
||||
ServerTime int64
|
||||
}
|
||||
|
||||
func EncodeCreateProfilePush(value CreateProfilePush) []byte {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
func EncodeNoticePush(value NoticePush) []byte {
|
||||
body := make([]byte, 0, 88)
|
||||
body = appendStringField(body, 1, value.NoticeID)
|
||||
body = appendStringField(body, 2, value.DeliveryID)
|
||||
body = appendStringField(body, 3, value.Title)
|
||||
body = appendStringField(body, 4, value.Body)
|
||||
body = appendStringField(body, 5, value.AttachmentRef)
|
||||
body = appendInt64Varint(body, 6, value.CreatedAt)
|
||||
return body
|
||||
}
|
||||
|
||||
func EncodeEnterWorldResponse(value EnterWorldResponse) []byte {
|
||||
body := make([]byte, 0, 104)
|
||||
body = appendStringField(body, 1, value.AccountID)
|
||||
body = appendStringField(body, 2, value.RoleID)
|
||||
body = appendStringField(body, 3, value.Name)
|
||||
body = appendIntVarintField(body, 4, value.Level)
|
||||
body = appendInt64Varint(body, 5, value.ServerTime)
|
||||
body = appendInt64Varint(body, 6, value.Gold)
|
||||
body = appendInt64Varint(body, 7, value.Diamond)
|
||||
body = appendIntVarintField(body, 8, value.InventoryCount)
|
||||
body = appendIntVarintField(body, 9, value.QuestCount)
|
||||
body = appendIntVarintField(body, 10, value.UnreadMail)
|
||||
return body
|
||||
}
|
||||
|
||||
func EncodeAddCurrencyRequest(value AddCurrencyRequest) []byte {
|
||||
body := make([]byte, 0, 24)
|
||||
body = appendStringField(body, 1, value.Currency)
|
||||
body = appendInt64Varint(body, 2, value.Delta)
|
||||
return body
|
||||
}
|
||||
|
||||
func EncodeAddCurrencyResponse(value AddCurrencyResponse) []byte {
|
||||
body := make([]byte, 0, 48)
|
||||
body = appendStringField(body, 1, value.AccountID)
|
||||
body = appendStringField(body, 2, value.Currency)
|
||||
body = appendInt64Varint(body, 3, value.Balance)
|
||||
body = appendInt64Varint(body, 4, value.ServerTime)
|
||||
return body
|
||||
}
|
||||
|
||||
func EncodeReadMailRequest(value ReadMailRequest) []byte {
|
||||
body := make([]byte, 0, 16)
|
||||
body = appendStringField(body, 1, value.MailID)
|
||||
return body
|
||||
}
|
||||
|
||||
func EncodeReadMailResponse(value ReadMailResponse) []byte {
|
||||
body := make([]byte, 0, 56)
|
||||
body = appendStringField(body, 1, value.AccountID)
|
||||
body = appendStringField(body, 2, value.MailID)
|
||||
body = appendBoolField(body, 3, value.Changed)
|
||||
body = appendIntVarintField(body, 4, value.UnreadMail)
|
||||
body = appendInt64Varint(body, 5, value.ServerTime)
|
||||
return body
|
||||
}
|
||||
|
||||
func DecodeNoticePush(body []byte) NoticePush {
|
||||
var value NoticePush
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.NoticeID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 2 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.DeliveryID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 3 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Title = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 4 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Body = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 5 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.AttachmentRef = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 6 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.CreatedAt = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func DecodeEnterWorldResponse(body []byte) EnterWorldResponse {
|
||||
var value EnterWorldResponse
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.AccountID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 2 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.RoleID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 3 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Name = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 4 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Level = intFromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 5 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.ServerTime = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 6 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Gold = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 7 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Diamond = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 8 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.InventoryCount = intFromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 9 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.QuestCount = intFromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 10 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.UnreadMail = intFromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func DecodeAddCurrencyRequest(body []byte) AddCurrencyRequest {
|
||||
var value AddCurrencyRequest
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Currency = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 2 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Delta = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func DecodeAddCurrencyResponse(body []byte) AddCurrencyResponse {
|
||||
var value AddCurrencyResponse
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.AccountID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 2 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Currency = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 3 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Balance = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 4 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.ServerTime = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func DecodeReadMailRequest(body []byte) ReadMailRequest {
|
||||
var value ReadMailRequest
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.MailID = fieldValue
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func DecodeReadMailResponse(body []byte) ReadMailResponse {
|
||||
var value ReadMailResponse
|
||||
for len(body) > 0 {
|
||||
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
|
||||
if consumed < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[consumed:]
|
||||
|
||||
switch {
|
||||
case fieldNumber == 1 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.AccountID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 2 && typ == protowire.BytesType:
|
||||
fieldValue, n := protowire.ConsumeString(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.MailID = fieldValue
|
||||
body = body[n:]
|
||||
case fieldNumber == 3 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.Changed = fieldValue != 0
|
||||
body = body[n:]
|
||||
case fieldNumber == 4 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.UnreadMail = intFromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
case fieldNumber == 5 && typ == protowire.VarintType:
|
||||
fieldValue, n := protowire.ConsumeVarint(body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
value.ServerTime = int64FromVarint(fieldValue)
|
||||
body = body[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
|
||||
if n < 0 {
|
||||
return value
|
||||
}
|
||||
body = body[n:]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// MaxPacketBodySize 是旧 packet 协议默认 body 上限。
|
||||
const MaxPacketBodySize uint32 = 1024 * 1024
|
||||
|
||||
// 旧 packet 协议解析错误。
|
||||
var (
|
||||
ErrPacketBodyTooLarge = errors.New("protocol packet body is too large")
|
||||
ErrInvalidPacketFlag = errors.New("protocol packet flag is invalid")
|
||||
)
|
||||
|
||||
// Packet 是旧 gateway packet 线协议的内存表示。
|
||||
type Packet struct {
|
||||
PacketID int32
|
||||
MsgID uint32
|
||||
Compressed bool
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// ReadPacket 从 reader 读取旧 packet。
|
||||
func ReadPacket(r io.Reader, maxBodySize uint32) (Packet, error) {
|
||||
maxBodySize = normMaxPacketBody(maxBodySize)
|
||||
var header [13]byte
|
||||
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||
return Packet{}, err
|
||||
}
|
||||
|
||||
bodySize := binary.LittleEndian.Uint32(header[0:4])
|
||||
if bodySize > maxBodySize {
|
||||
return Packet{}, fmt.Errorf("%w: %d", ErrPacketBodyTooLarge, bodySize)
|
||||
}
|
||||
|
||||
body := make([]byte, bodySize)
|
||||
if bodySize > 0 {
|
||||
if _, err := io.ReadFull(r, body); err != nil {
|
||||
return Packet{}, err
|
||||
}
|
||||
}
|
||||
|
||||
compressed, err := decodeCompressedFlag(header[12])
|
||||
if err != nil {
|
||||
return Packet{}, err
|
||||
}
|
||||
return Packet{
|
||||
PacketID: packetIDFromWire(binary.LittleEndian.Uint32(header[4:8])),
|
||||
MsgID: binary.LittleEndian.Uint32(header[8:12]),
|
||||
Compressed: compressed,
|
||||
Body: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WritePacket 把旧 packet 写入 writer。
|
||||
func WritePacket(w io.Writer, pkt Packet) error {
|
||||
bodySize := len(pkt.Body)
|
||||
if bodySize > int(MaxPacketBodySize) || uint64(bodySize) > uint64(^uint32(0)) {
|
||||
return fmt.Errorf("%w: %d", ErrPacketBodyTooLarge, bodySize)
|
||||
}
|
||||
var header [13]byte
|
||||
binary.LittleEndian.PutUint32(header[0:4], uint32(bodySize))
|
||||
binary.LittleEndian.PutUint32(header[4:8], packetIDToWire(pkt.PacketID))
|
||||
binary.LittleEndian.PutUint32(header[8:12], pkt.MsgID)
|
||||
if pkt.Compressed {
|
||||
header[12] = 1
|
||||
}
|
||||
|
||||
if bodySize == 0 {
|
||||
return writeFull(w, header[:])
|
||||
}
|
||||
return writeFullBuffers(w, header[:], pkt.Body)
|
||||
}
|
||||
|
||||
func normMaxPacketBody(maxBodySize uint32) uint32 {
|
||||
if maxBodySize == 0 {
|
||||
return MaxPacketBodySize
|
||||
}
|
||||
return maxBodySize
|
||||
}
|
||||
|
||||
func decodeCompressedFlag(flag byte) (bool, error) {
|
||||
switch flag {
|
||||
case 0:
|
||||
return false, nil
|
||||
case 1:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("%w: compressed=%d", ErrInvalidPacketFlag, flag)
|
||||
}
|
||||
}
|
||||
|
||||
func writeFull(w io.Writer, data []byte) error {
|
||||
for len(data) > 0 {
|
||||
n, err := w.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n <= 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFullBuffers(w io.Writer, parts ...[]byte) error {
|
||||
total := 0
|
||||
var scratch [4][]byte
|
||||
buffers := net.Buffers(scratch[:0])
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
total += len(part)
|
||||
buffers = append(buffers, part)
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, ok := w.(net.Conn); !ok {
|
||||
return writeFullParts(w, parts...)
|
||||
}
|
||||
written, err := buffers.WriteTo(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written == int64(total) {
|
||||
return nil
|
||||
}
|
||||
if written < 0 || written > int64(total) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return writeBufRemainder(w, int(written), parts...)
|
||||
}
|
||||
|
||||
func writeFullParts(w io.Writer, parts ...[]byte) error {
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := writeFull(w, part); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeBufRemainder(w io.Writer, written int, parts ...[]byte) error {
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
if written >= len(part) {
|
||||
written -= len(part)
|
||||
continue
|
||||
}
|
||||
if written > 0 {
|
||||
part = part[written:]
|
||||
written = 0
|
||||
}
|
||||
if err := writeFull(w, part); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package protocol
|
||||
|
||||
import "fmt"
|
||||
|
||||
func formatUnix(t interface{ Unix() int64 }) string {
|
||||
return fmt.Sprintf("%d", t.Unix())
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 支持的 gateway wire format 名称。
|
||||
const (
|
||||
WireFormatPacket = "packet"
|
||||
WireFormatFrameV1 = "frame_v1"
|
||||
)
|
||||
|
||||
// ErrInvalidWireFormat 表示 envelope 使用了不支持的线协议格式。
|
||||
var ErrInvalidWireFormat = errors.New("protocol wire format is invalid")
|
||||
|
||||
// Envelope 是 packet 与 frame_v1 的统一传输容器。
|
||||
type Envelope struct {
|
||||
Format string
|
||||
Packet Packet
|
||||
Frame Frame
|
||||
}
|
||||
|
||||
// ReadEnvelope 自动探测并读取 packet 或 frame_v1。
|
||||
func ReadEnvelope(r *bufio.Reader, maxBodySize uint32) (Envelope, error) {
|
||||
if r == nil {
|
||||
return Envelope{}, io.ErrUnexpectedEOF
|
||||
}
|
||||
peek, err := r.Peek(4)
|
||||
if err != nil {
|
||||
return Envelope{}, err
|
||||
}
|
||||
// 读侧兼容两种线协议:先探测 frame_v1 magic,未命中则退回旧 packet 格式,方便客户端灰度升级。
|
||||
if binary.LittleEndian.Uint16(peek[0:2]) == FrameMagic &&
|
||||
peek[2] == FrameVersion1 &&
|
||||
int(peek[3]) >= FrameHeaderSizeV1 {
|
||||
frame, err := ReadFrame(r, maxBodySize)
|
||||
if err != nil {
|
||||
return Envelope{}, err
|
||||
}
|
||||
return Envelope{
|
||||
Format: WireFormatFrameV1,
|
||||
Packet: packetFromFrame(frame),
|
||||
Frame: frame,
|
||||
}, nil
|
||||
}
|
||||
packet, err := ReadPacket(r, maxBodySize)
|
||||
if err != nil {
|
||||
return Envelope{}, err
|
||||
}
|
||||
return Envelope{
|
||||
Format: WireFormatPacket,
|
||||
Packet: packet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WriteEnvelope 按指定 wire format 写出 envelope。
|
||||
func WriteEnvelope(w io.Writer, envelope Envelope) error {
|
||||
format := strings.TrimSpace(envelope.Format)
|
||||
if format == "" {
|
||||
format = WireFormatPacket
|
||||
} else {
|
||||
format = NormalizeWireFormat(format)
|
||||
}
|
||||
switch format {
|
||||
case WireFormatPacket:
|
||||
return WritePacket(w, envelope.Packet)
|
||||
case WireFormatFrameV1:
|
||||
// frame_v1 的压缩标记以 Packet.Compressed 为准,避免调用方同时维护 Packet 和 Frame 两份状态时产生漂移。
|
||||
flags := envelope.Frame.Flags &^ FrameFlagCompressed
|
||||
if envelope.Packet.Compressed {
|
||||
flags |= FrameFlagCompressed
|
||||
}
|
||||
frame := Frame{
|
||||
Version: envelope.Frame.Version,
|
||||
Flags: flags,
|
||||
Codec: envelope.Frame.Codec,
|
||||
PacketID: envelope.Packet.PacketID,
|
||||
MsgID: envelope.Packet.MsgID,
|
||||
Sequence: envelope.Frame.Sequence,
|
||||
Body: envelope.Packet.Body,
|
||||
Metadata: CloneFrameMetadata(envelope.Frame.Metadata),
|
||||
}
|
||||
return WriteFrame(w, frame)
|
||||
default:
|
||||
return ErrInvalidWireFormat
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeWireFormat 归一化 wire format 文本。
|
||||
func NormalizeWireFormat(format string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(format)) {
|
||||
case "":
|
||||
return WireFormatPacket
|
||||
case WireFormatPacket:
|
||||
return WireFormatPacket
|
||||
case WireFormatFrameV1:
|
||||
return WireFormatFrameV1
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownSerializer = errors.New("unknown serializer")
|
||||
ErrInvalidTarget = errors.New("invalid serializer target")
|
||||
)
|
||||
|
||||
type Serializer interface {
|
||||
Name() string
|
||||
Marshal(any) ([]byte, error)
|
||||
Unmarshal([]byte, any) error
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
serializers map[string]Serializer
|
||||
}
|
||||
|
||||
func NewRegistry(serializers ...Serializer) *Registry {
|
||||
r := &Registry{serializers: make(map[string]Serializer)}
|
||||
for _, serializer := range serializers {
|
||||
_ = r.Register(serializer)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Registry) Register(serializer Serializer) error {
|
||||
if serializer == nil {
|
||||
return ErrUnknownSerializer
|
||||
}
|
||||
name := normalizeName(serializer.Name())
|
||||
if name == "" {
|
||||
return ErrUnknownSerializer
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.serializers[name] = serializer
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) (Serializer, bool) {
|
||||
if r == nil {
|
||||
return nil, false
|
||||
}
|
||||
name = normalizeName(name)
|
||||
r.mu.RLock()
|
||||
serializer, ok := r.serializers[name]
|
||||
r.mu.RUnlock()
|
||||
return serializer, ok
|
||||
}
|
||||
|
||||
func (r *Registry) MustGet(name string) (Serializer, error) {
|
||||
serializer, ok := r.Get(name)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownSerializer, name)
|
||||
}
|
||||
return serializer, nil
|
||||
}
|
||||
|
||||
func (r *Registry) Names() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
names := make([]string, 0, len(r.serializers))
|
||||
for name := range r.serializers {
|
||||
names = append(names, name)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
type JSON struct{}
|
||||
|
||||
func (JSON) Name() string { return "json" }
|
||||
|
||||
func (JSON) Marshal(value any) ([]byte, error) {
|
||||
return json.Marshal(value)
|
||||
}
|
||||
|
||||
func (JSON) Unmarshal(data []byte, target any) error {
|
||||
if target == nil {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
return json.Unmarshal(data, target)
|
||||
}
|
||||
|
||||
type Raw struct{}
|
||||
|
||||
func (Raw) Name() string { return "raw" }
|
||||
|
||||
func (Raw) Marshal(value any) ([]byte, error) {
|
||||
switch typed := value.(type) {
|
||||
case []byte:
|
||||
return append([]byte(nil), typed...), nil
|
||||
case string:
|
||||
return []byte(typed), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: raw serializer requires []byte or string", ErrInvalidTarget)
|
||||
}
|
||||
}
|
||||
|
||||
func (Raw) Unmarshal(data []byte, target any) error {
|
||||
switch typed := target.(type) {
|
||||
case *[]byte:
|
||||
if typed == nil {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
*typed = append((*typed)[:0], data...)
|
||||
return nil
|
||||
case *string:
|
||||
if typed == nil {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
*typed = string(data)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("%w: raw serializer target must be *[]byte or *string", ErrInvalidTarget)
|
||||
}
|
||||
}
|
||||
|
||||
type Protobuf struct {
|
||||
DiscardUnknown bool
|
||||
}
|
||||
|
||||
func (Protobuf) Name() string { return "protobuf" }
|
||||
|
||||
func (Protobuf) Marshal(value any) ([]byte, error) {
|
||||
msg, ok := value.(proto.Message)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: protobuf serializer requires proto.Message", ErrInvalidTarget)
|
||||
}
|
||||
return proto.Marshal(msg)
|
||||
}
|
||||
|
||||
func (p Protobuf) Unmarshal(data []byte, target any) error {
|
||||
if target == nil {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
msg, ok := target.(proto.Message)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: protobuf serializer target must be proto.Message", ErrInvalidTarget)
|
||||
}
|
||||
// 协议层已经限制单包大小;这里再限制嵌套深度,避免恶意递归消息拖垮解码栈。
|
||||
return proto.UnmarshalOptions{
|
||||
DiscardUnknown: p.DiscardUnknown,
|
||||
RecursionLimit: 32,
|
||||
}.Unmarshal(data, msg)
|
||||
}
|
||||
|
||||
type LenientProtobuf struct{}
|
||||
|
||||
func (LenientProtobuf) Name() string { return "protobuf_lenient" }
|
||||
|
||||
func (LenientProtobuf) Marshal(value any) ([]byte, error) {
|
||||
return (Protobuf{}).Marshal(value)
|
||||
}
|
||||
|
||||
func (LenientProtobuf) Unmarshal(data []byte, target any) error {
|
||||
return (Protobuf{DiscardUnknown: true}).Unmarshal(data, target)
|
||||
}
|
||||
|
||||
var DefaultRegistry = NewRegistry(JSON{}, Raw{}, Protobuf{}, LenientProtobuf{})
|
||||
|
||||
func normalizeName(name string) string {
|
||||
return strings.ToLower(strings.TrimSpace(name))
|
||||
}
|
||||
Reference in new issue
Block a user