fix: 按 K-00 约定修复 Go SDK 断线重交与退避
This commit is contained in:
+149
-48
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user