Я действительно разрабатываю многопоточный игровой сервер на С#. Я пытаюсь создать его как можно более проверяемым с помощью блока.
На данный момент шаблон, который я начал использовать, похож на что-то вроде этого:
Определяется класс Сервер:
OnStart Server управляет ConnnectionManager:
Класс ConnectionsPool:
Класс ClientConnection:
Схема должна выглядеть так:
Сервер → ConnectionManager → ConnectionsPool → ClientConnection → SocketHandler
Проблема:
Когда я нахожусь внутри SocketHandler, как я могу повлиять на другого игрока? (например, игрок A поражает игрока B: мне нужно получить экземпляр игрока B внутри ConnectionsPool и обновить его свойство HP) или даже обновить сам сервер (например, вызвать метод shutdown в классе Server)
Я предполагаю, что у вас есть 3 варианта:
Цель состоит в том, чтобы сделать все это единым тестируемым, используя лучшие практики, сохраняя при этом простой подход.
Мне было предложено ввести делегат для детей, который аналогичен 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);