断连与限流
前言
本次更新是限制基础限流(防刷屏)和断线通知后端。给 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
}
这个还是挺简单,就是那个断连我写得比较绕,不知道各位能不能看懂。好了,今天就到这。
我是Strandingwhale,也可以叫我bridges,计算机专业,专注游戏后端与Unreal引擎开发。在git pages和博客园上发布文章,其他网站上的都不是本人。
目前还在上大学,希望能做出很牛逼的项目,然后去到我喜欢的岗位。
本人是冯·诺伊曼派,不是图灵学派。
古法编程,AI提供一点思路,少用AI。
喜欢玩明日方舟
喜欢的游戏还有 死亡搁浅 灵魂摆渡人 异星工厂 等
浙公网安备 33010602011771号