修改协议工具
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
using Fantasy;
|
||||
using Fantasy.Async;
|
||||
using Fantasy.Network;
|
||||
using Fantasy.Network.Interface;
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public sealed class C2G_GetAccountInfoRequestHandler : MessageRPC<C2G_GetAccountInfoRequest, G2C_GetAccountInfoResponse>
|
||||
{
|
||||
protected override async FTask Run(Session session, C2G_GetAccountInfoRequest request, G2C_GetAccountInfoResponse response, Action reply)
|
||||
{
|
||||
var gameAccountFlagComponent = session.GetComponent<PlayerFlagComponent>();
|
||||
|
||||
if (gameAccountFlagComponent == null)
|
||||
{
|
||||
// 表示不应该访问这个接口,要先访问登录的接口。
|
||||
// response.ErrorCode = 1;
|
||||
session.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
Player account = gameAccountFlagComponent.Player;
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
// 表示这个Account已经被销毁过了。不是咱们想要的了
|
||||
}
|
||||
|
||||
response.GameAccountInfo = account.GetGameAccountInfo();
|
||||
await FTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
101
Hotfix/Gate/Handler/Outer/C2G_LoginRequestHandler.cs
Normal file
101
Hotfix/Gate/Handler/Outer/C2G_LoginRequestHandler.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using NB.Gate.System;
|
||||
using Fantasy;
|
||||
using Fantasy.Async;
|
||||
using Fantasy.Network;
|
||||
using Fantasy.Network.Interface;
|
||||
|
||||
namespace NB.Gate.Handler;
|
||||
|
||||
public sealed class C2G_LoginRequestHandler : MessageRPC<C2G_LoginRequest, G2C_LoginResponse>
|
||||
{
|
||||
protected override async FTask Run(Session session, C2G_LoginRequest request, G2C_LoginResponse response,
|
||||
Action reply)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.ToKen))
|
||||
{
|
||||
// 1、客户端漏传了 response.ErrorCode = 1;
|
||||
// 2、恶意攻击导致的 session.Dispose();
|
||||
session.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
var scene = session.Scene;
|
||||
// Log.Info($"网关服场景 {scene.Id} {scene.RouteId} {scene.SceneConfigId} {scene.RouteId} {session.RouteId}");
|
||||
if (!GateJWTHelper.ValidateToken(scene, request.ToKen, out var accountId))
|
||||
{
|
||||
// 如果失败,表示肯定是恶意攻击、所以毫不犹疑,直接断开当前会话。
|
||||
session.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// 在缓存中检查该账号是否存在
|
||||
var gameAccountManageComponent = scene.GetComponent<PlayerManageComponent>();
|
||||
Log.Debug("检查账号是否在缓存中");
|
||||
if (!gameAccountManageComponent.TryGet(accountId, out var account))
|
||||
{
|
||||
// 首先要先到数据库中查询是否有这个账号
|
||||
account = await PlayerHelper.LoadDataBase(scene, accountId);
|
||||
// 如果有的话,就直接加入在缓存中就可以了
|
||||
if (account == null)
|
||||
{
|
||||
Log.Debug("检查到账号没有在数据库中,需要创建一个新的账号并且保存到数据库中");
|
||||
// 如果没有,就要创建一个新的并且保存到数据库。
|
||||
// 如果不存在,表示这是一个新的账号,需要创建一下这个账号。
|
||||
account = await PlayerFactory.Create(scene, accountId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("检查到账号在数据库中");
|
||||
}
|
||||
|
||||
Log.Debug("把当前账号添加到缓存中");
|
||||
// 把创建完成的Account放入到缓存中
|
||||
gameAccountManageComponent.Add(account);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("检测到当前账号已经在缓存中了");
|
||||
// 如果有延迟下线的计划任务,那就先取消一下。
|
||||
account.CancelTimeout();
|
||||
// 如果在Gate的缓存中已经存在了该账号那只能以下几种可能:
|
||||
// 1、同一客户端发送了重复登录的请求数据。
|
||||
// 2、客户端经历的断线然后又重新连接到这个服务器上了(断线重连)。
|
||||
// 3、多个客户端同时登录了这个账号(顶号)。
|
||||
|
||||
if (session.RuntimeId == account.SessionRunTimeId)
|
||||
{
|
||||
// 如果执行到这里,说明是客户端发送了多次登录的请求,这样的情况下,直接返回就可以了,不需要做任何操作。
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Debug("检测到当前账号的Session不是同一个");
|
||||
if (scene.TryGetEntity<Session>(account.SessionRunTimeId, out var oldSession))
|
||||
{
|
||||
Log.Debug("当前账号的Session在当前的系统中,所以需要发送一个重复登录的命令,并且要断开这个Session");
|
||||
// 如果这个Session在当前框架中可以查询到。
|
||||
// 那表示就是当前的会话还是在存在的,有如下几个可能:
|
||||
// 1、客户端断线重连,要给这个Session发送一个消息,通知它有人登录了。
|
||||
// 2、其他的客户端登录了这个账号,要给这个Session发送一个消息,通知它有人登录了。
|
||||
|
||||
var gameAccountFlagComponent = oldSession.GetComponent<PlayerFlagComponent>();
|
||||
gameAccountFlagComponent.AccountID = 0;
|
||||
gameAccountFlagComponent.Player = null;
|
||||
// 给客户端发送一个重复登录的消息,如果当前客户端是自己上次登录的,发送也不会收到。
|
||||
oldSession.Send(new G2C_RepeatLogin());
|
||||
// 给当前Session做一个定时销毁的任务,因为不做这个定时销毁,直接销毁的话,有可能消息还没有发送过去就销毁了
|
||||
oldSession.SetTimeout(3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 给当前Session添加一个组件,当Session销毁的时候会销毁这个组件。
|
||||
var accountFlagComponent = session.AddComponent<PlayerFlagComponent>();
|
||||
accountFlagComponent.AccountID = accountId;
|
||||
accountFlagComponent.Player = account;
|
||||
|
||||
account.SessionRunTimeId = session.RuntimeId;
|
||||
response.GameAccountInfo = account.GetGameAccountInfo();
|
||||
Log.Debug($"当前的Gate服务器:{session.Scene.SceneConfigId} accountId:{accountId}");
|
||||
|
||||
//连接到游戏中心服
|
||||
}
|
||||
}
|
||||
64
Hotfix/Gate/System/JWT/GateJWTComponentSystem.cs
Normal file
64
Hotfix/Gate/System/JWT/GateJWTComponentSystem.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Cryptography;
|
||||
using Fantasy.Entitas.Interface;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public sealed class GateJWTComponentAwakeSystem : AwakeSystem<GateJWTComponent>
|
||||
{
|
||||
protected override void Awake(GateJWTComponent self)
|
||||
{
|
||||
self.Awake();
|
||||
}
|
||||
}
|
||||
|
||||
public static class GateJWTComponentSystem
|
||||
{
|
||||
public static void Awake(this GateJWTComponent self)
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportRSAPublicKey(Convert.FromBase64String(self.PublicKeyPem), out _);
|
||||
self.SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
|
||||
// 创建 TokenValidationParameters 对象,用于配置验证参数
|
||||
self.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateLifetime = false, // 禁止令牌验证时间是否过期
|
||||
ValidateIssuer = true, // 验证发行者
|
||||
ValidateAudience = true, // 验证受众
|
||||
ValidateIssuerSigningKey = true, // 验证签名密钥
|
||||
ValidIssuer = "Fantasy", // 有效的发行者
|
||||
ValidAudience = "Fantasy", // 有效的受众
|
||||
IssuerSigningKey = new RsaSecurityKey(rsa) // RSA公钥作为签名密钥
|
||||
};
|
||||
}
|
||||
|
||||
public static bool ValidateToken(this GateJWTComponent self, string token, out JwtPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
|
||||
try
|
||||
{
|
||||
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||
jwtSecurityTokenHandler.ValidateToken(token, self.TokenValidationParameters, out _);
|
||||
payload = jwtSecurityTokenHandler.ReadJwtToken(token).Payload;
|
||||
return true;
|
||||
}
|
||||
catch (SecurityTokenInvalidAudienceException)
|
||||
{
|
||||
Console.WriteLine("验证受众失败!");
|
||||
return false;
|
||||
}
|
||||
catch (SecurityTokenInvalidIssuerException)
|
||||
{
|
||||
Console.WriteLine("验证发行者失败!");
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Hotfix/Gate/System/JWT/GateJWTHelper.cs
Normal file
35
Hotfix/Gate/System/JWT/GateJWTHelper.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using Fantasy;
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public static class GateJWTHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证令牌是否合法
|
||||
/// </summary>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="token">ToKen</param>
|
||||
/// <param name="accountId">如果验证成功会返回正常的AccountId</param>
|
||||
/// <returns>如果是True表示验证成功,False表示验证失败</returns>
|
||||
public static bool ValidateToken(Scene scene, string token, out long accountId)
|
||||
{
|
||||
if (!ValidateToken(scene, token, out JwtPayload payload))
|
||||
{
|
||||
// 如果令牌验证失败,表示当前令牌不合法、那就返回为false,让上层处理。
|
||||
accountId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果不等于当前Scene的ConfigId的话,把就表示该连接不应该连接到当前的Gate里。
|
||||
// 所以理应把当前连接关闭掉。
|
||||
var sceneId = Convert.ToInt64(payload["SceneId"]);
|
||||
accountId = Convert.ToInt64(payload["aId"]);
|
||||
return sceneId == scene.SceneConfigId;
|
||||
}
|
||||
|
||||
private static bool ValidateToken(Scene scene, string token, out JwtPayload payload)
|
||||
{
|
||||
return scene.GetComponent<GateJWTComponent>().ValidateToken(token, out payload);
|
||||
}
|
||||
}
|
||||
13
Hotfix/Gate/System/Player/PlayerDestroySystem.cs
Normal file
13
Hotfix/Gate/System/Player/PlayerDestroySystem.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Fantasy.Entitas.Interface;
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public sealed class PlayerDestroySystem : DestroySystem<Player>
|
||||
{
|
||||
protected override void Destroy(Player self)
|
||||
{
|
||||
self.CreateTime = 0;
|
||||
self.LoginTime = 0;
|
||||
self.SessionRunTimeId = 0;
|
||||
}
|
||||
}
|
||||
29
Hotfix/Gate/System/Player/PlayerFactory.cs
Normal file
29
Hotfix/Gate/System/Player/PlayerFactory.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Fantasy;
|
||||
using Fantasy.Async;
|
||||
using Fantasy.Entitas;
|
||||
using Fantasy.Helper;
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public static class PlayerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个新的Player
|
||||
/// </summary>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="aId">ToKen令牌传递过来的aId</param>
|
||||
/// <param name="isSaveDataBase">是否在创建的过程中保存到数据库</param>
|
||||
/// <returns></returns>
|
||||
public static async FTask<Player> Create(Scene scene, long aId, bool isSaveDataBase = true)
|
||||
{
|
||||
var gameAccount = Entity.Create<Player>(scene, aId, false, false);
|
||||
gameAccount.LoginTime = gameAccount.CreateTime = TimeHelper.Now;
|
||||
|
||||
if (isSaveDataBase)
|
||||
{
|
||||
await gameAccount.SaveDataBase();
|
||||
}
|
||||
|
||||
return gameAccount;
|
||||
}
|
||||
}
|
||||
19
Hotfix/Gate/System/Player/PlayerFlagComponentSystem.cs
Normal file
19
Hotfix/Gate/System/Player/PlayerFlagComponentSystem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Fantasy.Entitas.Interface;
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public sealed class PlayerFlagComponentDestroySystem : DestroySystem<PlayerFlagComponent>
|
||||
{
|
||||
protected override void Destroy(PlayerFlagComponent self)
|
||||
{
|
||||
if (self.AccountID != 0)
|
||||
{
|
||||
// 执行下线过程、并且要求在5分钟后完成缓存清理。也就是5分钟后会保存数据到数据库。
|
||||
// 由于5分钟太长了、咱们测试的时候,不方便测试,也可以把这个时间改短一些,比如10秒。
|
||||
PlayerHelper.Disconnect(self.Scene, self.AccountID, 1000 * 60 * 5).Coroutine();
|
||||
self.AccountID = 0;
|
||||
}
|
||||
|
||||
self.Player = null;
|
||||
}
|
||||
}
|
||||
113
Hotfix/Gate/System/Player/PlayerHelper.cs
Normal file
113
Hotfix/Gate/System/Player/PlayerHelper.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using NB.Gate.System;
|
||||
using Fantasy;
|
||||
using Fantasy.Async;
|
||||
using Fantasy.Network;
|
||||
|
||||
namespace NB.Gate;
|
||||
|
||||
public static class PlayerHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 从数据库中读取GameAccount
|
||||
/// </summary>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="accountId">账号Id</param>
|
||||
/// <returns></returns>
|
||||
public static async FTask<Player> LoadDataBase(Scene scene, long accountId)
|
||||
{
|
||||
var account = await scene.World.DataBase.First<Player>(d => d.Id == accountId);
|
||||
if (account == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
account.Deserialize(scene);
|
||||
return account;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存账号到数据库中
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
public static async FTask SaveDataBase(this Player self)
|
||||
{
|
||||
await self.Scene.World.DataBase.Save(self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行该账号的断开逻辑,不要非必要不要使用这个接口,这个接口是内部使用。
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
public static async FTask Disconnect(this Player self)
|
||||
{
|
||||
// 保存该账号信息到数据库中。
|
||||
await SaveDataBase(self);
|
||||
// 在缓存中移除自己,并且执行自己的Dispose方法。
|
||||
self.Scene.GetComponent<PlayerManageComponent>().Remove(self.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 账号完整的断开逻辑,执行了这个接口后,该账号会完全下线。
|
||||
/// </summary>
|
||||
/// <param name="scene"></param>
|
||||
/// <param name="accountId"></param>
|
||||
/// <param name="timeOut"></param>
|
||||
public static async FTask Disconnect(Scene scene, long accountId, int timeOut = 1000 * 60 * 3)
|
||||
{
|
||||
// 调用该方法有如下几种情况:
|
||||
// 1、客户端主动断开,如:退出游戏、切换账号、等。 客户端会主动发送一个协议给服务器通知服务器断开。
|
||||
// 2、客户端断断线 客户端不会主动发送一个协议给服务器,是由服务器的心跳来检测是否断开了。
|
||||
// 如果是心跳检测断开的Session,我怎么能拿到当前的这个账号来进行下线处理呢?
|
||||
// 通过给当前的Session挂载一个组件,当销毁这个Session时候呢,也会销毁这个组件。
|
||||
// 这样的话,是不是可以在登录的时候,给这个组件把AccountId存到这个组件呢?
|
||||
|
||||
// 要检查当前缓存中是否存在该账号的数据
|
||||
var gameAccountManageComponent = scene.GetComponent<PlayerManageComponent>();
|
||||
if (!gameAccountManageComponent.TryGet(accountId, out var account))
|
||||
{
|
||||
// 如果缓存中没有、那表示已经下线或者根本不存在该账号,应该在打印一个警告,因为正常的情况下是不会出现的。
|
||||
Log.Warning($"GameAccountHelper Disconnect accountId : {accountId} not found");
|
||||
return;
|
||||
}
|
||||
// 为了防止逻辑的错误,加一个警告来排除下
|
||||
if (!scene.TryGetEntity<Session>(account.SessionRunTimeId, out var session))
|
||||
{
|
||||
// 如果没有找到对应的Session,那只有一种可能就是当前的链接会话已经断开了,一般的情况下也不会出现的,所以咱们也要打印一个警告。
|
||||
Log.Warning($"GameAccountHelper Disconnect accountId : {accountId} SessionRunTimeId : {account.SessionRunTimeId} not found");
|
||||
return;
|
||||
}
|
||||
// 如果不存在定时任务的组件,那就添加并设置定时任务
|
||||
if (account.IsTimeOutComponent())
|
||||
{
|
||||
// 如果已经存在了,那就表示当然已经有一个延时断开的任务了,那就不需要重复添加了
|
||||
return;
|
||||
}
|
||||
// 立即下线处理
|
||||
if (timeOut <= 0)
|
||||
{
|
||||
// 如果timeOut销毁或等会0的情况下,执行立即下线。
|
||||
await account.Disconnect();
|
||||
return;
|
||||
}
|
||||
// 设置延迟下线
|
||||
account.SetTimeout(timeOut, account.Disconnect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获得GameAccountInfo
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
/// <returns></returns>
|
||||
public static GameAccountInfo GetGameAccountInfo(this Player self)
|
||||
{
|
||||
// 其实可以不用每次都NEW一个新的GameAccountInfo
|
||||
// 可以在当前账号下创建一个GameAccountInfo,每次变动会提前通知这个GameAccountInfo
|
||||
// 又或者每次调用该方法的时候,把值重新赋值一下。
|
||||
|
||||
return new GameAccountInfo()
|
||||
{
|
||||
CreateTime = self.CreateTime,
|
||||
LoginTime = self.LoginTime
|
||||
};
|
||||
}
|
||||
}
|
||||
49
Hotfix/Gate/System/PlayerManageComponentSystem.cs
Normal file
49
Hotfix/Gate/System/PlayerManageComponentSystem.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Fantasy.Entitas.Interface;
|
||||
|
||||
namespace NB.Gate.System;
|
||||
|
||||
public sealed class PlayerManageComponentDestroySystem : DestroySystem<PlayerManageComponent>
|
||||
{
|
||||
protected override void Destroy(PlayerManageComponent self)
|
||||
{
|
||||
foreach (var (_, gameAccount) in self.Players)
|
||||
{
|
||||
gameAccount.Dispose();
|
||||
}
|
||||
|
||||
self.Players.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayerManageComponentSystem
|
||||
{
|
||||
public static void Add(this PlayerManageComponent self, Player account)
|
||||
{
|
||||
self.Players.Add(account.Id, account);
|
||||
}
|
||||
|
||||
public static Player Get(this PlayerManageComponent self, long accountId)
|
||||
{
|
||||
return self.Players.GetValueOrDefault(accountId);
|
||||
}
|
||||
|
||||
public static bool TryGet(this PlayerManageComponent self, long accountId, out Player? account)
|
||||
{
|
||||
return self.Players.TryGetValue(accountId, out account);
|
||||
}
|
||||
|
||||
public static void Remove(this PlayerManageComponent self, long accountId, bool isDispose = true)
|
||||
{
|
||||
if (!self.Players.Remove(accountId, out var account))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDispose)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
account.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user