using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers; using Gameboard.ShogiUI.Sockets.Managers.Utility; using Gameboard.ShogiUI.Sockets.Models; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Interfaces; using Gameboard.ShogiUI.Sockets.ServiceModels.Socket.Types; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Managers { public interface ISocketCommunicationManager { Task CommunicateWith(WebSocket w, string s); Task BroadcastToAll(IResponse response); Task BroadcastToGame(string gameName, IResponse response); Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2); void SubscribeToGame(Session session, string playerName); void SubscribeToBroadcast(WebSocket socket, string playerName); void UnsubscribeFromBroadcastAndGames(string playerName); void UnsubscribeFromGame(string gameName, string playerName); Task BroadcastToPlayers(IResponse response, params string[] playerNames); } public class SocketCommunicationManager : ISocketCommunicationManager { /// Dictionary key is player name. private readonly ConcurrentDictionary connections; /// Dictionary key is game name. private readonly ConcurrentDictionary sessions; private readonly ILogger logger; private readonly ActionHandlerResolver handlerResolver; public SocketCommunicationManager( ILogger logger, ActionHandlerResolver handlerResolver) { this.logger = logger; this.handlerResolver = handlerResolver; connections = new ConcurrentDictionary(); sessions = new ConcurrentDictionary(); } public async Task CommunicateWith(WebSocket socket, string userName) { SubscribeToBroadcast(socket, userName); while (!socket.CloseStatus.HasValue) { try { var message = await socket.ReceiveTextAsync(); if (string.IsNullOrWhiteSpace(message)) continue; logger.LogInformation("Request \n{0}\n", message); var request = JsonConvert.DeserializeObject(message); if (!Enum.IsDefined(typeof(ClientAction), request.Action)) { await socket.SendTextAsync("Error: Action not recognized."); } else { var handler = handlerResolver(request.Action); await handler.Handle(message, userName); } } catch (OperationCanceledException ex) { logger.LogError(ex.Message); } catch (WebSocketException ex) { logger.LogInformation($"{nameof(WebSocketException)} in {nameof(SocketCommunicationManager)}."); logger.LogInformation("Probably tried writing to a closed socket."); logger.LogError(ex.Message); } } UnsubscribeFromBroadcastAndGames(userName); } public void SubscribeToBroadcast(WebSocket socket, string playerName) { connections.TryAdd(playerName, socket); } public void UnsubscribeFromBroadcastAndGames(string playerName) { connections.TryRemove(playerName, out _); foreach (var kvp in sessions) { var sessionName = kvp.Key; UnsubscribeFromGame(sessionName, playerName); } } /// /// Unsubscribes the player from their current game, then subscribes to the new game. /// public void SubscribeToGame(Session session, string playerName) { // Unsubscribe from any other games foreach (var kvp in sessions) { var gameNameKey = kvp.Key; UnsubscribeFromGame(gameNameKey, playerName); } // Subscribe if (connections.TryGetValue(playerName, out var socket)) { var s = sessions.GetOrAdd(session.Name, session); s.Subscriptions.TryAdd(playerName, socket); } } public void UnsubscribeFromGame(string gameName, string playerName) { if (sessions.TryGetValue(gameName, out var s)) { s.Subscriptions.TryRemove(playerName, out _); if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _); } } public async Task BroadcastToPlayers(IResponse response, params string[] playerNames) { var tasks = new List(playerNames.Length); foreach (var name in playerNames) { if (connections.TryGetValue(name, out var socket)) { var serialized = JsonConvert.SerializeObject(response); logger.LogInformation("Response to {0} \n{1}\n", name, serialized); tasks.Add(socket.SendTextAsync(serialized)); } } await Task.WhenAll(tasks); } public Task BroadcastToAll(IResponse response) { var message = JsonConvert.SerializeObject(response); logger.LogInformation($"Broadcasting\n{0}", message); var tasks = new List(connections.Count); foreach (var kvp in connections) { var socket = kvp.Value; tasks.Add(socket.SendTextAsync(message)); } return Task.WhenAll(tasks); } public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2) { if (sessions.TryGetValue(gameName, out var session)) { var serialized1 = JsonConvert.SerializeObject(forPlayer1); var serialized2 = JsonConvert.SerializeObject(forPlayer2); return Task.WhenAll( session.SendToPlayer1(serialized1), session.SendToPlayer2(serialized2)); } return Task.CompletedTask; } public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers) { if (sessions.TryGetValue(gameName, out var session)) { var serialized = JsonConvert.SerializeObject(messageForAllPlayers); return session.Broadcast(serialized); } return Task.CompletedTask; } } }