前言
当然了,网络游戏中的异常太多了,断线重连,封号处理之类的,但是目前我还没有接触到那些模块,就没必要做超前的处理,等做到了也会整合到这篇博客里,本篇博客主要是讲解思路,可能代码上面有些描述不够清晰,大家可以去我的项目里翻一下完整代码
https://gitee.com/NKG_admin/MKGMobaBasedOnET
常见异常为
- 账号被人顶下来
- 客户端自身或者网络问题(死机,网络不良)无法连接服务器或者与服务器断开连接
- 客户端突发状况(断电,退出游戏),与服务器断开连接
在开始说明解决方案之前,我们先明确几个概念
- 如果客户端这边退出游戏,将会调用Session.Dispose(),并且服务端与之对应的Session也会执行Dispose
- 网络不良或者没有网络,将不会调用Session.Dispose(),要靠双端的心跳包情况来让服务端判断是否要断开连接(执行Session.Dispose()),然后双端各自执行断线后的逻辑
emmm,为什么这两个简单的概念把我绕了一天?(观众:是不是脑瘫就不用我们多说了⑧)
对应的三个解决方案
账号被人顶下来的解决方案
既然是被人顶下来,说明服务端知晓整个过程,所以就用服务端通知客户端的方式来实现整个逻辑
客户端
首先是断线组件,他被添加在一个Session上面(一般是gateSession),当Session.Dispose时,它的Dispose也会被执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| namespace ETHotfix { public class SessionOfflineComponent: Component { public override void Dispose() { if (this.IsDisposed) { return; } base.Dispose(); Game.Scene.RemoveComponent<SessionComponent>(); ETModel.Game.Scene.RemoveComponent<ETModel.SessionComponent>(); } } }
|
定义热更层离线协议
1 2 3 4
| message G2C_PlayerOffline { int32 m_playerOfflineType = 1; }
|
对服务端发来的离线消息进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| namespace ETHotfix { [MessageHandler] public class G2C_PlayerOfflineHandler: AMHandler<G2C_PlayerOffline> { protected override void Run(ETModel.Session session, G2C_PlayerOffline message) { Log.Info("收到了服务端的下线指令"); switch (message.MPlayerOfflineType) { case 1: Log.Info("由于账号被顶而离线"); break; } } } }
|
服务端
服务端要做的事情就比较多了,因为要涉及各个服务器消息的流转
先在InnerMessage定义几个需要在内部流转的协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| message G2R_PlayerOnline // IRequest { int32 RpcId = 90; long PlayerId = 1; string playerAccount = 3; int GateAppID = 2; }
message R2G_PlayerOnline // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; }
message G2R_PlayerOffline // IRequest { int32 RpcId = 90; long PlayerId = 1; string playerAccount = 3; }
message R2G_PlayerOffline // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; }
message R2G_PlayerKickOut // IRequest { int32 RpcId = 90; long PlayerId = 1; PlayerOfflineTypes Playerofflinetypes = 2; string PlayerAccount = 3; }
message G2R_PlayerKickOut // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; }
|
另外,由于InnerMessage的特殊性,需要单独写一个枚举类型
1 2 3 4 5 6 7 8 9 10 11
| namespace ETModel { public enum PlayerOfflineTypes { NoPlayForLongTime = 1, SamePlayerLogin = 2 } }
|
新增OnlineComponent组件,缓存并维护玩家,这个组件应当添加到Game.Scene上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| using System; using System.Collections.Generic;
namespace ETModel { public class OnlineComponent: Component { private readonly Dictionary<string, Tuple<long, int>> m_dictionarty = new Dictionary<string, Tuple<long, int>>();
public void Add(string playerAccount, long playerId, int gateAppId) { this.m_dictionarty.Add(playerAccount, new Tuple<long, int>(playerId, gateAppId)); }
public long GetPlayerId(string playerAccount) { Tuple<long, int> temp = new Tuple<long, int>(0, 0); this.m_dictionarty.TryGetValue(playerAccount, out temp); return temp.Item1; }
public int GetGateAppId(string playerAccount) { if (this.m_dictionarty.Count >= 1) { Tuple<long, int> temp = new Tuple<long, int>(0, 0); this.m_dictionarty.TryGetValue(playerAccount, out temp); return temp.Item2; }
return 0; }
public void Remove(string playerAccount) { Tuple<long, int> temp; if (!this.m_dictionarty.TryGetValue(playerAccount, out temp)) return; this.m_dictionarty.Remove(playerAccount); }
} }
|
修改SessionPlayerComponent,重写Dispose,在最后直接去OnlineComponent移除需要移除的玩家
1 2 3 4 5 6 7 8 9 10 11 12 13
| namespace ETModel { public class SessionPlayerComponent: Component { public Player Player;
public override void Dispose() { base.Dispose(); Game.Scene.GetComponent<OnlineComponent>().Remove(this.Player.Account); } } }
|
定义RealmHelper类,辅助我们对玩家执行下线操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| using System; using System.Net; using ETModel;
namespace ETHotfix { public static class RealmHelper { public static async ETTask KickOutPlayer(string playerAccount, PlayerOfflineTypes playerOfflineType) { int gateAppId = Game.Scene.GetComponent<OnlineComponent>().GetGateAppId(playerAccount); if (gateAppId != 0) { StartConfig playerGateConfig = Game.Scene.GetComponent<StartConfigComponent>().Get(gateAppId); IPEndPoint playerGateIPEndPoint = playerGateConfig.GetComponent<InnerConfig>().IPEndPoint; Session playerGateSession = Game.Scene.GetComponent<NetInnerComponent>().Get(playerGateIPEndPoint); long playerId = Game.Scene.GetComponent<OnlineComponent>().GetPlayerId(playerAccount); Player player = Game.Scene.GetComponent<PlayerComponent>().Get(playerId); long playerSessionId = player.GetComponent<UnitGateComponent>().GateSessionActorId; Session lastGateSession = Game.Scene.GetComponent<NetOuterComponent>().Get(playerSessionId); switch (playerOfflineType) { case PlayerOfflineTypes.NoPlayForLongTime: lastGateSession.Send(new G2C_PlayerOffline() { MPlayerOfflineType = 1 }); break; case PlayerOfflineTypes.SamePlayerLogin: lastGateSession.Send(new G2C_PlayerOffline() { MPlayerOfflineType = 2 }); break; }
await playerGateSession.Call(new R2G_PlayerKickOut() { PlayerAccount = playerAccount, PlayerId = playerId });
Console.WriteLine($"玩家{playerId}已被踢下线"); } } } }
|
接下来是具体的流转过程
1 2 3 4 5 6 7 8 9 10 11
| ==> C2G_LoginGateHandler==>await realmSession.Call(new G2R_PlayerOnline(){ playerAccount = account, PlayerId = player.Id, GateAppID = config.StartConfig.AppId }); ==>G2R_PlayerOnlineHandler ==> await RealmHelper.KickOutPlayer(message.playerAccount, PlayerOfflineTypes.SamePlayerLogin); ==> lastGateSession.Send(new G2C_PlayerOffline() { MPlayerOfflineType = 1 }); ==> await playerGateSession.Call(new R2G_PlayerKickOut() { PlayerAccount = playerAccount, PlayerId = playerId }); ==> onlineComponent.Add(message.playerAccount, message.PlayerId, message.GateAppID);
|
至此账号互顶的双端逻辑结束
客户端或服务端出故障的解决方案
客户端
同样的,先定义几个OutMessage协议
1 2 3 4 5 6 7 8 9 10 11
| message C2G_HeartBeat // IRequest { int32 RpcId = 90; }
message G2C_HeartBeat // IResponse { int32 RpcId = 90; int32 Error = 91; string Message = 92; }
|
心跳组件,同样的,他也会被添加到一个Session上(一般为gateSession)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| namespace ETModel { [ObjectSystem] public class HeartBeatSystem: UpdateSystem<HeartBeatComponent> { public override void Update(HeartBeatComponent self) { self.Update(); } }
public class HeartBeatComponent: Component { public float SendInterval = 10f;
private float RecordDeltaTime = 0f;
private bool hasOffline;
public async void Update() { if (this.hasOffline) return;
if (Time.time - this.RecordDeltaTime < this.SendInterval) return; this.RecordDeltaTime = Time.time;
try { G2C_HeartBeat result = (G2C_HeartBeat) await this.GetParent<Session>().Call(new C2G_HeartBeat()); } catch { if (this.hasOffline) return; this.hasOffline = true; Log.Info("发送心跳包失败"); } } } }
|
服务端
心跳消息处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| namespace ETHotfix { [MessageHandler(AppType.Gate)] public class C2G_HeartBeatHandler: AMRpcHandler<C2G_HeartBeat, G2C_HeartBeat> { protected override void Run(Session session, C2G_HeartBeat message, Action<G2C_HeartBeat> reply) { if (session.GetComponent<HeartBeatComponent>() != null) { session.GetComponent<HeartBeatComponent>().CurrentTime = TimeHelper.ClientNowSeconds(); } reply(new G2C_HeartBeat()); } } }
|
定义心跳组件,这个一般也要放gateSession上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| using System; using System.Net;
namespace ETModel { [ObjectSystem] public class HeartBeatSystem: UpdateSystem<HeartBeatComponent> { public override void Update(HeartBeatComponent self) { self.Update(); } }
public class HeartBeatComponent: Component { public long UpdateInterval = 5;
public long OutInterval = 10;
private long _recordDeltaTime = 0;
public long CurrentTime = 0;
public void Update() { if ((TimeHelper.ClientNowSeconds() - this._recordDeltaTime) < this.UpdateInterval || this.CurrentTime == 0) return; this._recordDeltaTime = TimeHelper.ClientNowSeconds();
if (TimeHelper.ClientNowSeconds() - CurrentTime > OutInterval) { Console.WriteLine("心跳失败"); Game.Scene.GetComponent<NetOuterComponent>().Remove(this.Parent.InstanceId); Game.Scene.GetComponent<NetInnerComponent>().Remove(this.Parent.InstanceId); } else { Console.WriteLine("心跳成功"); } } } }
|
脑瘫解决方案
多玩游戏,多看大佬们吹牛逼。