From 228709bb6ffc2b51e31ab9c9c9a5f6f8d3bdedf0 Mon Sep 17 00:00:00 2001 From: xeon Date: Fri, 19 Jan 2024 01:13:40 +0300 Subject: [PATCH] microservices, TCP gateway implemented --- .editorconfig | 4 + RPG.GameCore/RPG.GameCore.csproj | 4 + RPG.Network.Proto/CmdType.cs | 218 ++++++++++++++++++ RPG.Network.Proto/RPG.Network.Proto.csproj | 2 + RPG.Network.Proto/server_only.proto | 46 +++- .../HostApplicationBuilderExtensions.cs | 30 +++ .../Attributes/ServiceCommandAttribute.cs | 14 ++ .../Network/Command/ServiceCommand.cs | 16 ++ .../Network/Command/ServiceCommandEncoder.cs | 27 +++ .../Network/Command/ServiceCommandHandler.cs | 66 ++++++ RPG.Services.Core/Network/ServiceBox.cs | 52 +++++ RPG.Services.Core/Network/ServiceEndPoint.cs | 58 +++++ .../Options/RPGServiceOptions.cs | 7 + .../Options/ServiceNodeOptions.cs | 17 ++ RPG.Services.Core/RPG.Services.Core.csproj | 1 + RPG.Services.Core/RPGServiceBase.cs | 17 +- RPG.Services.Core/ServiceManager.cs | 44 ++++ RPG.Services.Core/Session/RPGSession.cs | 47 ++++ RPG.Services.Core/Session/SessionManager.cs | 43 ++++ .../Command/GameserverCommandHandler.cs | 43 ++++ RPG.Services.Gameserver/Program.cs | 18 +- .../RPG.Services.Gameserver.csproj | 10 + RPG.Services.Gameserver/RPGGameserver.cs | 10 + .../Session/PlayerSession.cs | 10 + RPG.Services.Gameserver/appsettings.json | 17 ++ .../Command/GateserverCommandHandler.cs | 44 ++++ RPG.Services.Gateserver/Network/NetPacket.cs | 68 ++++++ .../Network/Tcp/TcpGateway.cs | 87 +++++++ .../Options/GatewayOptions.cs | 10 + RPG.Services.Gateserver/Program.cs | 28 ++- .../RPG.Services.Gateserver.csproj | 10 + RPG.Services.Gateserver/RPGGateserver.cs | 25 ++ .../Session/NetworkSession.cs | 105 +++++++++ RPG.Services.Gateserver/appsettings.json | 21 ++ Snowflake.sln | 5 + 35 files changed, 1203 insertions(+), 21 deletions(-) create mode 100644 .editorconfig create mode 100644 RPG.Network.Proto/CmdType.cs create mode 100644 RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs create mode 100644 RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs create mode 100644 RPG.Services.Core/Network/Command/ServiceCommand.cs create mode 100644 RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs create mode 100644 RPG.Services.Core/Network/Command/ServiceCommandHandler.cs create mode 100644 RPG.Services.Core/Network/ServiceBox.cs create mode 100644 RPG.Services.Core/Network/ServiceEndPoint.cs create mode 100644 RPG.Services.Core/Options/RPGServiceOptions.cs create mode 100644 RPG.Services.Core/Options/ServiceNodeOptions.cs create mode 100644 RPG.Services.Core/ServiceManager.cs create mode 100644 RPG.Services.Core/Session/RPGSession.cs create mode 100644 RPG.Services.Core/Session/SessionManager.cs create mode 100644 RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs create mode 100644 RPG.Services.Gameserver/RPGGameserver.cs create mode 100644 RPG.Services.Gameserver/Session/PlayerSession.cs create mode 100644 RPG.Services.Gameserver/appsettings.json create mode 100644 RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs create mode 100644 RPG.Services.Gateserver/Network/NetPacket.cs create mode 100644 RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs create mode 100644 RPG.Services.Gateserver/Options/GatewayOptions.cs create mode 100644 RPG.Services.Gateserver/RPGGateserver.cs create mode 100644 RPG.Services.Gateserver/Session/NetworkSession.cs create mode 100644 RPG.Services.Gateserver/appsettings.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..75e7a15 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# IDE0290: Use primary constructor +csharp_style_prefer_primary_constructors = false diff --git a/RPG.GameCore/RPG.GameCore.csproj b/RPG.GameCore/RPG.GameCore.csproj index fa71b7a..88c3a39 100644 --- a/RPG.GameCore/RPG.GameCore.csproj +++ b/RPG.GameCore/RPG.GameCore.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/RPG.Network.Proto/CmdType.cs b/RPG.Network.Proto/CmdType.cs new file mode 100644 index 0000000..c055b01 --- /dev/null +++ b/RPG.Network.Proto/CmdType.cs @@ -0,0 +1,218 @@ +namespace RPG.Network.Proto; +public static class CmdType +{ + public const ushort CmdTypeNone = 0; + public const ushort CmdPlayerLoginCsReq = 1; + public const ushort CmdPlayerLoginScRsp = 2; + public const ushort CmdPlayerLogoutCsReq = 3; + public const ushort CmdPlayerLogoutScRsp = 4; + public const ushort CmdPlayerGetTokenCsReq = 5; + public const ushort CmdPlayerGetTokenScRsp = 6; + public const ushort CmdPlayerKeepAliveNotify = 7; + public const ushort CmdGmTalkScNotify = 8; + public const ushort CmdPlayerKickOutScNotify = 9; + public const ushort CmdGmTalkCsReq = 10; + public const ushort CmdGmTalkScRsp = 11; + public const ushort CmdGetStaminaExchangeCsReq = 12; + public const ushort CmdGetStaminaExchangeScRsp = 13; + public const ushort CmdExchangeStaminaCsReq = 14; + public const ushort CmdExchangeStaminaScRsp = 15; + public const ushort CmdGetAuthkeyCsReq = 16; + public const ushort CmdGetAuthkeyScRsp = 17; + public const ushort CmdRegionStopScNotify = 18; + public const ushort CmdAntiAddictScNotify = 19; + public const ushort CmdSetNicknameCsReq = 20; + public const ushort CmdSetNicknameScRsp = 21; + public const ushort CmdGetLevelRewardTakenListCsReq = 22; + public const ushort CmdGetLevelRewardTakenListScRsp = 23; + public const ushort CmdGetLevelRewardCsReq = 24; + public const ushort CmdGetLevelRewardScRsp = 25; + public const ushort CmdSyncTimeCsReq = 26; + public const ushort CmdSyncTimeScRsp = 27; + public const ushort CmdSetLanguageCsReq = 28; + public const ushort CmdSetLanguageScRsp = 29; + public const ushort CmdServerAnnounceNotify = 30; + public const ushort CmdPVEBattleResultCsReq = 101; + public const ushort CmdPVEBattleResultScRsp = 102; + public const ushort CmdQuitBattleCsReq = 103; + public const ushort CmdQuitBattleScRsp = 104; + public const ushort CmdGetCurBattleInfoCsReq = 105; + public const ushort CmdGetCurBattleInfoScRsp = 106; + public const ushort CmdSyncClientResVersionCsReq = 107; + public const ushort CmdSyncClientResVersionScRsp = 108; + public const ushort CmdGetStageDataCsReq = 201; + public const ushort CmdGetStageDataScRsp = 202; + public const ushort CmdStageBeginCsReq = 203; + public const ushort CmdStageBeginScRsp = 204; + public const ushort CmdGetAvatarDataCsReq = 301; + public const ushort CmdGetAvatarDataScRsp = 302; + public const ushort CmdAvatarExpUpCsReq = 303; + public const ushort CmdAvatarExpUpScRsp = 304; + public const ushort CmdUnlockSkilltreeCsReq = 305; + public const ushort CmdUnlockSkilltreeScRsp = 306; + public const ushort CmdPromoteAvatarCsReq = 307; + public const ushort CmdPromoteAvatarScRsp = 308; + public const ushort CmdDressAvatarCsReq = 309; + public const ushort CmdDressAvatarScRsp = 310; + public const ushort CmdTakeOffEquipmentCsReq = 311; + public const ushort CmdTakeOffEquipmentScRsp = 312; + public const ushort CmdAddAvatarScNotify = 313; + public const ushort CmdGetWaypointCsReq = 401; + public const ushort CmdGetWaypointScRsp = 402; + public const ushort CmdSetCurWaypointCsReq = 403; + public const ushort CmdSetCurWaypointScRsp = 404; + public const ushort CmdGetChapterCsReq = 405; + public const ushort CmdGetChapterScRsp = 406; + public const ushort CmdWaypointShowNewCsNotify = 407; + public const ushort CmdTakeChapterRewardCsReq = 408; + public const ushort CmdTakeChapterRewardScRsp = 409; + public const ushort CmdGetBagCsReq = 501; + public const ushort CmdGetBagScRsp = 502; + public const ushort CmdPromoteEquipmentCsReq = 503; + public const ushort CmdPromoteEquipmentScRsp = 504; + public const ushort CmdLockEquipmentCsReq = 505; + public const ushort CmdLockEquipmentScRsp = 506; + public const ushort CmdUseItemCsReq = 507; + public const ushort CmdUseItemScRsp = 508; + public const ushort CmdRankUpEquipmentCsReq = 509; + public const ushort CmdRankUpEquipmentScRsp = 510; + public const ushort CmdExpUpEquipmentCsReq = 511; + public const ushort CmdExpUpEquipmentScRsp = 512; + public const ushort CmdUseItemFoodCsReq = 513; + public const ushort CmdUseItemFoodScRsp = 514; + public const ushort CmdComposeItemCsReq = 515; + public const ushort CmdComposeItemScRsp = 516; + public const ushort CmdPlayerSyncScNotify = 601; + public const ushort CmdGetStageLineupCsReq = 701; + public const ushort CmdGetStageLineupScRsp = 702; + public const ushort CmdGetCurLineupDataCsReq = 703; + public const ushort CmdGetCurLineupDataScRsp = 704; + public const ushort CmdJoinLineupCsReq = 705; + public const ushort CmdJoinLineupScRsp = 706; + public const ushort CmdQuitLineupCsReq = 707; + public const ushort CmdQuitLineupScRsp = 708; + public const ushort CmdSwapLineupCsReq = 709; + public const ushort CmdSwapLineupScRsp = 710; + public const ushort CmdSyncLineupNotify = 711; + public const ushort CmdGetLineupAvatarDataCsReq = 712; + public const ushort CmdGetLineupAvatarDataScRsp = 713; + public const ushort CmdChangeLineupLeaderCsReq = 714; + public const ushort CmdChangeLineupLeaderScRsp = 715; + public const ushort CmdSwitchLineupIndexCsReq = 716; + public const ushort CmdSwitchLineupIndexScRsp = 717; + public const ushort CmdSetLineupNameCsReq = 718; + public const ushort CmdSetLineupNameScRsp = 719; + public const ushort CmdGetAllLineupDataCsReq = 720; + public const ushort CmdGetAllLineupDataScRsp = 721; + public const ushort CmdVirtualLineupDestroyNotify = 722; + public const ushort CmdGetMailCsReq = 801; + public const ushort CmdGetMailScRsp = 802; + public const ushort CmdMarkReadMailCsReq = 803; + public const ushort CmdMarkReadMailScRsp = 804; + public const ushort CmdDelMailCsReq = 805; + public const ushort CmdDelMailScRsp = 806; + public const ushort CmdTakeMailAttachmentCsReq = 807; + public const ushort CmdTakeMailAttachmentScRsp = 808; + public const ushort CmdNewMailScNotify = 809; + public const ushort CmdGetQuestDataCsReq = 901; + public const ushort CmdGetQuestDataScRsp = 902; + public const ushort CmdTakeQuestRewardCsReq = 903; + public const ushort CmdTakeQuestRewardScRsp = 904; + public const ushort CmdGetMazeCsReq = 1001; + public const ushort CmdGetMazeScRsp = 1002; + public const ushort CmdChooseMazeSeriesCsReq = 1003; + public const ushort CmdChooseMazeSeriesScRsp = 1004; + public const ushort CmdChooseMazeAbilityCsReq = 1005; + public const ushort CmdChooseMazeAbilityScRsp = 1006; + public const ushort CmdEnterMazeCsReq = 1007; + public const ushort CmdEnterMazeScRsp = 1008; + public const ushort CmdMazeBuffScNotify = 1011; + public const ushort CmdCastMazeSkillCsReq = 1012; + public const ushort CmdCastMazeSkillScRsp = 1013; + public const ushort CmdMazePlaneEventScNotify = 1014; + public const ushort CmdEnterMazeByServerScNotify = 1015; + public const ushort CmdGetMazeMapInfoCsReq = 1016; + public const ushort CmdGetMazeMapInfoScRsp = 1017; + public const ushort CmdFinishPlotCsReq = 1101; + public const ushort CmdFinishPlotScRsp = 1102; + public const ushort CmdGetMissionDataCsReq = 1201; + public const ushort CmdGetMissionDataScRsp = 1202; + public const ushort CmdFinishTalkMissionCsReq = 1203; + public const ushort CmdFinishTalkMissionScRsp = 1204; + public const ushort CmdMissionRewardScNotify = 1205; + public const ushort CmdSyncTaskCsReq = 1206; + public const ushort CmdSyncTaskScRsp = 1207; + public const ushort CmdDailyTaskDataScNotify = 1208; + public const ushort CmdTakeDailyTaskExtraRewardCsReq = 1209; + public const ushort CmdTakeDailyTaskExtraRewardScRsp = 1210; + public const ushort CmdDailyTaskRewardScNotify = 1211; + public const ushort CmdMissionGroupWarnScNotify = 1212; + public const ushort CmdFinishCosumeItemMissionCsReq = 1213; + public const ushort CmdFinishCosumeItemMissionScRsp = 1214; + public const ushort CmdEnterAdventureCsReq = 1301; + public const ushort CmdEnterAdventureScRsp = 1302; + public const ushort CmdSceneEntityMoveCsReq = 1401; + public const ushort CmdSceneEntityMoveScRsp = 1402; + public const ushort CmdInteractPropCsReq = 1403; + public const ushort CmdInteractPropScRsp = 1404; + public const ushort CmdSceneCastSkillCsReq = 1405; + public const ushort CmdSceneCastSkillScRsp = 1406; + public const ushort CmdGetCurSceneInfoCsReq = 1407; + public const ushort CmdGetCurSceneInfoScRsp = 1408; + public const ushort CmdSceneEntityUpdateScNotify = 1409; + public const ushort CmdSceneEntityDisappearScNotify = 1410; + public const ushort CmdSceneEntityMoveScNotify = 1411; + public const ushort CmdWaitCustomStringCsReq = 1412; + public const ushort CmdWaitCustomStringScRsp = 1413; + public const ushort CmdSpringTransferCsReq = 1414; + public const ushort CmdSpringTransferScRsp = 1415; + public const ushort CmdUpdateBuffScNotify = 1416; + public const ushort CmdDelBuffScNotify = 1417; + public const ushort CmdSpringRefreshCsReq = 1418; + public const ushort CmdSpringRefreshScRsp = 1419; + public const ushort CmdLastSpringRefreshTimeNotify = 1420; + public const ushort CmdReturnLastTownCsReq = 1421; + public const ushort CmdReturnLastTownScRsp = 1422; + public const ushort CmdSceneEnterStageCsReq = 1423; + public const ushort CmdSceneEnterStageScRsp = 1424; + public const ushort CmdEnterSectionCsReq = 1427; + public const ushort CmdEnterSectionScRsp = 1428; + public const ushort CmdSetCurInteractEntityCsReq = 1431; + public const ushort CmdSetCurInteractEntityScRsp = 1432; + public const ushort CmdRecoverAllLineupCsReq = 1433; + public const ushort CmdRecoverAllLineupScRsp = 1434; + public const ushort CmdSavePointsInfoNotify = 1435; + public const ushort CmdStartCocoonStageCsReq = 1436; + public const ushort CmdStartCocoonStageScRsp = 1437; + public const ushort CmdEntityBindPropCsReq = 1438; + public const ushort CmdEntityBindPropScRsp = 1439; + public const ushort CmdSetClientPausedCsReq = 1440; + public const ushort CmdSetClientPausedScRsp = 1441; + public const ushort CmdPropBeHitCsReq = 1442; + public const ushort CmdPropBeHitScRsp = 1443; + public const ushort CmdGetShopListCsReq = 1501; + public const ushort CmdGetShopListScRsp = 1502; + public const ushort CmdBuyGoodsCsReq = 1503; + public const ushort CmdBuyGoodsScRsp = 1504; + public const ushort CmdGetTutorialCsReq = 1601; + public const ushort CmdGetTutorialScRsp = 1602; + public const ushort CmdGetTutorialGuideCsReq = 1603; + public const ushort CmdGetTutorialGuideScRsp = 1604; + public const ushort CmdUnlockTutorialCsReq = 1605; + public const ushort CmdUnlockTutorialScRsp = 1606; + public const ushort CmdUnlockTutorialGuideCsReq = 1607; + public const ushort CmdUnlockTutorialGuideScRsp = 1608; + public const ushort CmdFinishTutorialCsReq = 1609; + public const ushort CmdFinishTutorialScRsp = 1610; + public const ushort CmdFinishTutorialGuideCsReq = 1611; + public const ushort CmdFinishTutorialGuideScRsp = 1612; + public const ushort CmdGetChallengeCsReq = 1701; + public const ushort CmdGetChallengeScRsp = 1702; + public const ushort CmdStartChallengeCsReq = 1703; + public const ushort CmdStartChallengeScRsp = 1704; + public const ushort CmdLeaveChallengeCsReq = 1705; + public const ushort CmdLeaveChallengeScRsp = 1706; + public const ushort CmdChallengeSettleNotify = 1707; + public const ushort CmdFinishChallengeCsReq = 1708; + public const ushort CmdFinishChallengeScRsp = 1709; +} \ No newline at end of file diff --git a/RPG.Network.Proto/RPG.Network.Proto.csproj b/RPG.Network.Proto/RPG.Network.Proto.csproj index fcff061..3263c22 100644 --- a/RPG.Network.Proto/RPG.Network.Proto.csproj +++ b/RPG.Network.Proto/RPG.Network.Proto.csproj @@ -8,6 +8,7 @@ + @@ -20,6 +21,7 @@ + diff --git a/RPG.Network.Proto/server_only.proto b/RPG.Network.Proto/server_only.proto index c892c55..b48b344 100644 --- a/RPG.Network.Proto/server_only.proto +++ b/RPG.Network.Proto/server_only.proto @@ -3,21 +3,49 @@ option csharp_namespace = "RPG.Network.Proto"; enum RPGServiceType { - SERVICE_TYPE_NONE = 0; - SERVICE_TYPE_SDK = 1; - SERVICE_TYPE_GATESERVER = 2; - SERVICE_TYPE_GAMESERVER = 3; + RPG_SERVICE_TYPE_NONE = 0; + RPG_SERVICE_TYPE_SDK = 1; + RPG_SERVICE_TYPE_GATESERVER = 2; + RPG_SERVICE_TYPE_GAMESERVER = 3; } -message ActionMetadata +enum ServiceCommandType { - RPGServiceType sender_type = 1; - uint64 session_id = 2; - uint32 player_uid = 3; + SERVICE_COMMAND_TYPE_NONE = 0; + SERVICE_COMMAND_TYPE_BIND_CONTAINER = 100; + SERVICE_COMMAND_TYPE_BIND_CONTAINER_RESULT = 101; + SERVICE_COMMAND_TYPE_UNBIND_CONTAINER = 102; + SERVICE_COMMAND_TYPE_FORWARD_GAME_MESSAGE = 103; } -message ForwardGameMessageNotify +message CmdBindContainer +{ + uint64 session_id = 1; + uint32 player_uid = 2; +} + +message CmdBindContainerResult +{ + uint32 retcode = 1; + uint64 session_id = 2; +} + +message CmdUnbindContainer +{ + UnbindContainerReason reason = 1; + uint64 session_id = 2; + + enum UnbindContainerReason + { + UNBIND_CONTAINER_REASON_NONE = 0; + UNBIND_CONTAINER_REASON_LOGOUT = 1; + UNBIND_CONTAINER_REASON_KICK = 2; + } +} + +message CmdForwardGameMessage { uint32 cmd_type = 1; bytes payload = 2; + uint64 session_id = 3; } \ No newline at end of file diff --git a/RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs b/RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs new file mode 100644 index 0000000..4cabbc7 --- /dev/null +++ b/RPG.Services.Core/Extensions/HostApplicationBuilderExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using RPG.Services.Core.Network; +using RPG.Services.Core.Network.Command; +using RPG.Services.Core.Options; +using RPG.Services.Core.Session; + +namespace RPG.Services.Core.Extensions; +public static class HostApplicationBuilderExtensions +{ + public static HostApplicationBuilder SetupRPGService(this HostApplicationBuilder builder) + where TService : RPGServiceBase + where TCommandHandler : ServiceCommandHandler + { + IConfigurationSection serviceOptionsSection = builder.Configuration.GetRequiredSection("Service"); + IConfigurationSection serviceNodesSection = builder.Configuration.GetRequiredSection("Nodes"); + + builder.Services.Configure(serviceOptionsSection) + .Configure(serviceNodesSection); + + builder.Services.AddHostedService() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + return builder; + } +} diff --git a/RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs b/RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs new file mode 100644 index 0000000..e2de492 --- /dev/null +++ b/RPG.Services.Core/Network/Attributes/ServiceCommandAttribute.cs @@ -0,0 +1,14 @@ +using RPG.Network.Proto; + +namespace RPG.Services.Core.Network.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public class ServiceCommandAttribute : Attribute +{ + public ServiceCommandType CommandType { get; } + + public ServiceCommandAttribute(ServiceCommandType commandType) + { + CommandType = commandType; + } +} diff --git a/RPG.Services.Core/Network/Command/ServiceCommand.cs b/RPG.Services.Core/Network/Command/ServiceCommand.cs new file mode 100644 index 0000000..846fb25 --- /dev/null +++ b/RPG.Services.Core/Network/Command/ServiceCommand.cs @@ -0,0 +1,16 @@ +using RPG.Network.Proto; + +namespace RPG.Services.Core.Network.Command; +public class ServiceCommand +{ + public RPGServiceType SenderType { get; } + public ServiceCommandType CommandType { get; } + public ReadOnlyMemory Body { get; } + + public ServiceCommand(RPGServiceType sender, ServiceCommandType commandType, ReadOnlyMemory body) + { + SenderType = sender; + CommandType = commandType; + Body = body; + } +} diff --git a/RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs b/RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs new file mode 100644 index 0000000..d37141c --- /dev/null +++ b/RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs @@ -0,0 +1,27 @@ +using System.Buffers.Binary; +using RPG.Network.Proto; + +namespace RPG.Services.Core.Network.Command; +public static class ServiceCommandEncoder +{ + public static ServiceCommand DecodeCommand(ReadOnlyMemory buffer) + { + ReadOnlySpan span = buffer.Span; + + RPGServiceType senderType = (RPGServiceType)span[0]; + ServiceCommandType commandType = (ServiceCommandType)BinaryPrimitives.ReadUInt16BigEndian(span[1..3]); + ReadOnlyMemory body = buffer.Slice(7, BinaryPrimitives.ReadInt32BigEndian(span[3..7])); + + return new(senderType, commandType, body); + } + + public static void EncodeCommand(ServiceCommand command, Memory buffer) + { + Span span = buffer.Span; + + span[0] = (byte)command.SenderType; + BinaryPrimitives.WriteUInt16BigEndian(span[1..3], (ushort)command.CommandType); + BinaryPrimitives.WriteInt32BigEndian(span[3..7], command.Body.Length); + command.Body.CopyTo(buffer[7..]); + } +} diff --git a/RPG.Services.Core/Network/Command/ServiceCommandHandler.cs b/RPG.Services.Core/Network/Command/ServiceCommandHandler.cs new file mode 100644 index 0000000..a84539f --- /dev/null +++ b/RPG.Services.Core/Network/Command/ServiceCommandHandler.cs @@ -0,0 +1,66 @@ +using System.Collections.Immutable; +using System.Linq.Expressions; +using System.Reflection; +using Google.Protobuf; +using Microsoft.Extensions.Logging; +using RPG.Network.Proto; +using RPG.Services.Core.Network.Attributes; + +namespace RPG.Services.Core.Network.Command; +public abstract class ServiceCommandHandler +{ + private delegate Task HandlerDelegate(ServiceCommand command); + private readonly ImmutableDictionary _handlers; + + private readonly ILogger _logger; + private readonly ServiceBox _services; + + public ServiceCommandHandler(ILogger logger, ServiceBox services) + { + _logger = logger; + _services = services; + _handlers = MapHandlers(); + } + + public async Task HandleAsync(ServiceCommand command) + { + if (_handlers.TryGetValue(command.CommandType, out HandlerDelegate? handler)) + { + await handler(command); + } + else + { + _logger.LogWarning("Handler for service command of type {type} not found!", command.CommandType); + } + } + + protected void Send(ServiceCommandType commandType, TBody body, RPGServiceType target) where TBody : IMessage + { + ServiceCommand command = new(_services.CurrentType, commandType, body.ToByteArray()); + + byte[] buffer = GC.AllocateUninitializedArray(command.Body.Length + 7); + ServiceCommandEncoder.EncodeCommand(command, buffer); + + _services.SendToService(target, buffer); + } + + private ImmutableDictionary MapHandlers() + { + var builder = ImmutableDictionary.CreateBuilder(); + + IEnumerable methods = GetType().GetMethods().Where(m => m.GetCustomAttribute() != null); + foreach (MethodInfo method in methods) + { + ServiceCommandAttribute attribute = method.GetCustomAttribute()!; + + Expression self = Expression.Convert(Expression.Constant(this), GetType()); + ParameterExpression commandParameter = Expression.Parameter(typeof(ServiceCommand)); + MethodCallExpression call = Expression.Call(self, method, commandParameter); + + Expression lambda = Expression.Lambda(call, commandParameter); + builder.Add(attribute.CommandType, lambda.Compile()); + } + + return builder.ToImmutable(); + } +} diff --git a/RPG.Services.Core/Network/ServiceBox.cs b/RPG.Services.Core/Network/ServiceBox.cs new file mode 100644 index 0000000..3706c33 --- /dev/null +++ b/RPG.Services.Core/Network/ServiceBox.cs @@ -0,0 +1,52 @@ +using System.Collections.Immutable; +using Microsoft.Extensions.Options; +using NetMQ; +using NetMQ.Sockets; +using RPG.Network.Proto; +using RPG.Services.Core.Options; + +namespace RPG.Services.Core.Network; +public class ServiceBox +{ + private readonly IOptions _nodeOptions; + private readonly IOptions _serviceOptions; + + private ImmutableDictionary? _sockets; + + public ServiceBox(IOptions nodeOptions, IOptions serviceOptions) + { + _nodeOptions = nodeOptions; + _serviceOptions = serviceOptions; + } + + public RPGServiceType CurrentType => _serviceOptions.Value.ServiceType; + + public void Construct() + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (ServiceNodeOptions.Entry entry in _nodeOptions.Value) + { + if (entry.Type == CurrentType) continue; + + NetMQSocket socket = new PushSocket($">tcp://{entry.Host}:{entry.Port}"); + socket.Options.SendHighWatermark = 10000; + builder.Add(entry.Type, socket); + } + + _sockets = builder.ToImmutable(); + } + + public void SendToService(RPGServiceType serviceType, byte[] data) + { + if (_sockets == null) throw new InvalidOperationException("SendToService called when socket map not constructed!"); + + if (_sockets.TryGetValue(serviceType, out NetMQSocket? socket)) + { + lock (socket) + { + socket.SendFrame(data); + } + } + } +} diff --git a/RPG.Services.Core/Network/ServiceEndPoint.cs b/RPG.Services.Core/Network/ServiceEndPoint.cs new file mode 100644 index 0000000..52d9ed7 --- /dev/null +++ b/RPG.Services.Core/Network/ServiceEndPoint.cs @@ -0,0 +1,58 @@ +using NetMQ; +using NetMQ.Sockets; +using RPG.Services.Core.Network.Command; +using RPG.Services.Core.Options; + +namespace RPG.Services.Core.Network; +internal class ServiceEndPoint +{ + private readonly NetMQSocket _socket; + + private CancellationTokenSource? _receiveCancellation; + private Task? _receiveTask; + + public delegate Task CommandEventHandler(ServiceCommand command); + public event CommandEventHandler? OnCommand; + + public ServiceEndPoint(ServiceNodeOptions.Entry optionsEntry) + { + _socket = new PullSocket($"@tcp://{optionsEntry.Host}:{optionsEntry.Port}"); + _socket.Options.ReceiveHighWatermark = 10000; + } + + public void Start() + { + _receiveCancellation = new(); + _receiveTask = Task.Run(() => Receive(_receiveCancellation.Token)); + } + + private async Task Receive(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + NetMQMessage netMessage = _socket.ReceiveMultipartMessage(); + while (!netMessage.IsEmpty) + { + byte[] buffer = netMessage.Pop().Buffer; + if (OnCommand != null) + await OnCommand(ServiceCommandEncoder.DecodeCommand(buffer)); + } + } + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + throw; + } + } + + public async Task StopAsync() + { + if (_receiveCancellation != null && _receiveTask != null) + { + await _receiveCancellation.CancelAsync(); + await _receiveTask; + } + } +} diff --git a/RPG.Services.Core/Options/RPGServiceOptions.cs b/RPG.Services.Core/Options/RPGServiceOptions.cs new file mode 100644 index 0000000..20fac86 --- /dev/null +++ b/RPG.Services.Core/Options/RPGServiceOptions.cs @@ -0,0 +1,7 @@ +using RPG.Network.Proto; + +namespace RPG.Services.Core.Options; +public class RPGServiceOptions +{ + public required RPGServiceType ServiceType { get; set; } +} diff --git a/RPG.Services.Core/Options/ServiceNodeOptions.cs b/RPG.Services.Core/Options/ServiceNodeOptions.cs new file mode 100644 index 0000000..246d5ab --- /dev/null +++ b/RPG.Services.Core/Options/ServiceNodeOptions.cs @@ -0,0 +1,17 @@ +using RPG.Network.Proto; + +namespace RPG.Services.Core.Options; +public class ServiceNodeOptions : List +{ + public class Entry + { + public required RPGServiceType Type { get; set; } + public required string Host { get; set; } + public required int Port { get; set; } + } + + public Entry GetEntry(RPGServiceType type) + { + return Find(e => e.Type == type) ?? throw new ArgumentException("Entry not found", nameof(type)); + } +} diff --git a/RPG.Services.Core/RPG.Services.Core.csproj b/RPG.Services.Core/RPG.Services.Core.csproj index 6b584d4..7ee551b 100644 --- a/RPG.Services.Core/RPG.Services.Core.csproj +++ b/RPG.Services.Core/RPG.Services.Core.csproj @@ -8,6 +8,7 @@ + diff --git a/RPG.Services.Core/RPGServiceBase.cs b/RPG.Services.Core/RPGServiceBase.cs index 6a29dd4..778e65d 100644 --- a/RPG.Services.Core/RPGServiceBase.cs +++ b/RPG.Services.Core/RPGServiceBase.cs @@ -3,13 +3,22 @@ namespace RPG.Services.Core; public abstract class RPGServiceBase : IHostedService { - public virtual Task StartAsync(CancellationToken cancellationToken) + private readonly ServiceManager _serviceManager; + + public RPGServiceBase(ServiceManager serviceManager) { - throw new NotImplementedException(); + _serviceManager = serviceManager; } - public virtual Task StopAsync(CancellationToken cancellationToken) + public virtual Task StartAsync(CancellationToken cancellationToken) { - throw new NotImplementedException(); + _serviceManager.Start(); + + return Task.CompletedTask; + } + + public virtual async Task StopAsync(CancellationToken cancellationToken) + { + await _serviceManager.ShutdownAsync(); } } diff --git a/RPG.Services.Core/ServiceManager.cs b/RPG.Services.Core/ServiceManager.cs new file mode 100644 index 0000000..f717647 --- /dev/null +++ b/RPG.Services.Core/ServiceManager.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Options; +using RPG.Services.Core.Network; +using RPG.Services.Core.Network.Command; +using RPG.Services.Core.Options; + +namespace RPG.Services.Core; +public class ServiceManager +{ + private readonly IOptions _nodeOptions; + private readonly IOptions _serviceOptions; + private readonly ServiceCommandHandler _handler; + private readonly ServiceBox _serviceBox; + + private ServiceEndPoint? _serviceEndPoint; + + public ServiceManager(IOptions options, IOptions serviceOptions, ServiceCommandHandler commandHandler, ServiceBox serviceBox) + { + _nodeOptions = options; + _serviceOptions = serviceOptions; + _handler = commandHandler; + _serviceBox = serviceBox; + } + + public void Start() + { + ServiceNodeOptions nodeOptions = _nodeOptions.Value; + RPGServiceOptions serviceOptions = _serviceOptions.Value; + + _serviceBox.Construct(); + + _serviceEndPoint = new(nodeOptions.GetEntry(serviceOptions.ServiceType)); + _serviceEndPoint.OnCommand += _handler.HandleAsync; + _serviceEndPoint.Start(); + } + + public async Task ShutdownAsync() + { + if (_serviceEndPoint != null) + { + await _serviceEndPoint.StopAsync(); + _serviceEndPoint.OnCommand -= _handler.HandleAsync; + } + } +} diff --git a/RPG.Services.Core/Session/RPGSession.cs b/RPG.Services.Core/Session/RPGSession.cs new file mode 100644 index 0000000..5a608e9 --- /dev/null +++ b/RPG.Services.Core/Session/RPGSession.cs @@ -0,0 +1,47 @@ +using Google.Protobuf; +using RPG.Network.Proto; +using RPG.Services.Core.Network; +using RPG.Services.Core.Network.Command; + +namespace RPG.Services.Core.Session; +public abstract class RPGSession +{ + private readonly ServiceBox _serviceBox; + + public ulong SessionId { get; } + public uint PlayerUid { get; set; } + + public RPGSession(ulong sessionId, ServiceBox serviceBox) + { + SessionId = sessionId; + _serviceBox = serviceBox; + } + + public void SendToService(RPGServiceType target, ServiceCommandType commandType, TBody body) where TBody : IMessage + { + ServiceCommand command = new(_serviceBox.CurrentType, commandType, body.ToByteArray()); + + byte[] commandBuffer = GC.AllocateUninitializedArray(7 + command.Body.Length); + ServiceCommandEncoder.EncodeCommand(command, commandBuffer); + + _serviceBox.SendToService(target, commandBuffer); + } + + public void BindService(RPGServiceType service) + { + SendToService(service, ServiceCommandType.BindContainer, new CmdBindContainer + { + SessionId = SessionId, + PlayerUid = PlayerUid + }); + } + + public void UnbindService(RPGServiceType service, CmdUnbindContainer.Types.UnbindContainerReason reason = CmdUnbindContainer.Types.UnbindContainerReason.Logout) + { + SendToService(service, ServiceCommandType.UnbindContainer, new CmdUnbindContainer + { + SessionId = SessionId, + Reason = reason + }); + } +} diff --git a/RPG.Services.Core/Session/SessionManager.cs b/RPG.Services.Core/Session/SessionManager.cs new file mode 100644 index 0000000..0182389 --- /dev/null +++ b/RPG.Services.Core/Session/SessionManager.cs @@ -0,0 +1,43 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace RPG.Services.Core.Session; +public class SessionManager +{ + private readonly ConcurrentDictionary _sessions; + private readonly IServiceProvider _serviceProvider; + + public SessionManager(IServiceProvider serviceProvider) + { + _sessions = []; + _serviceProvider = serviceProvider; + } + + public TSession? Create(ulong id) where TSession : RPGSession + { + if (_sessions.ContainsKey(id)) return null; + + TSession session = ActivatorUtilities.CreateInstance(_serviceProvider, id); + + _sessions[id] = session; + return session; + } + + public bool TryGet(ulong id, [NotNullWhen(true)] out TSession? session) where TSession : RPGSession + { + if (_sessions.TryGetValue(id, out RPGSession? value)) + { + session = (TSession)value; + return true; + } + + session = null; + return false; + } + + public void Remove(RPGSession session) + { + _ = _sessions.TryRemove(session.SessionId, out _); + } +} diff --git a/RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs b/RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs new file mode 100644 index 0000000..da497d5 --- /dev/null +++ b/RPG.Services.Gameserver/Network/Command/GameserverCommandHandler.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Logging; +using RPG.Network.Proto; +using RPG.Services.Core.Network; +using RPG.Services.Core.Network.Attributes; +using RPG.Services.Core.Network.Command; +using RPG.Services.Core.Session; +using RPG.Services.Gameserver.Session; + +namespace RPG.Services.Gameserver.Network.Command; +internal class GameserverCommandHandler : ServiceCommandHandler +{ + private readonly SessionManager _sessionManager; + + public GameserverCommandHandler(ILogger logger, ServiceBox services, SessionManager sessionManager) : base(logger, services) + { + _sessionManager = sessionManager; + } + + [ServiceCommand(ServiceCommandType.BindContainer)] + public Task OnCmdBindContainer(ServiceCommand command) + { + CmdBindContainer cmdBindContainer = CmdBindContainer.Parser.ParseFrom(command.Body.Span); + PlayerSession? session = _sessionManager.Create(cmdBindContainer.SessionId); + if (session == null) + { + Send(ServiceCommandType.BindContainerResult, new CmdBindContainerResult + { + Retcode = 1, + SessionId = cmdBindContainer.SessionId + }, command.SenderType); + return Task.CompletedTask; + } + + session.PlayerUid = cmdBindContainer.PlayerUid; + Send(ServiceCommandType.BindContainerResult, new CmdBindContainerResult + { + Retcode = 0, + SessionId = cmdBindContainer.SessionId + }, command.SenderType); + + return Task.CompletedTask; + } +} diff --git a/RPG.Services.Gameserver/Program.cs b/RPG.Services.Gameserver/Program.cs index 98f13a6..73192ad 100644 --- a/RPG.Services.Gameserver/Program.cs +++ b/RPG.Services.Gameserver/Program.cs @@ -1,9 +1,19 @@ -namespace RPG.Services.Gameserver; +using Microsoft.Extensions.Hosting; +using RPG.Services.Core.Extensions; +using RPG.Services.Gameserver.Network.Command; -internal class Program +namespace RPG.Services.Gameserver; + +internal static class Program { - static void Main(string[] args) + private static async Task Main(string[] args) { - Console.WriteLine("Hello, World!"); + Console.Title = "Snowflake | Gameserver"; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + + builder.SetupRPGService(); + + await builder.Build().RunAsync(); } } diff --git a/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj b/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj index 2150e37..39a3637 100644 --- a/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj +++ b/RPG.Services.Gameserver/RPG.Services.Gameserver.csproj @@ -7,4 +7,14 @@ enable + + + + + + + PreserveNewest + + + diff --git a/RPG.Services.Gameserver/RPGGameserver.cs b/RPG.Services.Gameserver/RPGGameserver.cs new file mode 100644 index 0000000..ccae1e9 --- /dev/null +++ b/RPG.Services.Gameserver/RPGGameserver.cs @@ -0,0 +1,10 @@ +using RPG.Services.Core; + +namespace RPG.Services.Gameserver; +internal class RPGGameserver : RPGServiceBase +{ + public RPGGameserver(ServiceManager serviceManager) : base(serviceManager) + { + // RPGGameserver. + } +} diff --git a/RPG.Services.Gameserver/Session/PlayerSession.cs b/RPG.Services.Gameserver/Session/PlayerSession.cs new file mode 100644 index 0000000..f54d9e8 --- /dev/null +++ b/RPG.Services.Gameserver/Session/PlayerSession.cs @@ -0,0 +1,10 @@ +using RPG.Services.Core.Network; +using RPG.Services.Core.Session; + +namespace RPG.Services.Gameserver.Session; +internal class PlayerSession : RPGSession +{ + public PlayerSession(ulong sessionId, ServiceBox serviceBox) : base(sessionId, serviceBox) + { + } +} diff --git a/RPG.Services.Gameserver/appsettings.json b/RPG.Services.Gameserver/appsettings.json new file mode 100644 index 0000000..33622e7 --- /dev/null +++ b/RPG.Services.Gameserver/appsettings.json @@ -0,0 +1,17 @@ +{ + "Service": { + "ServiceType": "Gameserver" + }, + "Nodes": [ + { + "Type": "Gateserver", + "Host": "127.0.0.1", + "Port": "21051" + }, + { + "Type": "Gameserver", + "Host": "127.0.0.1", + "Port": "21081" + } + ] +} \ No newline at end of file diff --git a/RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs b/RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs new file mode 100644 index 0000000..b7fd030 --- /dev/null +++ b/RPG.Services.Gateserver/Network/Command/GateserverCommandHandler.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using RPG.Network.Proto; +using RPG.Services.Core.Network; +using RPG.Services.Core.Network.Attributes; +using RPG.Services.Core.Network.Command; +using RPG.Services.Core.Session; +using RPG.Services.Gateserver.Session; + +namespace RPG.Services.Gateserver.Network.Command; +internal class GateserverCommandHandler : ServiceCommandHandler +{ + private readonly SessionManager _sessionManager; + + public GateserverCommandHandler(ILogger logger, ServiceBox services, SessionManager sessionManager) : base(logger, services) + { + _sessionManager = sessionManager; + } + + [ServiceCommand(ServiceCommandType.BindContainerResult)] + public async Task OnBindContainerResult(ServiceCommand command) + { + CmdBindContainerResult result = CmdBindContainerResult.Parser.ParseFrom(command.Body.Span); + + if (_sessionManager.TryGet(result.SessionId, out NetworkSession? session)) + { + PlayerGetTokenScRsp rsp; + if (result.Retcode != 0) + { + rsp = new() { Retcode = 1 }; + } + else + { + rsp = new() + { + Retcode = 0, + Msg = "OK", + Uid = session.PlayerUid + }; + } + + await session.SendAsync(CmdType.CmdPlayerGetTokenScRsp, rsp); + } + } +} diff --git a/RPG.Services.Gateserver/Network/NetPacket.cs b/RPG.Services.Gateserver/Network/NetPacket.cs new file mode 100644 index 0000000..f88b77f --- /dev/null +++ b/RPG.Services.Gateserver/Network/NetPacket.cs @@ -0,0 +1,68 @@ +using System.Buffers.Binary; + +namespace RPG.Services.Gateserver.Network; +internal class NetPacket +{ + public const int Overhead = 16; + + private const uint HeadMagic = 0x01234567; + private const uint TailMagic = 0x89ABCDEF; + + public ushort CmdType { get; } + public ReadOnlyMemory Head { get; } + public ReadOnlyMemory Body { get; } + + public int Size => Overhead + Head.Length + Body.Length; + + public NetPacket(ushort cmdType, ReadOnlyMemory head, ReadOnlyMemory body) + { + CmdType = cmdType; + Head = head; + Body = body; + } + + public static DeserializationResult TryDeserialize(ReadOnlyMemory buffer, out NetPacket? packet, out int bytesRead) + { + packet = null; + bytesRead = 0; + if (buffer.Length < Overhead) return DeserializationResult.BufferExceeded; + + ReadOnlySpan span = buffer.Span; + + if (BinaryPrimitives.ReadUInt32BigEndian(span[..4]) != HeadMagic) return DeserializationResult.Corrupted; + + ushort cmdType = BinaryPrimitives.ReadUInt16BigEndian(span[4..6]); + int headSize = BinaryPrimitives.ReadUInt16BigEndian(span[6..8]); + int bodySize = BinaryPrimitives.ReadInt32BigEndian(span[8..12]); + + if (buffer.Length < Overhead + headSize + bodySize) return DeserializationResult.BufferExceeded; + if (BinaryPrimitives.ReadUInt32BigEndian(span[(12 + headSize + bodySize)..]) != TailMagic) return DeserializationResult.Corrupted; + + packet = new(cmdType, buffer.Slice(12, headSize), buffer.Slice(12 + headSize, bodySize)); + bytesRead = Overhead + headSize + bodySize; + + return DeserializationResult.Success; + } + + public void Serialize(Memory buffer) + { + Span span = buffer.Span; + + BinaryPrimitives.WriteUInt32BigEndian(span[..4], HeadMagic); + BinaryPrimitives.WriteUInt16BigEndian(span[4..6], CmdType); + BinaryPrimitives.WriteUInt16BigEndian(span[6..8], (ushort)Head.Length); + BinaryPrimitives.WriteInt32BigEndian(span[8..12], Body.Length); + + Head.CopyTo(buffer[12..]); + Body.CopyTo(buffer[(12 + Head.Length)..]); + + BinaryPrimitives.WriteUInt32BigEndian(span[(12 + Head.Length + Body.Length)..], TailMagic); + } + + public enum DeserializationResult + { + Success, + BufferExceeded, + Corrupted + } +} diff --git a/RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs b/RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs new file mode 100644 index 0000000..1cd99da --- /dev/null +++ b/RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs @@ -0,0 +1,87 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RPG.Services.Core.Session; +using RPG.Services.Gateserver.Options; +using RPG.Services.Gateserver.Session; + +namespace RPG.Services.Gateserver.Network.Tcp; +internal class TcpGateway +{ + private readonly IOptions _options; + private readonly SessionManager _sessionManager; + private readonly ILogger _logger; + + private Socket? _socket; + + private CancellationTokenSource? _acceptCancellation; + private Task? _acceptTask; + + private ulong _sessionIdCounter; + + public TcpGateway(IOptions options, SessionManager sessionManager, ILogger logger) + { + _options = options; + _sessionManager = sessionManager; + _logger = logger; + } + + public void Start() + { + IPEndPoint bindEndPoint = _options.Value.BindEndPoint; + + _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _socket.Bind(bindEndPoint); + _socket.Listen(100); + + _acceptCancellation = new(); + _acceptTask = RunAcceptLoopAsync(_acceptCancellation.Token); + + _logger.LogInformation("Listening at tcp://{endPoint}", bindEndPoint); + } + + public async Task StopAsync() + { + if (_acceptCancellation != null && _acceptTask != null) + { + await _acceptCancellation.CancelAsync(); + await _acceptTask; + } + + _socket?.Close(); + } + + private async Task RunAcceptLoopAsync(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + Socket clientSocket = await _socket!.AcceptAsync(cancellationToken); + _ = RunSessionAsync(clientSocket); + } + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + throw; + } + } + + private async Task RunSessionAsync(Socket socket) + { + try + { + NetworkSession? session = _sessionManager.Create(Interlocked.Increment(ref _sessionIdCounter)); + if (session == null) return; + + session.Socket = socket; + + await session.RunAsync(); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + _logger.LogError("Unhandled exception occurred: {exception}", exception); + } + } +} diff --git a/RPG.Services.Gateserver/Options/GatewayOptions.cs b/RPG.Services.Gateserver/Options/GatewayOptions.cs new file mode 100644 index 0000000..d52c435 --- /dev/null +++ b/RPG.Services.Gateserver/Options/GatewayOptions.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace RPG.Services.Gateserver.Options; +internal class GatewayOptions +{ + public required string Host { get; set; } + public required int Port { get; set; } + + public IPEndPoint BindEndPoint => new(IPAddress.Parse(Host), Port); +} diff --git a/RPG.Services.Gateserver/Program.cs b/RPG.Services.Gateserver/Program.cs index 00230ad..ac5e29e 100644 --- a/RPG.Services.Gateserver/Program.cs +++ b/RPG.Services.Gateserver/Program.cs @@ -1,9 +1,29 @@ -namespace RPG.Services.Gateserver; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using RPG.Services.Core.Extensions; +using RPG.Services.Gateserver.Network.Command; +using RPG.Services.Gateserver.Network.Tcp; +using RPG.Services.Gateserver.Options; -internal class Program +namespace RPG.Services.Gateserver; + +internal static class Program { - static void Main(string[] args) + private const string GatewayOptionsSectionName = "Gateway"; + + private static async Task Main(string[] args) { - Console.WriteLine("Hello, World!"); + Console.Title = "Snowflake | Gateserver"; + + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + builder.SetupRPGService(); + + IConfigurationSection gatewaySection = builder.Configuration.GetRequiredSection(GatewayOptionsSectionName); + + builder.Services.Configure(gatewaySection) + .AddSingleton(); + + await builder.Build().RunAsync(); } } diff --git a/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj b/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj index 2150e37..39a3637 100644 --- a/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj +++ b/RPG.Services.Gateserver/RPG.Services.Gateserver.csproj @@ -7,4 +7,14 @@ enable + + + + + + + PreserveNewest + + + diff --git a/RPG.Services.Gateserver/RPGGateserver.cs b/RPG.Services.Gateserver/RPGGateserver.cs new file mode 100644 index 0000000..d6ad11e --- /dev/null +++ b/RPG.Services.Gateserver/RPGGateserver.cs @@ -0,0 +1,25 @@ +using RPG.Services.Core; +using RPG.Services.Gateserver.Network.Tcp; + +namespace RPG.Services.Gateserver; +internal class RPGGateserver : RPGServiceBase +{ + private readonly TcpGateway _gateway; + + public RPGGateserver(ServiceManager serviceManager, TcpGateway tcpGateway) : base(serviceManager) + { + _gateway = tcpGateway; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await base.StartAsync(cancellationToken); + _gateway.Start(); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await base.StopAsync(cancellationToken); + await _gateway.StopAsync(); + } +} diff --git a/RPG.Services.Gateserver/Session/NetworkSession.cs b/RPG.Services.Gateserver/Session/NetworkSession.cs new file mode 100644 index 0000000..cca6745 --- /dev/null +++ b/RPG.Services.Gateserver/Session/NetworkSession.cs @@ -0,0 +1,105 @@ +using System.Net.Sockets; +using Google.Protobuf; +using RPG.Network.Proto; +using RPG.Services.Core.Network; +using RPG.Services.Core.Network.Command; +using RPG.Services.Core.Session; +using RPG.Services.Gateserver.Network; + +namespace RPG.Services.Gateserver.Session; +internal class NetworkSession : RPGSession +{ + private const int ReadTimeoutMs = 30000; + private const int ReceiveBufferSize = 16384; + + private readonly byte[] _recvBuffer; + + public Socket? Socket { private get; set; } + public PlayerGetTokenCsReq? GetTokenCsReq { get; private set; } + + public NetworkSession(ulong sessionId, ServiceBox serviceBox) : base(sessionId, serviceBox) + { + _recvBuffer = GC.AllocateUninitializedArray(ReceiveBufferSize); + } + + public async Task RunAsync() + { + if (Socket == null) throw new InvalidOperationException("RunAsync called but socket was not set!"); + + int recvBufferIdx = 0; + Memory recvBufferMem = _recvBuffer.AsMemory(); + + while (true) + { + int readAmount = await ReadWithTimeoutAsync(Socket, recvBufferMem[recvBufferIdx..], ReadTimeoutMs); + if (readAmount == 0) break; + + recvBufferIdx += readAmount; + + do + { + NetPacket.DeserializationResult result = NetPacket.TryDeserialize(recvBufferMem[..recvBufferIdx], out NetPacket? packet, out int bytesRead); + if (result == NetPacket.DeserializationResult.BufferExceeded) break; + if (result == NetPacket.DeserializationResult.Corrupted) return; + + HandleSessionPacketAsync(packet!); + Buffer.BlockCopy(_recvBuffer, recvBufferIdx, _recvBuffer, 0, recvBufferIdx -= bytesRead); + } + while (recvBufferIdx >= NetPacket.Overhead); + } + } + + public async Task SendAsync(ushort cmdType, TBody body) where TBody : IMessage + { + await SendAsync(new(cmdType, ReadOnlyMemory.Empty, body.ToByteArray())); + } + + public async Task SendAsync(NetPacket packet) + { + if (Socket == null) return; + + byte[] buffer = GC.AllocateUninitializedArray(packet.Size); + packet.Serialize(buffer); + + await Socket!.SendAsync(buffer); + } + + private void HandleSessionPacketAsync(NetPacket packet) + { + switch (packet.CmdType) + { + case CmdType.CmdPlayerGetTokenCsReq: + HandlePlayerGetTokenCsReq(PlayerGetTokenCsReq.Parser.ParseFrom(packet.Body.Span)); + break; + case CmdType.CmdPlayerKeepAliveNotify: + break; + default: + ForwardToGameserver(packet); + break; + } + } + + private void HandlePlayerGetTokenCsReq(PlayerGetTokenCsReq req) + { + GetTokenCsReq = req; + PlayerUid = uint.Parse(req.AccountUid); + + BindService(RPGServiceType.Gameserver); + } + + private void ForwardToGameserver(NetPacket packet) + { + SendToService(RPGServiceType.Gameserver, ServiceCommandType.ForwardGameMessage, new CmdForwardGameMessage + { + SessionId = SessionId, + CmdType = packet.CmdType, + Payload = ByteString.CopyFrom(packet.Body.Span) + }); + } + + private static async ValueTask ReadWithTimeoutAsync(Socket socket, Memory buffer, int timeoutMs) + { + CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(timeoutMs)); + return await socket.ReceiveAsync(buffer, cts.Token); + } +} diff --git a/RPG.Services.Gateserver/appsettings.json b/RPG.Services.Gateserver/appsettings.json new file mode 100644 index 0000000..dbbf968 --- /dev/null +++ b/RPG.Services.Gateserver/appsettings.json @@ -0,0 +1,21 @@ +{ + "Service": { + "ServiceType": "Gateserver" + }, + "Nodes": [ + { + "Type": "Gateserver", + "Host": "127.0.0.1", + "Port": "21051" + }, + { + "Type": "Gameserver", + "Host": "127.0.0.1", + "Port": "21081" + } + ], + "Gateway": { + "Host": "0.0.0.0", + "Port": 20301 + } +} \ No newline at end of file diff --git a/Snowflake.sln b/Snowflake.sln index 5b3e1f1..f90ed67 100644 --- a/Snowflake.sln +++ b/Snowflake.sln @@ -19,6 +19,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Gameserver", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Core", "RPG.Services.Core\RPG.Services.Core.csproj", "{1B434662-DEC9-40C9-A709-CE87026191D9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D96091AB-B78F-4092-ADEF-7A4D9F1B90C6}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU