fix: 按 K-00 约定修复 Go SDK 断线重交与退避

This commit is contained in:
Nixevol
2026-09-30 16:24:13 +08:00
parent 3749b9bdf1
commit f6f8ccf269
20 changed files with 1544 additions and 324 deletions
+149 -48
View File
@@ -8,6 +8,11 @@ import (
// Connect 连接服务器。credential 为密码或会话令牌。
func (c *Client) Connect(ctx context.Context, rawURL, endpointID string, credential Credential, opts Options) error {
o := opts.withDefaults()
if o.MaxReceiveBytes > 0 && o.MaxReceiveBytes < 1024 {
return apiErr(CodeBadRequest, "max_receive_bytes 小于 1024")
}
c.mu.Lock()
if c.closed {
c.mu.Unlock()
@@ -17,11 +22,15 @@ func (c *Client) Connect(ctx context.Context, rawURL, endpointID string, credent
c.mu.Unlock()
return apiErr(CodeBadRequest, "已在连接中")
}
o := opts.withDefaults()
c.opts = o
if c.store == nil || c.opts.DedupCapacity != c.store.cap {
c.store = newLRU(o.DedupCapacity)
}
c.endpointID = endpointID
c.url = rawURL
c.stopReconnect = false
c.lastStopCode = ""
c.lastStopErr = nil
c.handshook = false
pass := credential.Password
if credential.SessionToken != "" {
@@ -41,10 +50,13 @@ func (c *Client) Connect(ctx context.Context, rawURL, endpointID string, credent
inner, cancel := context.WithCancel(context.Background())
c.ctx = inner
c.cancel = cancel
c.incoming = make(chan queuedFrame, eventQueueCap)
c.cbQ = make(chan func(), 256)
c.setStateLocked(StateConnecting, "")
c.mu.Unlock()
go c.downLoop(inner)
go c.cbLoop(inner)
tr.SetCredential(pass)
cfg := transportConfig{
@@ -54,19 +66,13 @@ func (c *Client) Connect(ctx context.Context, rawURL, endpointID string, credent
AllowTCP: o.AllowTCP,
Backoff: c.backoff,
OnDown: c.handleDown,
OnOffline: func() {
c.mu.Lock()
c.handshook = false
if !c.stopReconnect && !c.closed {
c.setStateLocked(StateReconnecting, "")
}
c.mu.Unlock()
},
OnAuthFailed: func(reason AuthReason) { c.failAuth(reason) },
OnKicked: func() { c.failKicked() },
MQTTReady: func(readyCtx context.Context) error { return c.doHello(readyCtx) },
OnOffline: c.onTransportOffline,
OnAuthFailed: func(reason AuthReason) { c.failAuth(reason) },
OnKicked: func() { c.failKicked() },
MQTTReady: func(readyCtx context.Context) error { return c.doHello(readyCtx) },
}
if err := tr.Start(inner, cfg); err != nil {
c.teardown(false, apiErr(CodeNotConnected, err.Error()), false)
return err
}
@@ -76,59 +82,125 @@ func (c *Client) Connect(ctx context.Context, rawURL, endpointID string, credent
ok := c.handshook
failed := c.stopReconnect
st := c.state
code := c.lastStopCode
c.mu.Unlock()
if ok {
return nil
}
if failed || st == StateAuthFailed || st == StateKicked {
return apiErr(CodeAuthFailed, string(st))
if code == "" {
code = string(st)
}
return apiErr(code, "认证失败,停止重连")
}
select {
case <-ctx.Done():
_ = c.Close()
c.teardown(false, apiErr(CodeNotConnected, "连接已取消"), true)
return ctx.Err()
case <-time.After(20 * time.Millisecond):
}
}
_ = c.Close()
c.teardown(false, apiErr(CodeNotConnected, "连接超时"), true)
return apiErr(CodeNotConnected, "连接超时")
}
func (c *Client) failAuth(reason AuthReason) {
code := string(reason)
if code == "" {
code = CodeBadCredentials
}
err := apiErr(code, "认证失败,停止重连")
c.mu.Lock()
if c.stopReconnect && c.lastStopCode != "" {
c.mu.Unlock()
return
}
c.stopReconnect = true
c.handshook = false
c.setStateLocked(StateAuthFailed, string(reason))
c.failQueuedLocked(apiErr(string(reason), "认证失败,停止重连"))
c.lastStopCode = code
c.lastStopErr = err
c.setStateLocked(StateAuthFailed, code)
c.failQueuedLocked(err)
c.failPendingLocked(err, true)
c.closeConnLostLocked()
tr := c.transport
cancel := c.cancel
c.transport = nil
c.mu.Unlock()
if cancel != nil {
cancel()
}
if tr != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_ = tr.Stop(ctx)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = tr.Stop(ctx)
}()
}
}
func (c *Client) failKicked() {
err := apiErr(CodeTakenOver, "被顶号,停止重连")
c.mu.Lock()
if c.stopReconnect && c.lastStopCode == CodeTakenOver {
c.mu.Unlock()
return
}
c.stopReconnect = true
c.handshook = false
c.setStateLocked(StateKicked, "0x8E")
c.failQueuedLocked(apiErr(CodeKicked, "被顶号,停止重连"))
c.lastStopCode = CodeTakenOver
c.lastStopErr = err
c.setStateLocked(StateKicked, CodeTakenOver)
c.failQueuedLocked(err)
c.failPendingLocked(err, true)
c.closeConnLostLocked()
tr := c.transport
cancel := c.cancel
c.transport = nil
c.mu.Unlock()
if cancel != nil {
cancel()
}
if tr != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_ = tr.Stop(ctx)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = tr.Stop(ctx)
}()
}
}
func (c *Client) handleFatal(reason string) {
if reason == "" {
reason = "fatal"
}
err := apiErr(reason, "致命错误,停止重连")
c.mu.Lock()
if c.stopReconnect && c.lastStopCode != "" {
c.mu.Unlock()
return
}
c.stopReconnect = true
c.handshook = false
c.lastStopCode = reason
c.lastStopErr = err
c.setStateLocked(StateAuthFailed, reason)
c.failQueuedLocked(err)
c.failPendingLocked(err, true)
c.closeConnLostLocked()
tr := c.transport
cancel := c.cancel
c.transport = nil
c.mu.Unlock()
if cancel != nil {
cancel()
}
if tr != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = tr.Stop(ctx)
}()
}
}
@@ -136,13 +208,11 @@ func (c *Client) setStateLocked(st ConnectionState, reason string) {
c.state = st
h := c.onConnection
ev := ConnectionEvent{State: st, Reason: reason}
go func() {
c.cbMu.Lock()
defer c.cbMu.Unlock()
c.dispatchLocked(func() {
if h != nil {
h(ev)
}
}()
})
}
func (c *Client) doHello(ctx context.Context) error {
@@ -151,6 +221,7 @@ func (c *Client) doHello(ctx context.Context) error {
sentAt := c.helloSentAt
label := c.opts.ClientLabel
maxRecv := c.opts.MaxReceiveBytes
c.connLost = make(chan struct{})
c.mu.Unlock()
req := map[string]any{
@@ -197,19 +268,30 @@ func (c *Client) doHello(ctx context.Context) error {
}
c.clockSkew = skew
c.handshook = true
if c.backoff != nil {
c.backoff.MarkOnline()
}
c.setStateLocked(StateOnline, "")
token := hd.SessionToken
if token != "" {
c.session = token
c.transport.SetCredential(token)
if c.transport != nil {
c.transport.SetCredential(token)
}
c.credKind = "token"
}
watchSet := c.watchSet
watchIDs := append([]string(nil), c.watchIDs...)
watchAll := c.watchAll
c.mu.Unlock()
if token != "" && c.onSession != nil {
c.cbMu.Lock()
c.onSession(token)
c.cbMu.Unlock()
}
if watchSet {
go func() {
_ = c.WatchPresence(context.Background(), watchIDs, watchAll)
}()
}
c.drainSendQueue()
return nil
@@ -229,40 +311,59 @@ func (c *Client) Limits() HandshakeLimits {
return c.limits
}
// Logout 作废会话并停止重连。
// Logout 作废会话并停止重连。请求失败仍返回给应用。
func (c *Client) Logout(ctx context.Context) error {
req := map[string]any{"v": 1, "type": "self.logout", "rid": c.nextRID()}
_, err := c.request(ctx, req, false)
c.mu.Lock()
c.stopReconnect = true
c.session = ""
cancel := c.cancel
c.mu.Unlock()
if cancel != nil {
cancel()
}
var err error
_, err = c.request(ctx, req, false)
stop := apiErr(CodeLoggedOut, "已退出")
c.teardown(false, stop, true)
return err
}
// Close 关闭连接并停止重连。
func (c *Client) Close() error {
err := apiErr(CodeClosed, "已关闭")
c.teardown(true, err, true)
return nil
}
func (c *Client) teardown(setClosed bool, stopErr error, waitStop bool) {
c.mu.Lock()
c.closed = true
if setClosed {
c.closed = true
}
c.stopReconnect = true
c.failQueuedLocked(apiErr(CodeClosed, "已关闭"))
if stopErr != nil {
c.lastStopErr = stopErr
c.lastStopCode = errCode(stopErr)
}
c.failQueuedLocked(c.stopErrLocked())
c.failPendingLocked(c.stopErrLocked(), true)
c.closeConnLostLocked()
c.handshook = false
c.session = ""
tr := c.transport
c.transport = nil
cancel := c.cancel
c.setStateLocked(StateOffline, "")
c.setStateLocked(StateOffline, c.lastStopCode)
c.mu.Unlock()
if cancel != nil {
cancel()
}
if tr != nil {
if tr == nil {
return
}
fn := func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return tr.Stop(ctx)
_ = tr.Stop(ctx)
}
return nil
if waitStop {
fn()
return
}
go fn()
}
func (c *Client) failQueuedLocked(err error) {