@DailyDDose

Как реализовать роутинг для WebSocketServer?

Клиенты могут отправлять серверу разные сообщения в формате JSON:
{
    Type: "AUTH_MESSAGE",
    Payload: {
        UserName: "dailydose"
    }
}

{
    Type: "CHAT_MESSAGE",
    Payload: {
        Message: "hello world"
    }
}


Сервер же должен их принимать и обрабатывать их в зависимости от типа сообщения
server.OnMessage = webSocketMessage => Router.Handle(socket, webSocketMessage, this);


Я попробовал реализовать этот функционал на основе Цепочки обязанностей(Chain of responsibility).

Router:
Router
public static class Router
    {
        static Router()
        {
            Handlers = new List<BaseHandler>()
                           {
                               new ChatHandler(),
                               // new UsersListHandler(),
                               new AuthHandler(),
                               new InvalidHandler()
                           };
        }
 
        private static List<BaseHandler> Handlers { get; }
 
        public static void Handle(IWebSocketConnection socket, string webSocketMessage, IServer server)
        {
            var messageType = Newtonsoft.Json.JsonConvert
                .DeserializeObject<WebSocketMessage<Payloads.ChatMessage>>(webSocketMessage).Type;
 
            foreach (var handler in Handlers)
            {
                if (handler.CanHandle(socket, messageType, server))
                {
                    handler.Handle(socket, webSocketMessage, server);
                    return;
                }
            }
        }
    }


handlers:
handlers
public abstract class BaseHandler
    {
        protected abstract string Target { get; }
 
        public abstract void Handle(IWebSocketConnection socket, string webSocketMessage, IServer server);
 
        public virtual bool CanHandle(IWebSocketConnection socket, string messageType, IServer server)
        {
            return messageType == Target;
        }
    }
public class AuthHandler : BaseHandler
    {
        protected override string Target => AuthMessage.Type;
 
        public override bool CanHandle(IWebSocketConnection socket, string messageType, IServer server)
        {
            return base.CanHandle(socket, messageType, server) && !server.ConnectedSockets.ContainsKey(socket);
        }
 
        public override void Handle(IWebSocketConnection socket, string webSocketMessage, IServer server)
        {
            Authorize(socket, webSocketMessage, server);
        }
 
        private void Authorize(IWebSocketConnection socket, string webSocketMessage, IServer server)
        {
            try
            {
                var data = 
                    Newtonsoft.Json.JsonConvert.DeserializeObject<WebSocketMessage<AuthMessage>>(webSocketMessage);
                if (server.ConnectedSockets.ContainsValue(data.Payload.UserName))
                {
                    DisconnectSocket(socket, data, server);
                }
                else
                {
                    ConnectSocket(socket, data, server);
                }
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }
        }
    }
public class ChatHandler : BaseHandler
    {
        protected override string Target => ChatMessage.Type;
 
        public override void Handle(IWebSocketConnection socket, string webSocketMessage, IServer server)
        {
            try
            {
                var data =
                    Newtonsoft.Json.JsonConvert.DeserializeObject<WebSocketMessage<ChatMessage>>(webSocketMessage);
                var chatMessage = new ChatMessage()
                {
                    UserName = server.ConnectedSockets[socket],
                    Message = data.Payload.Message
                };
                SendToAll(chatMessage, server);
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }
        }
 
        public void SendToAll(ChatMessage chatMessage, IServer server)
        {
            var message = Helper.BuildMessage(chatMessage);
            foreach (var client in server.ConnectedSockets)
            {
                client.Key.Send(message);
            }
        }
    }
public class InvalidHandler : BaseHandler
    {
        protected override string Target => string.Empty;
 
        public override bool CanHandle(IWebSocketConnection socket, string messageType, IServer server)
        {
            return true;
        }
 
        public override void Handle(IWebSocketConnection socket, string webSocketMessage, IServer server)
        {
            var invalidMessage = new Payloads.InvalidMessage()
            {
                Message = "Обнаружена ошибка. Предоставьте, пожалуйста, эту информацию разработчикам: "
                    + webSocketMessage
            };
            socket.Send(Helper.BuildMessage(invalidMessage));
        }
    }


messages:
messages
public class WebSocketMessage<T> where T : Payloads.BaseMessage
    {
        public string Type { get; set; }
        public T Payload { get; set; }
    }
public class BaseMessage
    {
        protected virtual string MessageType { get; } = string.Empty;
        public override string ToString() => MessageType;
    }
public partial class AuthMessage : BaseMessage
    {
        public const string Type = "AUTH_MESSAGE";
        public string Message { get; set; }
        public StatusCode Status { get; set; }
        public string UserName { get; set; }
        protected override string MessageType => Type;
    }
public class ChatMessage : BaseMessage
    {
        public const string Type = "CHAT_MESSAGE";
        public string Message { get; set; }
        public string Time { get; set; } = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        public string UserName { get; set; }
        protected override string MessageType => Type;
    }
public class InvalidMessage : BaseMessage
    {
        public const string Type = "INVALID_MESSAGE";
        public string Message { get; set; }
        protected override string MessageType => Type;
    }


И как-то мне оно не очень нравится:
Router - статический класс и содержит в себе экземпляры обработчиков.
Везде во все обработчики передаются аргументы (IWebSocketConnection socket, string webSocketMessage, IServer server) даже там где они не нужны.

Подскажите, пожалуйста, более правильный подход реализации роутинга и/или предложите модификации этого кода.

Спасибо!
  • Вопрос задан
  • 44 просмотра
Решения вопроса 1
@kttotto
пофиг на чем писать
Не совсем понятно зачем Вам нужна цепочка обязонностей, если для каждого типа сообщения выполняется строго один хендлер.
Как только Вы находите первый подходящий, Вы выходите из цикла
if (handler.CanHandle(socket, messageType, server))
{
	handler.Handle(socket, webSocketMessage, server);
	return;
}

Цепочка обязанностей, это по сути конвеер, по которому пройдет сообщение. Этот патерн хорош, что легко можно добавить новое условие обработчика для каких то поступающих данных. Например, первый хендлер расшифровать, второй отформатировать, третий залогировать и т.д. Для этого в каждом Вашем хендлере должен быть список разрешенных для обработки типов сообщений (чтобы каждый хендлер в конвеере знал какие ему нужно типы сообщений обработать) или в самом сообщении должно быть указано какими хендлерами его обрабатывать. Первый вариант получше.
Но у вас в каждом хендлере указано строго для одного типа. Поэтому можно было обойтись обычным свитчом.
public BaseHandler HandlerFactory(messageType)
	switch(messageType)
	{
		case ChatMessage.Type:
			retun new ChatHandler();
			break;
		case AuthMessage.Type:
			retun new AuthHandler();
			break;
		...
	}
}

var handler = Successor.HandlerFactory(messageType);
handler.Handle(socket, webSocketMessage, server);

Ну или на крайняк без этого фора
var handler = Handlers.FirstOdefault(x => x.CanHandle(socket, messageType, server));
if(handle != null)
	handler.Handle(socket, webSocketMessage, server);

Или сделайте из списка хендлеров дикшинари
Handlers[messageType].Handle(socket, webSocketMessage, server);

Лично я бы использовал фабрику, только в данном случае без лишних созданий экземпляров, это выглядит проще и понятнее, добавить в нее еще один хендлер - это две строки кода.
Если Вас смущает статический класс, сделайте синглтон. По факту фабрики обычно и делают синглтонами.
Если везде смущает три параметра в конструкторе (socket, messageType, server), оберните их в фасад
class HandlerParam
{
	public IWebSocketConnection socket { get; set; }
	public string webSocketMessage { get; set; }
	public IServer server { get; set; }
}

var handlerParam = new HandlerParam
{
	...
}

handler.Handle(handlerParam);
Ответ написан
Комментировать
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы