microservices, TCP gateway implemented
This commit is contained in:
parent
7f9dbbc53d
commit
228709bb6f
35 changed files with 1203 additions and 21 deletions
4
.editorconfig
Normal file
4
.editorconfig
Normal file
|
@ -0,0 +1,4 @@
|
|||
[*.cs]
|
||||
|
||||
# IDE0290: Use primary constructor
|
||||
csharp_style_prefer_primary_constructors = false
|
|
@ -6,4 +6,8 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
218
RPG.Network.Proto/CmdType.cs
Normal file
218
RPG.Network.Proto/CmdType.cs
Normal file
|
@ -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;
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Remove="Messages.proto" />
|
||||
<None Remove="server_only.proto" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -20,6 +21,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="messages.proto" GrpcServices="None" />
|
||||
<Protobuf Include="server_only.proto" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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<TService, TCommandHandler>(this HostApplicationBuilder builder)
|
||||
where TService : RPGServiceBase
|
||||
where TCommandHandler : ServiceCommandHandler
|
||||
{
|
||||
IConfigurationSection serviceOptionsSection = builder.Configuration.GetRequiredSection("Service");
|
||||
IConfigurationSection serviceNodesSection = builder.Configuration.GetRequiredSection("Nodes");
|
||||
|
||||
builder.Services.Configure<RPGServiceOptions>(serviceOptionsSection)
|
||||
.Configure<ServiceNodeOptions>(serviceNodesSection);
|
||||
|
||||
builder.Services.AddHostedService<TService>()
|
||||
.AddSingleton<ServiceManager>()
|
||||
.AddSingleton<SessionManager>()
|
||||
.AddSingleton<ServiceBox>()
|
||||
.AddSingleton<ServiceCommandHandler, TCommandHandler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
16
RPG.Services.Core/Network/Command/ServiceCommand.cs
Normal file
16
RPG.Services.Core/Network/Command/ServiceCommand.cs
Normal file
|
@ -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<byte> Body { get; }
|
||||
|
||||
public ServiceCommand(RPGServiceType sender, ServiceCommandType commandType, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
SenderType = sender;
|
||||
CommandType = commandType;
|
||||
Body = body;
|
||||
}
|
||||
}
|
27
RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs
Normal file
27
RPG.Services.Core/Network/Command/ServiceCommandEncoder.cs
Normal file
|
@ -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<byte> buffer)
|
||||
{
|
||||
ReadOnlySpan<byte> span = buffer.Span;
|
||||
|
||||
RPGServiceType senderType = (RPGServiceType)span[0];
|
||||
ServiceCommandType commandType = (ServiceCommandType)BinaryPrimitives.ReadUInt16BigEndian(span[1..3]);
|
||||
ReadOnlyMemory<byte> body = buffer.Slice(7, BinaryPrimitives.ReadInt32BigEndian(span[3..7]));
|
||||
|
||||
return new(senderType, commandType, body);
|
||||
}
|
||||
|
||||
public static void EncodeCommand(ServiceCommand command, Memory<byte> buffer)
|
||||
{
|
||||
Span<byte> 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..]);
|
||||
}
|
||||
}
|
66
RPG.Services.Core/Network/Command/ServiceCommandHandler.cs
Normal file
66
RPG.Services.Core/Network/Command/ServiceCommandHandler.cs
Normal file
|
@ -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<ServiceCommandType, HandlerDelegate> _handlers;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ServiceBox _services;
|
||||
|
||||
public ServiceCommandHandler(ILogger<ServiceCommandHandler> 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<TBody>(ServiceCommandType commandType, TBody body, RPGServiceType target) where TBody : IMessage<TBody>
|
||||
{
|
||||
ServiceCommand command = new(_services.CurrentType, commandType, body.ToByteArray());
|
||||
|
||||
byte[] buffer = GC.AllocateUninitializedArray<byte>(command.Body.Length + 7);
|
||||
ServiceCommandEncoder.EncodeCommand(command, buffer);
|
||||
|
||||
_services.SendToService(target, buffer);
|
||||
}
|
||||
|
||||
private ImmutableDictionary<ServiceCommandType, HandlerDelegate> MapHandlers()
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<ServiceCommandType, HandlerDelegate>();
|
||||
|
||||
IEnumerable<MethodInfo> methods = GetType().GetMethods().Where(m => m.GetCustomAttribute<ServiceCommandAttribute>() != null);
|
||||
foreach (MethodInfo method in methods)
|
||||
{
|
||||
ServiceCommandAttribute attribute = method.GetCustomAttribute<ServiceCommandAttribute>()!;
|
||||
|
||||
Expression self = Expression.Convert(Expression.Constant(this), GetType());
|
||||
ParameterExpression commandParameter = Expression.Parameter(typeof(ServiceCommand));
|
||||
MethodCallExpression call = Expression.Call(self, method, commandParameter);
|
||||
|
||||
Expression<HandlerDelegate> lambda = Expression.Lambda<HandlerDelegate>(call, commandParameter);
|
||||
builder.Add(attribute.CommandType, lambda.Compile());
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
52
RPG.Services.Core/Network/ServiceBox.cs
Normal file
52
RPG.Services.Core/Network/ServiceBox.cs
Normal file
|
@ -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<ServiceNodeOptions> _nodeOptions;
|
||||
private readonly IOptions<RPGServiceOptions> _serviceOptions;
|
||||
|
||||
private ImmutableDictionary<RPGServiceType, NetMQSocket>? _sockets;
|
||||
|
||||
public ServiceBox(IOptions<ServiceNodeOptions> nodeOptions, IOptions<RPGServiceOptions> serviceOptions)
|
||||
{
|
||||
_nodeOptions = nodeOptions;
|
||||
_serviceOptions = serviceOptions;
|
||||
}
|
||||
|
||||
public RPGServiceType CurrentType => _serviceOptions.Value.ServiceType;
|
||||
|
||||
public void Construct()
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<RPGServiceType, NetMQSocket>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
RPG.Services.Core/Network/ServiceEndPoint.cs
Normal file
58
RPG.Services.Core/Network/ServiceEndPoint.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
7
RPG.Services.Core/Options/RPGServiceOptions.cs
Normal file
7
RPG.Services.Core/Options/RPGServiceOptions.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using RPG.Network.Proto;
|
||||
|
||||
namespace RPG.Services.Core.Options;
|
||||
public class RPGServiceOptions
|
||||
{
|
||||
public required RPGServiceType ServiceType { get; set; }
|
||||
}
|
17
RPG.Services.Core/Options/ServiceNodeOptions.cs
Normal file
17
RPG.Services.Core/Options/ServiceNodeOptions.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using RPG.Network.Proto;
|
||||
|
||||
namespace RPG.Services.Core.Options;
|
||||
public class ServiceNodeOptions : List<ServiceNodeOptions.Entry>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="NetMQ" Version="4.0.1.13" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
44
RPG.Services.Core/ServiceManager.cs
Normal file
44
RPG.Services.Core/ServiceManager.cs
Normal file
|
@ -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<ServiceNodeOptions> _nodeOptions;
|
||||
private readonly IOptions<RPGServiceOptions> _serviceOptions;
|
||||
private readonly ServiceCommandHandler _handler;
|
||||
private readonly ServiceBox _serviceBox;
|
||||
|
||||
private ServiceEndPoint? _serviceEndPoint;
|
||||
|
||||
public ServiceManager(IOptions<ServiceNodeOptions> options, IOptions<RPGServiceOptions> 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;
|
||||
}
|
||||
}
|
||||
}
|
47
RPG.Services.Core/Session/RPGSession.cs
Normal file
47
RPG.Services.Core/Session/RPGSession.cs
Normal file
|
@ -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<TBody>(RPGServiceType target, ServiceCommandType commandType, TBody body) where TBody : IMessage<TBody>
|
||||
{
|
||||
ServiceCommand command = new(_serviceBox.CurrentType, commandType, body.ToByteArray());
|
||||
|
||||
byte[] commandBuffer = GC.AllocateUninitializedArray<byte>(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
|
||||
});
|
||||
}
|
||||
}
|
43
RPG.Services.Core/Session/SessionManager.cs
Normal file
43
RPG.Services.Core/Session/SessionManager.cs
Normal file
|
@ -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<ulong, RPGSession> _sessions;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public SessionManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
_sessions = [];
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public TSession? Create<TSession>(ulong id) where TSession : RPGSession
|
||||
{
|
||||
if (_sessions.ContainsKey(id)) return null;
|
||||
|
||||
TSession session = ActivatorUtilities.CreateInstance<TSession>(_serviceProvider, id);
|
||||
|
||||
_sessions[id] = session;
|
||||
return session;
|
||||
}
|
||||
|
||||
public bool TryGet<TSession>(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 _);
|
||||
}
|
||||
}
|
|
@ -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<ServiceCommandHandler> 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<PlayerSession>(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;
|
||||
}
|
||||
}
|
|
@ -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<RPGGameserver, GameserverCommandHandler>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,14 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RPG.Services.Core\RPG.Services.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
10
RPG.Services.Gameserver/RPGGameserver.cs
Normal file
10
RPG.Services.Gameserver/RPGGameserver.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using RPG.Services.Core;
|
||||
|
||||
namespace RPG.Services.Gameserver;
|
||||
internal class RPGGameserver : RPGServiceBase
|
||||
{
|
||||
public RPGGameserver(ServiceManager serviceManager) : base(serviceManager)
|
||||
{
|
||||
// RPGGameserver.
|
||||
}
|
||||
}
|
10
RPG.Services.Gameserver/Session/PlayerSession.cs
Normal file
10
RPG.Services.Gameserver/Session/PlayerSession.cs
Normal file
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
17
RPG.Services.Gameserver/appsettings.json
Normal file
17
RPG.Services.Gameserver/appsettings.json
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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<ServiceCommandHandler> 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);
|
||||
}
|
||||
}
|
||||
}
|
68
RPG.Services.Gateserver/Network/NetPacket.cs
Normal file
68
RPG.Services.Gateserver/Network/NetPacket.cs
Normal file
|
@ -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<byte> Head { get; }
|
||||
public ReadOnlyMemory<byte> Body { get; }
|
||||
|
||||
public int Size => Overhead + Head.Length + Body.Length;
|
||||
|
||||
public NetPacket(ushort cmdType, ReadOnlyMemory<byte> head, ReadOnlyMemory<byte> body)
|
||||
{
|
||||
CmdType = cmdType;
|
||||
Head = head;
|
||||
Body = body;
|
||||
}
|
||||
|
||||
public static DeserializationResult TryDeserialize(ReadOnlyMemory<byte> buffer, out NetPacket? packet, out int bytesRead)
|
||||
{
|
||||
packet = null;
|
||||
bytesRead = 0;
|
||||
if (buffer.Length < Overhead) return DeserializationResult.BufferExceeded;
|
||||
|
||||
ReadOnlySpan<byte> 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<byte> buffer)
|
||||
{
|
||||
Span<byte> 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
|
||||
}
|
||||
}
|
87
RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs
Normal file
87
RPG.Services.Gateserver/Network/Tcp/TcpGateway.cs
Normal file
|
@ -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<GatewayOptions> _options;
|
||||
private readonly SessionManager _sessionManager;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private Socket? _socket;
|
||||
|
||||
private CancellationTokenSource? _acceptCancellation;
|
||||
private Task? _acceptTask;
|
||||
|
||||
private ulong _sessionIdCounter;
|
||||
|
||||
public TcpGateway(IOptions<GatewayOptions> options, SessionManager sessionManager, ILogger<TcpGateway> 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<NetworkSession>(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);
|
||||
}
|
||||
}
|
||||
}
|
10
RPG.Services.Gateserver/Options/GatewayOptions.cs
Normal file
10
RPG.Services.Gateserver/Options/GatewayOptions.cs
Normal file
|
@ -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);
|
||||
}
|
|
@ -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<RPGGateserver, GateserverCommandHandler>();
|
||||
|
||||
IConfigurationSection gatewaySection = builder.Configuration.GetRequiredSection(GatewayOptionsSectionName);
|
||||
|
||||
builder.Services.Configure<GatewayOptions>(gatewaySection)
|
||||
.AddSingleton<TcpGateway>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,14 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RPG.Services.Core\RPG.Services.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
25
RPG.Services.Gateserver/RPGGateserver.cs
Normal file
25
RPG.Services.Gateserver/RPGGateserver.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
105
RPG.Services.Gateserver/Session/NetworkSession.cs
Normal file
105
RPG.Services.Gateserver/Session/NetworkSession.cs
Normal file
|
@ -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<byte>(ReceiveBufferSize);
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
if (Socket == null) throw new InvalidOperationException("RunAsync called but socket was not set!");
|
||||
|
||||
int recvBufferIdx = 0;
|
||||
Memory<byte> 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<TBody>(ushort cmdType, TBody body) where TBody : IMessage<TBody>
|
||||
{
|
||||
await SendAsync(new(cmdType, ReadOnlyMemory<byte>.Empty, body.ToByteArray()));
|
||||
}
|
||||
|
||||
public async Task SendAsync(NetPacket packet)
|
||||
{
|
||||
if (Socket == null) return;
|
||||
|
||||
byte[] buffer = GC.AllocateUninitializedArray<byte>(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<int> ReadWithTimeoutAsync(Socket socket, Memory<byte> buffer, int timeoutMs)
|
||||
{
|
||||
CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(timeoutMs));
|
||||
return await socket.ReceiveAsync(buffer, cts.Token);
|
||||
}
|
||||
}
|
21
RPG.Services.Gateserver/appsettings.json
Normal file
21
RPG.Services.Gateserver/appsettings.json
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue