Cysharp / MagicOnion

Unified Realtime/API framework for .NET platform and Unity.
MIT License
3.89k stars 433 forks source link

Change the backend of HubGroup to Multicaster #778

Closed mayuki closed 5 months ago

mayuki commented 5 months ago

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.

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

licentia88 commented 2 months ago

what happened to IInMemoryStorage

mayuki commented 2 months ago

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 commented 2 months ago

Thank you for clarification.