fix: 完成 broker 复审 B-03 至 B-12
每连接异步下发与背压、写出后断开、校验当前连接与订阅、生命周期串行、登录条件更新、闲置按在线计、认证超时并发与 Shutdown 0x8B。
This commit is contained in:
+134
-43
@@ -37,7 +37,20 @@ var ErrNoConnection = errors.New("broker: no active connection")
|
||||
// ErrLargeFrameTimeout 全局大帧名额在有界等待内拿不到。
|
||||
var ErrLargeFrameTimeout = errors.New("broker: large frame quota timeout")
|
||||
|
||||
const largeAcquireWait = 5 * time.Second
|
||||
// ErrBackpressure 该连接下行队列已满(帧数或字节数)。
|
||||
var ErrBackpressure = errors.New("broker: downlink backpressure")
|
||||
|
||||
// ErrNotSubscribed 当前连接尚未订阅下行主题。
|
||||
var ErrNotSubscribed = errors.New("broker: down topic not subscribed")
|
||||
|
||||
// ErrSessionWriteConflict 密码登录写令牌时发现库已被并发更新。
|
||||
var ErrSessionWriteConflict = errors.New("broker: session token write conflict")
|
||||
|
||||
const (
|
||||
largeAcquireWait = 5 * time.Second
|
||||
downQueueMax = 256
|
||||
downQueueBytes = 16 << 20
|
||||
)
|
||||
|
||||
// AuthResult 是登录校验结论(N3 实现真实逻辑;N2 默认拒绝)。
|
||||
type AuthResult struct {
|
||||
@@ -100,6 +113,9 @@ type Broker struct {
|
||||
|
||||
largeSem chan struct{}
|
||||
closed atomic.Bool
|
||||
|
||||
lifeMu sync.Mutex
|
||||
lifeLocks map[string]*sync.Mutex
|
||||
}
|
||||
|
||||
type connState struct {
|
||||
@@ -120,6 +136,13 @@ type connState struct {
|
||||
metricsCounted bool
|
||||
established bool
|
||||
createdAt time.Time
|
||||
closing bool
|
||||
superseded bool
|
||||
downCh chan downItem
|
||||
downStop chan struct{}
|
||||
downDone chan struct{}
|
||||
downBytes atomic.Int64
|
||||
sentPub atomic.Int64
|
||||
mu sync.Mutex
|
||||
|
||||
handshakeTimer *time.Timer
|
||||
@@ -162,18 +185,19 @@ func New(opts Options) (*Broker, error) {
|
||||
})
|
||||
|
||||
b := &Broker{
|
||||
server: srv,
|
||||
auth: auth,
|
||||
uplink: uplink,
|
||||
log: log,
|
||||
onDrop: opts.OnPublishDropped,
|
||||
metrics: opts.Metrics,
|
||||
current: make(map[string]*connState),
|
||||
byClient: make(map[*mqtt.Client]*connState),
|
||||
byConnID: make(map[port.ConnID]*connState),
|
||||
closedCh: make(chan struct{}),
|
||||
queues: make(map[string]*uplinkQueue),
|
||||
largeSem: make(chan struct{}, largeFrameSlots),
|
||||
server: srv,
|
||||
auth: auth,
|
||||
uplink: uplink,
|
||||
log: log,
|
||||
onDrop: opts.OnPublishDropped,
|
||||
metrics: opts.Metrics,
|
||||
current: make(map[string]*connState),
|
||||
byClient: make(map[*mqtt.Client]*connState),
|
||||
byConnID: make(map[port.ConnID]*connState),
|
||||
closedCh: make(chan struct{}),
|
||||
queues: make(map[string]*uplinkQueue),
|
||||
largeSem: make(chan struct{}, largeFrameSlots),
|
||||
lifeLocks: make(map[string]*sync.Mutex),
|
||||
}
|
||||
b.hook = &nixHook{b: b}
|
||||
if err := srv.AddHook(b.hook, nil); err != nil {
|
||||
@@ -203,10 +227,36 @@ func (b *Broker) Close() error {
|
||||
for _, q := range b.queues {
|
||||
q.close()
|
||||
}
|
||||
b.queues = make(map[string]*uplinkQueue)
|
||||
b.queuesMu.Unlock()
|
||||
return b.server.Close()
|
||||
}
|
||||
|
||||
// Shutdown 向所有连接发 MQTT 5 0x8B 后关闭。完整 HTTP 停机顺序见 L-03。
|
||||
func (b *Broker) Shutdown(ctx context.Context) error {
|
||||
if b.closed.Load() {
|
||||
return nil
|
||||
}
|
||||
b.connsMu.RLock()
|
||||
clients := make([]*mqtt.Client, 0, len(b.byClient))
|
||||
for cl := range b.byClient {
|
||||
if cl != nil {
|
||||
clients = append(clients, cl)
|
||||
}
|
||||
}
|
||||
b.connsMu.RUnlock()
|
||||
for _, cl := range clients {
|
||||
_ = b.server.DisconnectClient(cl, packets.ErrServerShuttingDown)
|
||||
}
|
||||
if ctx != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
}
|
||||
}
|
||||
return b.Close()
|
||||
}
|
||||
|
||||
// AttachTCP 把裸 TCP/TLS 连接交给 mochi;阻塞到连接结束。
|
||||
func (b *Broker) AttachTCP(conn net.Conn) error {
|
||||
return b.server.EstablishConnection("tcp", conn)
|
||||
@@ -219,44 +269,55 @@ func (b *Broker) AttachWS(conn net.Conn) error {
|
||||
|
||||
// PublishDown 实现 port.Downlink。
|
||||
func (b *Broker) PublishDown(ctx context.Context, endpointID string, connID port.ConnID, payload []byte, opts port.PublishOpts) error {
|
||||
if b.closed.Load() {
|
||||
return errors.New("broker: closed")
|
||||
}
|
||||
st := b.lookupConn(endpointID, connID)
|
||||
if st == nil {
|
||||
return ErrNoConnection
|
||||
}
|
||||
|
||||
limit := effectivePayloadLimit(st.maxPacketSize, st.maxRecvBytes)
|
||||
if limit > 0 && len(payload) > limit {
|
||||
return ErrPayloadTooLarge
|
||||
}
|
||||
|
||||
qos := opts.QoS
|
||||
if qos > 1 {
|
||||
qos = 1
|
||||
}
|
||||
topic := downTopic(endpointID)
|
||||
large := len(payload) > largeFrameBytes
|
||||
if large {
|
||||
if err := b.acquireLarge(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
st.mu.Lock()
|
||||
st.largePending++
|
||||
st.mu.Unlock()
|
||||
return b.enqueueDownlink(endpointID, connID, payload, qos, "")
|
||||
}
|
||||
|
||||
// PublishThenDisconnect 把一帧写入该连接下行队列,写出后再断开(无固定 sleep)。
|
||||
func (b *Broker) PublishThenDisconnect(_ context.Context, endpointID string, connID port.ConnID, payload []byte, qos byte, reason port.DisconnectReason) error {
|
||||
if qos > 1 {
|
||||
qos = 1
|
||||
}
|
||||
if reason == "" {
|
||||
reason = port.DisconnectNormal
|
||||
}
|
||||
return b.enqueueDownlink(endpointID, connID, payload, qos, reason)
|
||||
}
|
||||
|
||||
func (b *Broker) enqueueDownlink(endpointID string, connID port.ConnID, payload []byte, qos byte, disconnect port.DisconnectReason) error {
|
||||
if b.closed.Load() {
|
||||
return errors.New("broker: closed")
|
||||
}
|
||||
st := b.lookupCurrent(endpointID, connID)
|
||||
if st == nil {
|
||||
return ErrNoConnection
|
||||
}
|
||||
|
||||
if err := b.server.Publish(topic, payload, false, qos); err != nil {
|
||||
if large {
|
||||
b.finishLargePublish(st)
|
||||
}
|
||||
return err
|
||||
st.mu.Lock()
|
||||
maxRecv := st.maxRecvBytes
|
||||
closing := st.closing
|
||||
superseded := st.superseded
|
||||
st.mu.Unlock()
|
||||
if closing || superseded {
|
||||
return ErrNoConnection
|
||||
}
|
||||
if large {
|
||||
b.finishLargePublish(st)
|
||||
if !b.hasDownSub(st) {
|
||||
return ErrNotSubscribed
|
||||
}
|
||||
return nil
|
||||
|
||||
limit := EffectivePayloadLimit(st.maxPacketSize, maxRecv)
|
||||
if limit > 0 && len(payload) > limit {
|
||||
return ErrPayloadTooLarge
|
||||
}
|
||||
|
||||
return st.enqueueDown(downItem{
|
||||
payload: append([]byte(nil), payload...),
|
||||
qos: qos,
|
||||
disconnect: disconnect,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *Broker) acquireLarge(ctx context.Context) error {
|
||||
@@ -367,6 +428,31 @@ func (b *Broker) lookupConn(endpointID string, connID port.ConnID) *connState {
|
||||
return b.current[endpointID]
|
||||
}
|
||||
|
||||
// lookupCurrent 只返回该端当前连接;connID 非空时必须仍是当前连接。
|
||||
func (b *Broker) lookupCurrent(endpointID string, connID port.ConnID) *connState {
|
||||
b.connsMu.RLock()
|
||||
defer b.connsMu.RUnlock()
|
||||
cur := b.current[endpointID]
|
||||
if cur == nil {
|
||||
return nil
|
||||
}
|
||||
if connID != "" && cur.connID != connID {
|
||||
return nil
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
func (b *Broker) endpointLife(endpointID string) *sync.Mutex {
|
||||
b.lifeMu.Lock()
|
||||
defer b.lifeMu.Unlock()
|
||||
m := b.lifeLocks[endpointID]
|
||||
if m == nil {
|
||||
m = &sync.Mutex{}
|
||||
b.lifeLocks[endpointID] = m
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func downTopic(endpointID string) string {
|
||||
return "nix/c/" + endpointID + "/down"
|
||||
}
|
||||
@@ -375,6 +461,11 @@ func upTopic(endpointID string) string {
|
||||
return "nix/c/" + endpointID + "/up"
|
||||
}
|
||||
|
||||
// EffectivePayloadLimit 下行载荷上限:客户端 Maximum Packet Size 减包头预留,再与 max_receive_bytes 取更严者。
|
||||
func EffectivePayloadLimit(maxPacketSize uint32, maxRecvBytes int) int {
|
||||
return effectivePayloadLimit(maxPacketSize, maxRecvBytes)
|
||||
}
|
||||
|
||||
func effectivePayloadLimit(maxPacketSize uint32, maxRecvBytes int) int {
|
||||
limit := 0
|
||||
if maxPacketSize > 0 {
|
||||
|
||||
Reference in New Issue
Block a user