断连与限流

前言

本次更新是限制基础限流(防刷屏)和断线通知后端。给 Session 加一个每连接消息频率计数(滑动窗口或简单令牌桶):比如 "每 5 秒最多 N 条消息",超出则直接丢弃 / 拒绝,并打日志。心跳(msgId 0)、登录(1000-1999)不要被限流误杀 —— 限流主要针对战斗高频指令(2000+)。目的是防客户端刷指令打垮后端,这是需求 "防止短时间大量请求打垮服务" 的落地。

玩家断开时(handleConn 的 defer 里),网关目前只清 clientMap—— 加一步:若该会话已在房间,给 battle 发一条 "断线通知"(新增内部消息号,如 2010)。battle 收到后标记该房间 "玩家已断线,等待重连"(配合你已有的快照重连机制:断开后房间继续跑,超时未重连再销毁,防止僵尸房间占用)

断连

先在网关的defer里加入断连返回。

	defer func() {
		// 断开时先取出会话状态:已进房且已登录才通知 battle 进入等待重连
		roomID := session.RoomID()
		uid := session.UID()
        ...

		if roomID != 0 && uid != 0 {
			notifyBattleOffline(uid, roomID)
		}
	}()

notifyBattleOffline 客户端断开时通知 battle:该玩家的房间进入等待重连状态

func notifyBattleOffline(uid uint64, roomID uint32) {
	body, err := json.Marshal(struct {
		UID    uint64 `json:"uid"`
		RoomID uint32 `json:"room_id"`
	}{UID: uid, RoomID: roomID})
	if err != nil {
		logger.Println("序列化断线通知失败:", err)
		return
	}
	pkt, err := common.Encode(2010, 0, body)
	if err != nil {
		logger.Println("编码断线通知失败:", err)
		return
	}
	if !battleBackend.send(pkt) {
		logger.Printf("battle 未连接,uid=%d 断线通知丢弃", uid)
		return
	}
	logger.Printf("玩家 uid=%d 断开,通知 battle roomId=%d 等待重连", uid, roomID)
}

再传到bettle服务中网关内部通知:玩家断线,房间进入等待重连状态(无需回包)

    case 2010:
	    // 网关内部通知:玩家断线,房间进入等待重连状态(无需回包)
		handlePlayerOffline(p.Payload)
		continue
// handlePlayerOffline 标记房间玩家已断线,等待重连;超时未重连则销毁房间
func handlePlayerOffline(payload []byte) {
	var n playerOfflineNotify
	if err := json.Unmarshal(payload, &n); err != nil {
		logger.Println("解析断线通知失败:", err)
		return
	}
	room := getRoom(n.RoomID)
	if room == nil {
		logger.Printf("断线通知:房间%d 不存在或已结束", n.RoomID)
		return
	}
	if !room.markOffline(n.UID) {
		logger.Printf("断线通知:房间%d 不属于 uid=%d,忽略", n.RoomID, n.UID)
		return
	}
}

markOffline 标记玩家断线:房间继续 tick 并暂停推送,等待重连;超时未重连则销毁

func (r *Room) markOffline(uid uint64) bool {
	r.mu.Lock()
	if r.over || r.uid != uid {
		r.mu.Unlock()
		return false
	}
	r.disconnected = true
	r.offlineSeq++
	seq := r.offlineSeq
	r.mu.Unlock()

	logger.Printf("房间%d 玩家 uid=%d 已断线,等待重连(超时%v)", r.ID, uid, offlineTimeout)
	go r.destroyIfStillOffline(seq)
	return true
}

destroyIfStillOffline 断线超时仍未重连则销毁房间;seq 防止旧计时器误删已重连的房间

func (r *Room) destroyIfStillOffline(seq uint64) {
	timer := time.NewTimer(offlineTimeout)
	defer timer.Stop()
	<-timer.C

	r.mu.Lock()
	if r.over || !r.disconnected || r.offlineSeq != seq {
		r.mu.Unlock()
		return
	}
	r.over = true
	r.mu.Unlock()

	removeRoom(r.ID)
	logger.Printf("房间%d 断线超时未重连,房间已销毁", r.ID)
}

限流

战斗消息限流:心跳与登录不限流,只针对 2000+ 高频指令

	case p.MessageID >= 2000:
		if !session.AllowBattleMsg(time.Now()) {
			logger.Printf("connID=%d 战斗消息超频,限流丢弃 msgId=%d", session.ConnID(), p.MessageID)
			continue
		}

AllowBattleMsg 战斗消息令牌桶限流:返回 false 表示超频,应丢弃该消息

func (s *Session) AllowBattleMsg(now time.Time) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if elapsed := now.Sub(s.battleRefillAt).Seconds(); elapsed > 0 {
		refill := elapsed * float64(battleMsgCapacity) / battleMsgWindow.Seconds()
		s.battleTokens = math.Min(float64(battleMsgCapacity), s.battleTokens+refill)
		s.battleRefillAt = now
	}
	if s.battleTokens < 1 {
		return false
	}
	s.battleTokens--
	return true
}

这个还是挺简单,就是那个断连我写得比较绕,不知道各位能不能看懂。好了,今天就到这。

posted @ 2026-11-05 22:15  布里吉斯  阅读(16)  评论(0)    收藏  举报