microservices, TCP gateway implemented

This commit is contained in:
xeon 2024-01-19 01:13:40 +03:00
parent 7f9dbbc53d
commit 228709bb6f
35 changed files with 1203 additions and 21 deletions

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
[*.cs]
# IDE0290: Use primary constructor
csharp_style_prefer_primary_constructors = false

View file

@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
</Project>

View 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;
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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;
}
}

View 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..]);
}
}

View 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();
}
}

View 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);
}
}
}
}

View 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;
}
}
}

View file

@ -0,0 +1,7 @@
using RPG.Network.Proto;
namespace RPG.Services.Core.Options;
public class RPGServiceOptions
{
public required RPGServiceType ServiceType { get; set; }
}

View 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));
}
}

View file

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="NetMQ" Version="4.0.1.13" />
</ItemGroup>
<ItemGroup>

View file

@ -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();
}
}

View 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;
}
}
}

View 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
});
}
}

View 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 _);
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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>

View file

@ -0,0 +1,10 @@
using RPG.Services.Core;
namespace RPG.Services.Gameserver;
internal class RPGGameserver : RPGServiceBase
{
public RPGGameserver(ServiceManager serviceManager) : base(serviceManager)
{
// RPGGameserver.
}
}

View 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)
{
}
}

View 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"
}
]
}

View file

@ -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);
}
}
}

View 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
}
}

View 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);
}
}
}

View 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);
}

View file

@ -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();
}
}

View file

@ -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>

View 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();
}
}

View 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);
}
}

View 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
}
}

View file

@ -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