Шаблон общения для дочерних объектов с родителями

2

Я действительно разрабатываю многопоточный игровой сервер на С#. Я пытаюсь создать его как можно более проверяемым с помощью блока.

На данный момент шаблон, который я начал использовать, похож на что-то вроде этого:

Определяется класс Сервер:

  • Служит для запуска и выключения сервера.

OnStart Server управляет ConnnectionManager:

  • Содержит экземпляр класса ConnectionPool
  • Служит для приема асинхронных подключений (через TCPListener.BeginAcceptSocket)
  • Когда сокет принят, создается экземпляр ClientConnection, добавленный в список в ConnectionsPool

Класс ConnectionsPool:

  • Ответственный за добавление и удаление активных подключений к пулу, управляемый как список

Класс ClientConnection:

  • Ответственный за получение данных через Socket.BeginReceive
  • Когда все получено, создается экземпляр SocketHandler (в основном анализируйте содержимое сокета и запускайте действия)

Схема должна выглядеть так:
Сервер → ConnectionManager → ConnectionsPool → ClientConnection → SocketHandler

Проблема:
Когда я нахожусь внутри SocketHandler, как я могу повлиять на другого игрока? (например, игрок A поражает игрока B: мне нужно получить экземпляр игрока B внутри ConnectionsPool и обновить его свойство HP) или даже обновить сам сервер (например, вызвать метод shutdown в классе Server)

Я предполагаю, что у вас есть 3 варианта:

  • Преобразование всех важных классов (Server + ConnectionsPool) в статические классы > недостаток: не может быть проверен модулем
  • Преобразование всех важных классов в классы Singleton > недостаток: трудно проверить
  • Внедрить внутри дочернего конструктора экземпляры важных классов > Недостаток: менее читаемый код, поскольку передается много информации.

Цель состоит в том, чтобы сделать все это единым тестируемым, используя лучшие практики, сохраняя при этом простой подход.

Мне было предложено ввести делегат для детей, который аналогичен 3-му методу, но я не уверен в том, что он будет тестировать это и реальный эффект, так как все будет многопоточным, конечно.

Какой был бы лучший выбор архитектуры здесь?

Теги:
unit-testing
design-patterns
c#-4.0

1 ответ

3
Лучший ответ

Тема, которую я нахожу, заключается в том, что сетевой код на самом деле не является единым тестируемым и должен быть изолирован от кода, который вы хотите протестировать (например, для объекта Player). Мне кажется, что вы тесно связали Player с ClientConnection, что может сделать его менее проверяемым. Я также думаю, что соединение игрока с его соединением, вероятно, нарушает SRP, так как они имеют очень отчетливые обязанности.

Я написал систему, которая кажется похожей на то, что вы хотите добиться, и я сделал это, связав игрока с соединением только через делегаты и интерфейсы. У меня есть отдельная библиотека классов World и Server, а третий проект - приложение, которое ссылается на два, и создает экземпляр сервера и мира. Мой объект Player находится в World и Connection на сервере - эти два проекта не ссылаются друг на друга, поэтому вы можете рассматривать Player без каких-либо условий работы в сети.

Метод, который я использую, состоит в том, чтобы сделать реферат SocketHandler абстрактным и реализовать конкретный обработчик в проекте Application. Обработчик имеет ссылку на объект world, который может быть передан ему при создании. (Я делаю это с помощью классов factory - например, ClientConnectionFactory (в приложении), где Сервер знает только абстрактные соединения и фабрики.)

public class ClientConnectionFactory : IConnectionFactory
{
    private readonly World world;

    public ClientConnectionFactory(World world) {
        this.world = world;
    }

    Connection IConnectionFactory.Create(Socket socket) {
        return new ClientConnection(socket, world);
    }
}

Затем ClientConnection может перенаправить ссылку World на обработчик, а также любую информацию, которую обработчик требует о самом соединении, например, в частности, методы отправки. Здесь я просто передаю объект Connection самому обработчику.

public ClientConnection(Socket socket, World world)
    : base(socket) {
    this.handler = new ClientHandler(this, world);
    ...
}

Большая часть координации между логикой игры и сетью затем содержится внутри ClientHandler и, возможно, с любыми другими объектами сестры, которые она ссылается.

class ClientHandler : SocketHandler
{
    private readonly Connection connection;
    private readonly World world;

    public ClientHandler(Connection connection, World world) {
        this.connection = connection;
        this.world = world;
        ...
    }

    //overriden method from SocketHandler
    public override void HandleMessage(Byte[] data) {
        ...
    }
}

Остальное включает ClientHandler, создающий объект Player, назначение делегатов или интерфейсов для обновления действий, а затем добавление игрока в пул игроков в World. Вначале я сделал это со списком событий в Player, который я подписываюсь с помощью методов в ClientHandler (который знает о соединении), но оказалось, что в объекте Player были десятки событий, которые стали бесполезными для обслуживания.

В качестве опции я пошел использовать абстрактные уведомления в проекте World на игроке. Например, для перемещения у меня был бы объект IMovementNotifier в Player. В приложении я создал бы ClientNotifier, который реализует этот интерфейс и выполняет соответствующую передачу данных клиенту. ClientHandler создаст ClientNotifier и передаст ему объект Player.

class ClientNotifier : IMovementNotifier //, ..., + bunch of other notifiers
{
    private readonly Connection connection;

    public ClientHandler(Connection connection) {
        this.connection = connection;
    }

    void IMovementNotifier.Move(Player player, Location destination) {
        ...
        connection.Send(new MoveMessage(...));
    }
}

ClientHandler ctor можно изменить, чтобы создать экземпляр этого уведомителя.

public ClientHandler(Connection connection, World world) {
    this.connection = connection;
    this.world = world;
    ...
    var notifier = new ClientNotifier(this);
    this.player = new Player(notifier);
    this.world.Players.Add(player);
}

Таким образом, результирующая система - это та, где ClientHandler отвечает за все входящие сообщения и события, а ClientNotifier обрабатывает все исходящие сообщения. Эти два класса довольно сложно проверить, поскольку они содержат много другого дерьма. В любом случае, сеть не может быть полностью протестирована, но объект Connection в этих двух классах может быть изделен. Всемирная библиотека полностью тестируется единым целым без какого-либо рассмотрения сетевых компонентов, что в любом случае я хотел бы получить в моем дизайне.

Это большая система, и я не упомянул здесь много, но надеюсь, что это может дать вам несколько намеков. Спросите меня, хотите ли вы получить более подробную информацию об этом.

Если бы я мог дать больше советов, было бы избегать статических или синглетов - они вернутся, чтобы укусить вас. Поощряйте усложнение, если вам нужно в качестве альтернативы, но документ, где вам нужно. Система, которая у меня может показаться сложной для сопровождающего, но хорошо документирована. Пользователю он особенно прост в использовании. Главная по существу

var world = new World();
var connectionFactory = new ClientConnectionFactory(world);
var server = new Server(settings.LocalIP, settings.LocalPort, connectionFactory);
  • 0
    Отличный ответ! Я посмотрю на это сегодня вечером и посмотрю, как это повлияет на текущий код.
  • 0
    Я посмотрел подробно ваш пост, но у меня есть некоторые колебания здесь. Существует World DLL с классом World и классом Player. Существует сетевая DLL с классом ClientConnection (и обработчиками). Как вы можете внедрить экземпляр класса World в ClientConnection, если у Networking нет ссылок на World?
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню