Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the backend of HubGroup to Multicaster #778

Merged
merged 9 commits into from
May 30, 2024
Merged

Conversation

mayuki
Copy link
Member

@mayuki mayuki commented May 29, 2024

This PR migrates the implementation of StreamingHub's Group to a new library called Multicaster.
This allows for controls of the Group to be executed within the Hub's methods or within any application logic.

Added APIs

Client Property

A property Client that returns the receiver for the currently connected client is added to StreamingHubBase<THub, TReceiver>.
This eliminates the need to create a group for receiver invocations to a single client.

Client.OnMessage("Sender", "Hello from the server!");

Updated APIs

IGroup -> IGroup<T>

Group.AddAsync in StreamingHubBase<THub, TReceiver> now returns an IGroup<TReceiver>.

// Before
IGroup group = await Group.AddAsync("ChatRoom-A");
// After
IGroup<IChatReceiver> group = await Group.AddAsync("ChatRoom-A");

Changes to Broadcast Methods

StreamingHub.Broadcast* and IGroup.CreateBroadcaster* methods have been updated to new APIs via IMulticastGroup.

  • Broadcast -> All
  • BroadcastToSelf -> Only or Single or the Client property of StreamingHub
  • BroadcastExcept -> Except
  • BroadcastTo -> Only or Single
// Before
var group = await Group.AddAsync("ChatRoom-A");
BroadcastToSelf(group).OnMessage("Sender", "Message to the current client");
Broadcast(group).OnMessage("Sender", "Message to clients");
BroadcastExcept(group, otherClientContextId).OnMessage("Sender", "Message to clients excepts specified clients");
BroadcastTo(group, otherClientContextId).OnMessage("Sender", "Message to specified clients");
// After
var group = await Group.AddAsync("ChatRoom-A");
Client.OnMessage("Sender", "Message to the current client"); // or `group.Only(Context.ContextId)`
group.All.OnMessage("Sender", "Message to clients");
group.Except(otherClientContextId).OnMessage("Sender", "Message to clients except specified clients");
group.Only(otherClientContextId).OnMessage("Sender", "Message to specified clients");

Changes to Types Specified in GroupConfigurationAttribute

Although GroupConfigurationAttribute can be used to select a group's implementation for each Hub, IHubGroupRepositoryFactory has been removed. Instead, a type for IMulticastGroupProvider must be specified.

[GroupConfiguration(typeof(Cysharp.Runtime.Multicast.Distributed.RedisGroupProvider))]
public class MyHub : StreamingHubBase<...> { ... }

Controlling Groups through Application Logic

By obtaining IMulticastGroupProvider through DI, you can manage the group's lifecycle and members within the application logic.

// for internal API services.
class InternalBattleService(BattleFieldRepository battleFields, IMulticastGroupProvider groupProvider) : ServiceBase<IInternalBattleService>
{
    public async UnaryResult CreateBattleAsync(string battleId)
    {
        var group = groupProvider.GetOrAddSynchronous<UserId, IBattleFieldHubReceiver>(battleId);
        battleFields.Battles[battleId] = new BattleField(group);
    }
    public async UnaryResult CompleteBattleAsync(string battleId)
    {
        if (battleFields.Battles.TryRemove(battleId, out var battleField))
        {
            battleField.Dispose();
        }
    }
}

// for Client
class BattleFieldHub(BattleFieldRepository battleFields) : StreamingHubBase<IBattleFieldHub, IBattleFieldHubReceiver>
{
    public async Task JoinAsync(string battleId)
    {
        battleFields.Battles[battleId].AddMember(User.Id, Client);
    }
    protected override ValueTask OnDisconnected()
    {
        battleFields.Battles[battleId].RemoveMember(User.Id);
        return default;
    }
}

// Game Logics / Models
class BattleFieldRepository
{
    public ConcurrentDictionary<string, BattleField> Battles { get; } = new();
}
class BattleField : IDisposable
{
    private readonly IMulticastSyncGroup<UserId, IBattleHubReceiver> _group;
    public BattleField(IMulticastSyncGroup<UserId, IBattleHubReceiver> group)
    {
        _group = group;
    }
    public void AddMember(UserId id, IBattleHubReceiver member) => _group.Add(id, member);
    public void RemoveMember(UserId id) => _group.Remove(id);
    public void Dispose() => _group.Dispose(); // Unregister the group from the group provider.
}

Groups created with IMulticastGroupProvider can broadcast to clients through the group by registering StreamingHub's Client property.

While this provides flexible management of groups, be aware that the client registration and group lifecycle are no longer managed by MagicOnion, and manual management will be necessary.

Removed APIs

IInMemoryStorage has been deleted.

We thought it would be better if MagicOnion did not manage application state. IInMemoryStorage cannot synchronize state on multiple servers, and it is usual for the game logic to manage state.

Application developers can implement a similar feature using DI and ConcurrentDictionary, etc.

Migration v6 -> v7

The following Shims can be imported into a project to migrate from v6 to v7 to maintain compatibility where existing APIs are used. You can import this Shim into your project and use StreamingHubBaseCompat instead of StreamingHubBase.

https://gist.github.com/mayuki/974ab44d5464eefb821a5619209f7068

@mayuki mayuki merged commit e3019ee into vNext May 30, 2024
3 checks passed
@mayuki mayuki deleted the feature/Multicaster branch May 30, 2024 06:26
@licentia88
Copy link

what happened to IInMemoryStorage

@mayuki
Copy link
Member Author

mayuki commented Sep 24, 2024

IInMemoryStorage will no longer be supported from the next version.

This is because MagicOnion itself is not suitable for storing application state, for example when handling multiple servers or when there is a need to separate logic.

Application developers can implement equivalent functionality using DI and ConcurrentDictionary, etc. as needed.

@licentia88
Copy link

Thank you for clarification.

@flier268
Copy link

How do I send message to Client on another service?

public class AnotherService(IDownloadNotificationHub hub)

There are not enough info about StreamingHub
If I use this, I will get two instance

builder.Services.AddSingleton<IModelNameFis2TcpModeMapService, ModelNameFis2TcpModeMapService>();

Readme.md still using Broadcast but group.All.OnMessage

@mayuki
Copy link
Member Author

mayuki commented Jan 16, 2025

The README is currently for v6.

With v7 (not yet released), you can receive IMulticastGroupProvider via DI. By using the GroupProvider in both the Hub and Service, you can create and delete groups and add and remove members from anywhere.

However, developers need to consider that the lifetime of the group is managed by the application.

public class GroupService(IMulticastGroupProvider groupProvider) : IDisposable
{
    // NOTE: You can also manage multiple groups using a dictionary, etc.
    private readonly IMulticastSyncGroup<Guid, IMyReceiver> _group = groupProvider.GetOrAddSynchronousGroup<Guid, IMyHubReceiver>();

    public void SendMessageToAll(string message) => _group.All.OnMessage(message);

    public void AddMember(Guid id, IMyHubReceiver receiver) => _group.Add(receiver);
    public void RemoveMember(Guid id) => _group.Remove(id);

    public void Dispose() => _group.Dispose();
}

public class MyBackgroundService(GroupService groupService) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var timer = new PeriodicTimer(TimeSpan.FromSeconds(60));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            groupService.SendMessageToAll("Send message periodically...");
        }
    }
}

...

builder.Services.AddSingleton<GroupService>();
builder.Services.AddHostedService<MyBackgroundService>();

...

public class MyHub(GroupService groupService) : StreamingHubBase<IMyHub, IMyHubReceiver>, IMyHub
{
    protected override ValueTask OnConnected()
    {
        groupService.AddMember(ContextId, Client);
        return default;
    }

    protected override ValueTask OnDisconnected()
    {
        groupService.RemoveMember(ContextId);
        return default;
    }

    public Task SendMessage(string message) => groupService.SendMessageToAll(message);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants