199 lines
8.5 KiB
Go
199 lines
8.5 KiB
Go
package mtprotoedge
|
||
|
||
import (
|
||
"bufio"
|
||
"context"
|
||
"encoding/hex"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/gotd/td/bin"
|
||
"github.com/gotd/td/crypto"
|
||
"github.com/gotd/td/proto"
|
||
"github.com/gotd/td/transport"
|
||
|
||
"telesrv/internal/compat/layerwire"
|
||
)
|
||
|
||
// Conn 是一个已识别 session 的客户端连接,持有向其加密发送消息所需的全部上下文。
|
||
// 由 SessionManager 管理,供请求响应与主动 push 共用。
|
||
//
|
||
// Send 并发安全:所有出站消息先进 per-Conn outbound actor,由它串行分配 msg_id/seq_no、
|
||
// 加密并写 transport,避免高并发 RPC 响应与 push 交错造成 MTProto 顺序错误。
|
||
type outboundWriter interface {
|
||
Send(context.Context, *bin.Buffer) error
|
||
}
|
||
|
||
type Conn struct {
|
||
transport transport.Conn
|
||
writer outboundWriter
|
||
cipher crypto.Cipher
|
||
msgID *proto.MessageIDGen
|
||
writeTimeout time.Duration
|
||
metrics Metrics
|
||
|
||
authKeyID [8]byte
|
||
// authKeyHex 是 authKeyID 的 hex 缓存:每条 RPC 的结构化日志都会带它,
|
||
// 建连时算一次,避免热路径反复 hex 编码分配。
|
||
authKeyHex string
|
||
sessionID int64
|
||
salt int64
|
||
key crypto.AuthKey
|
||
|
||
outbound chan outboundOp
|
||
outboundControl chan outboundOp
|
||
outboundStop chan struct{}
|
||
outboundDone chan struct{}
|
||
outboundClose sync.Once
|
||
// outboundEnqueueMu orders producer registration against terminal close. Close
|
||
// flips closing under this lock before waiting, so no WaitGroup Add can race Wait.
|
||
outboundEnqueueMu sync.Mutex
|
||
outboundEnqueueWG sync.WaitGroup
|
||
outboundClosing bool
|
||
// Queue backing is intentionally small and bounded per Conn; control has a separate queue
|
||
// and strict actor priority. Server-created connections share outboundTrackedBudget.
|
||
outboundQueueSize int
|
||
outboundControlQueueSize int
|
||
outboundTrackedBudget *outboundTrackedBudget
|
||
outboundBudgetOnce sync.Once
|
||
// Encoded MTProto service frames and control vectors use independent headroom: pong,
|
||
// new_session_created, bad_msg and msgs_ack must remain admissible when the body budget is
|
||
// full. Content-related control frames keep this budget while pending for resend.
|
||
outboundControlTrackedBudget *outboundTrackedBudget
|
||
outboundControlBudgetOnce sync.Once
|
||
outboundScratchPool *outboundScratchPool
|
||
outboundScratchOnce sync.Once
|
||
// terminal 表示该 logical Conn 已停止接受新的出站操作。写失败时由
|
||
// outbound actor 置位并只发停止信号,不能在 actor 内等待自身退出。
|
||
terminal atomic.Bool
|
||
transportClose sync.Once
|
||
|
||
rpcScheduler *inboundRPCScheduler
|
||
rpcCancel context.CancelFunc
|
||
rpcClose sync.Once
|
||
rpcMu sync.Mutex
|
||
rpcWG sync.WaitGroup
|
||
// rpcReservationWG 跟踪 Copy 前预算到 commit/abort 的短窗口,使 Close 返回时
|
||
// 全局/单连接预算都已归还或转交给明确的 queued/running task。
|
||
rpcReservationWG sync.WaitGroup
|
||
rpcTimeout time.Duration
|
||
rpcQueue []inboundRPC
|
||
rpcQueueSize int
|
||
rpcReserved int
|
||
rpcRunning int
|
||
rpcReady bool
|
||
rpcClosed bool
|
||
// inflightRPCBytes 跟踪已入队未完成的 inbound RPC body 总字节,配合 maxInflightRPCBytes
|
||
// 给 RPC 队列设字节预算(不止限条数),防对抗客户端发大请求撑内存。
|
||
inflightRPCBytes atomic.Int64
|
||
// 单连接只保留并发配额;实际 worker 来自 Server 共享池,避免每连接预留 goroutine。
|
||
rpcRootCtx context.Context
|
||
rpcMaxInflight int
|
||
|
||
// sentContentMessages 只由 outbound actor 访问,用于生成 MTProto seq_no。
|
||
sentContentMessages int32
|
||
// outboundRand 只由 outbound actor 访问:对 cipher 随机源的缓冲预读,
|
||
// 把每帧 padding 的 getrandom syscall 摊薄成 ~1KiB 一次。
|
||
outboundRand *bufio.Reader
|
||
|
||
identityMu sync.RWMutex
|
||
businessAuthKeyID [8]byte
|
||
businessAuthKeyHex string
|
||
businessAuthKeyResolved bool
|
||
userID atomic.Int64
|
||
userIDResolved atomic.Bool
|
||
receivesUpdates atomic.Bool
|
||
// membershipsSynced 表示该连接的 channel membership 推送路由(byMemberChannel)
|
||
// 已成功建立。它与 receivesUpdates 共同构成「session 完全就绪」:membership
|
||
// 同步失败时保持 false,让置位短路放行、下一条 RPC 重试同步,避免
|
||
// 「已置位但 channel 路由缺失」的 session 静默漏收超级群推送。
|
||
membershipsSynced atomic.Bool
|
||
// membershipGen 是本连接 channel membership 索引的修订号:任何增量修订
|
||
// (join/leave/kick 的 Add/Remove、身份切换/下线的整体清除)都递增。全量同步方
|
||
// 在读取持久成员列表前采样、落地时带回比对,检测「读取窗口内发生增量修订」的
|
||
// 丢失更新竞态(SetSessionChannelMemberships 改走合并路径并保持未就绪重试)。
|
||
membershipGen atomic.Int64
|
||
// createdAt 是连接建立时刻,供同 auth_key session 数触顶时驱逐真正最旧的连接。
|
||
createdAt time.Time
|
||
// keyDestroyed 标记本连接的 auth_key 已被 destroy_auth_key 删除。serveConn 对已建立
|
||
// 连接复用缓存密钥跳过每帧 AuthKeyStore 回查;置位后强制回落到 Get→AuthKeyNotFound,
|
||
// 维持「destroy_auth_key 发起连接下一帧自然失效」契约。只由 destroy_auth_key 处理器置位。
|
||
keyDestroyed atomic.Bool
|
||
// lastSessionSaveUnix 是上次把本连接 session 持久化到 SessionStore 的 unix 秒,用于把
|
||
// 每帧 Save 去抖到固定间隔——session 持久化是软状态(生产无热读路径,仅观测/未来用)。
|
||
// 只由单连接的读循环 goroutine 访问。
|
||
lastSessionSaveUnix atomic.Int64
|
||
// clientLayer 是本连接协商的 TL layer(invokeWithLayer/initConnection),由 handleRPC
|
||
// 在每次 Dispatch 后从 RPC 注册表刷新。出站(rpc_result/push)按此把 227 对象降级给老客户端;
|
||
// 0 表示尚未协商,按 canonical(227) 处理=不降级。
|
||
clientLayer atomic.Int32
|
||
}
|
||
|
||
// ClientLayer 返回连接协商的 TL layer;未协商时返回 canonical layer(227,不降级)。
|
||
func (c *Conn) ClientLayer() int {
|
||
if l := c.clientLayer.Load(); l != 0 {
|
||
return int(l)
|
||
}
|
||
return layerwire.CanonicalLayer
|
||
}
|
||
|
||
// SetClientLayer 记录连接协商的 TL layer。
|
||
func (c *Conn) SetClientLayer(layer int) { c.clientLayer.Store(int32(layer)) }
|
||
|
||
// AuthKeyID 返回连接的 auth_key_id。
|
||
func (c *Conn) AuthKeyID() [8]byte { return c.authKeyID }
|
||
|
||
// BusinessAuthKeyID 返回业务视角的 auth_key_id。
|
||
//
|
||
// temp auth_key 绑定后解析为 perm auth_key;第二个返回值表示本连接是否已完成解析,
|
||
// 即便解析结果等于原始 auth_key_id 也会返回 true,以避免每个 RPC 重复查绑定表。
|
||
func (c *Conn) BusinessAuthKeyID() ([8]byte, bool) {
|
||
c.identityMu.RLock()
|
||
defer c.identityMu.RUnlock()
|
||
return c.businessAuthKeyID, c.businessAuthKeyResolved
|
||
}
|
||
|
||
// BusinessAuthKeyHex 返回业务视角 auth_key_id 的 hex 缓存(每 RPC 日志用,免重复编码)。
|
||
func (c *Conn) BusinessAuthKeyHex() (string, bool) {
|
||
c.identityMu.RLock()
|
||
defer c.identityMu.RUnlock()
|
||
return c.businessAuthKeyHex, c.businessAuthKeyResolved
|
||
}
|
||
|
||
// SetBusinessAuthKeyID 缓存业务视角 auth_key_id。
|
||
func (c *Conn) SetBusinessAuthKeyID(id [8]byte) {
|
||
c.identityMu.Lock()
|
||
changed := !c.businessAuthKeyResolved || c.businessAuthKeyID != id
|
||
if changed || c.businessAuthKeyHex == "" {
|
||
c.businessAuthKeyHex = hex.EncodeToString(id[:])
|
||
}
|
||
c.businessAuthKeyID = id
|
||
c.businessAuthKeyResolved = true
|
||
c.identityMu.Unlock()
|
||
if changed {
|
||
c.userID.Store(0)
|
||
c.userIDResolved.Store(false)
|
||
}
|
||
}
|
||
|
||
// SessionID 返回连接的 session_id。
|
||
func (c *Conn) SessionID() int64 { return c.sessionID }
|
||
|
||
// UserID 返回绑定的用户 id;未登录为 0。
|
||
func (c *Conn) UserID() int64 { return c.userID.Load() }
|
||
|
||
// UserIDResolved 返回 user_id 授权状态是否已为当前连接解析过。
|
||
//
|
||
// resolved=true 且 userID=0 表示该 auth_key 当前未登录;这样登录前的多次 RPC
|
||
// 不会反复查询授权表,后续登录成功会由 BindUser 覆盖为真实用户。
|
||
func (c *Conn) UserIDResolved() (userID int64, resolved bool) {
|
||
return c.userID.Load(), c.userIDResolved.Load()
|
||
}
|
||
|
||
// ReceivesUpdates 报告该连接是否接收主动推送的 updates。
|
||
func (c *Conn) ReceivesUpdates() bool { return c.receivesUpdates.Load() }
|
||
|
||
// SetReceivesUpdates 设置该连接是否接收主动推送的 updates。
|
||
// 登录后的主连接在 updates.getState/getDifference 建立同步基线后置为 true。
|
||
func (c *Conn) SetReceivesUpdates(v bool) { c.receivesUpdates.Store(v) }
|