Разделение Клиент-Сервер и пакетная система ElegantNetworking

Разделение Клиент-Сервер и пакетная система ElegantNetworking

Версия(и) Minecraft
1.7.10, 1.12, 1.15, 1.16, 1.17


6175.jpg

Разделение Клиент-Сервер и пакетная система

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

Оглавление:
  1. Разделение Клиент-Сервер
  2. Коммуникация ванильный Клиент ←→ Сервер
  3. Безопасность при обработке пакетов с клиента
  4. Коммуникация Клиент ←→ Сервер с помощью SimpleNetworkWrapper
  5. Коммуникация Клиент ←→ Сервер с помощью библиотеки ElegantNetworking

Разделение Клиент-Сервер
Когда мы говорим "клиент" или "сервер", интуитивно понятно, о какой части игры мы говорим. В конце концов, клиент - это то, с чем пользователь взаимодействует, а сервер - это куда он подключается к многопользовательской игре. Легко, правда?
Оказывается, существует двусмысленность даже с двумя такими терминами. Поясним четыре возможных значения "клиент" и "сервер" :
  • Физический клиент - это программа, которая выполняется всякий раз при запуске Minecraft из лаунчера. Все потоки, процессы и службы, которые выполняются во время графического, интерактивного времени жизни игры, являются частью физического клиента.
  • Физический сервер - часто известный как выделенный сервер (Dedicated server), это программа, которая выполняется всякий раз, когда вы запускаете какой-либо файл minecraft_server.jar, который не создает графический интерфейс.
  • Логический сервер - это то, что запускает логику игры : спавн мобов, погода, обновление инвентарей, здоровье, AI и все другие игровые механики. Логический сервер присутствует на физическом сервере, но также может работать внутри физического клиента вместе с логическим клиентом в одиночном мире (Integrated server). Логический сервер всегда работает в потоке с именем Server Thread.
  • Логический клиент - это то, что принимает ввод от игрока и передает его на логический сервер. Кроме того, он получает информацию с логического сервера и делает ее графически доступной игроку. Логический клиент работает в Client Thread, хотя часто другие потоки порождаются для для обработки таких вещей, как обработка звуков и рендер чанков.
Диаграммы ниже иллюстрируют эти отношения
physical-logical-mc-sides.png

physical-logical-mc-sides (1).png

Далее мы будем рассматривать только логические клиент и сервер. Код майнкрафта может быть разделен на две "стороны" - Клиентскую и Серверную.
  • Серверная сторона отвечает за поддержание оригинала мира - обновление блоков и сущностей основываясь на пакетах, полученных от клиентов, отправление обновленной информации всем клиентам.
  • Клиентская сторона в первую очередь отвечает за обработку входной информации (нажатие клавиш, клики мыши) и отрисовку экрана.
Одновременно существует только один Сервер и множество Клиентов, подключенных к нему. Даже в одиночной игре серверный и клиентский код работает одновременно (в разных потоках).

Некоторые части кода используются и клиентом, и сервером, например методы Блоков. Если вы пишете код, который может быть вызван либо Клиентом, либо Сервером, как можно определить с какой стороны он вызывается?
Вашему методу почти всегда доступен объект World, или объект, содержащий в себе ссылку на него (обычно называется worldObj). Если это так, то
  • Если World.isRemote() == true , то это сторона Клиента
  • Если World.isRemote() == false, то это сторона Сервера
Эта логическая проверка - самый простой способ проверки сторон. Запрос этого поля в объекте World устанавливает логическую сторону, к которой принадлежит мир. То есть, если это поле true, мир в настоящее время работает на логическом клиенте. Если поле false, мир работает на логическом сервере. Из этого следует, что физический сервер всегда будет содержать false в этом поле, но мы не можем утверждать, что false подразумевает физический сервер, поскольку это поле также может быть ложным для логического сервера внутри физического клиента (другими словами, мира с одним игроком).
Используйте эту проверку, когда вам нужно определить, следует ли запускать логику игры или другую механику. Например, если вы хотите повредить игрока каждый раз, когда он нажимает на ваш блок, или ваш механизм превращает землю в алмазы, вы должны сделать это только после того как убедитесь, что работаете с серверной стороной. Применение логики игры к клиенту может привести к десинхронизации (сущностям-призракам, десинхронизированной статистике и тд) в лучшем случае и крашам в худшем.

В других случаях можно использовать FMLcommonHandler.instance().getEffectiveSide()(или EffectiveSide.get() на новых версиях), который угадывает текущую сторону, анализируя поток, в котором вызывается. Этот способ не является эффективным и надежным, поэтому используйте его с осторожностью.

Для определения физической стороны выполнения, используйте FMLCommonHandler.instance().getSide() (или FMLEnvironment.dist на новых версиях). Поскольку физическая сторона определяется при запуске, метод не полагается на угадывание, однако число вариантов использования этого метода ограничено (Пример : выпадение специального предмета при ломании блока, но только если мод запущен на выделенном сервере, а не в одиночной игре).

Основные ошибки
Когда клиент и сервер должны синхронизироваться друг с другом, они обмениваются информацией по сети. Даже когда мы находимся в одиночной игре, клиент и сервер все еще полностью разделены и не имеют доступа к объектом друг друга. Если ваш код, работающий (скажем) на стороне сервера, обратился к объектам, принадлежащим стороне клиента, это приведет к случайным крашам и странному поведению. Это также приведет к крашу вашего кода сразу после установки на выделенный сервер.
Всякий раз, когда вы хотите отправлять информацию с одной логической стороны на другую, вы должны использовать сетевые пакеты. Невероятно заманчиво в одиночной игре напрямую передавать данные с логического сервера на логический клиент, что очень часто непреднамеренно делается через статические поля, но поскольку логический клиент и логический сервер используют одну и ту же JVM в одиночной игре, оба потока, записывающие и считывающие из статический полей, будут вызывать разного рода состояния гонки и классические проблемы, связанные с потоковой обработкой.
Эта ошибка также может быть совершена при попытке обратиться к client-only классам, таким как net.minecraft.client.Minecraft, существующем только на логическом клиенте, из общего кода, выполняющегося на логическом сервере. Код будет работать в одиночной игре, но крашнется на физическом сервере.

Коммуникация ванильный Клиент ←→ Сервер
Ванильная связь между клиентскими и серверными сторонами происходит через пакеты, которые отправляются туда и обратно с помощью NetHandlerPlayClient и NetHandlerPlayServer. Основные этапы показаны на диаграмме ниже.
VanillaClientServer.png
Ключевые моменты
Пакеты - это набор информации (числа, строки, ItemStack'и и др), конвертированный в байты для передачи по сети.
Пакеты, отправленные с Клиента на Сервер, начинаются с C, например C07PacketPlayerDigging.
Пакеты, отправленные с сервера на клиент, начинаются с S, например S06PacketUpdateHealth.
Можно создать ванильные пакеты и отправлять их с помощью NetHandler, однако это почти никогда не требуется. Если вы вызовете правильные существующие методы, они отправят вам пакеты (например, ServerConfigurationManager.sendChatMsg() вместо S02PacketChat).

Безопасность при обработке пакетов с клиента
При обработке пакетов с клиента не пренебрегайте дополнительными проверками полученных данных! Клиент может попытаться отправить специально созданные пакеты с неожиданными для вас данными, чтобы вызвать лаги, дюпы и краши сервера.
Основной проблемой является уязвимость к произвольной генерации чанков. Обычно это происходит, когда сервер доверяет позиции блока, указанной в полученном пакете, чтобы взаимодействовать с блоком или тайлом на этом месте. При попытке доступа к блокам в не загруженных участках мира, сервер либо загрузит этот участок с диска, либо сгенерирует его. Это может привести к катастрофическому падению производительности и захламлению места на диске, совершенно не оставляя следов.
Чтобы избежать этой проблемы, общим правилом является ограничение доступа блокам и сущностям, удаленных от него на определенное расстояние - проверка квадрата расстояния игрока до блока (рекомендовано), либо доступ только к загруженным участкам мира - когда world.isBlockLoaded(pos) истинно.
Главное помнить, что если ваш пакет отсылается с клиента, и в зависимости его содержимого сервер производит какие-то действия, необходимо делать проверки на корректность полученных данных.

Коммуникация Клиент ←→ Сервер с помощью SimpleNetworkWrapper
Forge предоставляет SimpleNetworkWrapper для создания и обмена пакетами.
Инструкции, как использовать этот метод вы найдете здесь:
Статья из англоязычного источника
Статья с форума

Коммуникация Клиент ←→ Сервер с помощью библиотеки ElegantNetworking
Во многих случаях вам не нужно создавать собственные пакеты, чтобы синхронизировать код клиента и сервера. Код ванили и Forge уже синхронизируют некоторые вещи автоматически :
  • Создание, удаление, перемещение, здоровье, и другие действия для сущностей.
  • Удаление и размещение блоков.
  • Добавление переменных DataWatcher в вашу сущность.
  • Контейнеры (инвентарь, печь и тд).
Хотя ванильный майнкрафт использует NetHandlerPlayClient и NetHandlerPlayServer для отправки пакетов, вы не должны использовать их в модах. Для этой цели рекомендуется использовать библиотеку от нашего земляка - ElegantNetworking.
Инструкцию, как добавить библиотеку себе в проект, найдете в его статье.

Каждый элегантный пакет представляет из себя отдельный класс:
EN packet structure.png
  • Аннотация @ElegantPacket обязательна, она сообщает фреймворку о существовании пакета.
  • Имя пакета, оно же имя класса - позволяет отличить один пакет от другого.
  • Базовый интерфейс может быть ClientToServerPacket или ServerToClientPacket, он определяет возможное направление отправки пакета - с клиента на сервер или с сервера на клиент.
  • Передаваемые данные - поля в классе пакета, могут быть примитивами, дата-классами, коллекциями, массивами, енумами, интерфейсами, подробнее ниже.
  • Обработчик пакета - абстрактный метод из базового интерфейса, который вы должны реализовать в пакете. Он вызывается при получении пакета и содержит логику обработки конкретного пакета.
Для отправки пакета создайте его экземпляр и вызовите у него один из следующих методов
ClientToServerPacket#
sendToServer() - с клиента на сервер​
ServerToClientPacket#
sendToPlayer(player) - с сервера на клиент игрока​
sendToClients() - с сервера на все клиенты​
sendPacketToAllAround(world, x,y,z, range) - всем игрокам в радиусе вокруг точки​
sendToDimension(world) - всем игрокам в измерении​
sendToChunk(world, chunkX, chunkZ) - всем игрокам, которые находятся в пределах view distance вокруг чанка​

Генерация, передача и получение сообщения (от клиента к серверу) проиллюстрированы на диаграмме ниже. Отправка сообщения от сервера к клиенту осуществляется по той же схеме.
principle of networking.png
На диаграмме можно видеть, что по сети пакеты передаются в виде каких-то ByteBuf. Но ведь мы не писали никакого кода работы с ByteBuf! Обычно разработчик действительно должен написать логику преобразования своих структур данных в ByteBuf и назад. Однако, ElegantNetworking делает это за вас!
Чтобы авто-сериализация работала, стоит учесть некоторые детали:
  • Дата-классы - это просто классы с полями, конструкторами, геттерами и сеттерами
  • Каждое поле пакета должно быть доступно для чтения в пределах package класса вашего пакета непосредственно или через геттер
  • Каждое изменяемое поле пакета должно быть доступно для присваивания непосредственно или через сеттер
  • Для неизменяемых полей должен быть конструктор с аргументами соответственно полям. Если неизменяемых полей нет, то должен быть конструктор без аргументов. Порядок аргументов должен совпадать с порядком объявления полей.
  • Поля, которые не нужно сериализовать, можно пометить модификатором transient
Пример:
Java:
public class Some {
    public final String str1;
    final String str2; //private-package
    private final String str3; //Приватное поле, но имеет геттер

    public int num1;
    private int num2; //Приватное поле, но имеет геттер и сеттер

    private boolean available; // Приватное поле, но имеет геттер и сеттер

    transient boolean service; // Какое-то служебное поле, которое не нужно сериализовывать

    public Some(String str1, String str2, String str3) { // Конструктор для всех финальных полей
       this.str1 = str1;
       this.str2 = str2;
       this.str3 = str3;
    }

    String getStr3() {
        return str3;
    }

    int getNum2() {
        return num2;
    }

    void setNum2(int v) {
        num2 = v;
    }

    boolean isAvailable() { // Для boolean можно использовать приставку is вместо get
        return available;
    }

    void setAvailable(boolean v) {
        available = v;
    }
}

Вот список того, что библиотека сериализует самостоятельно:
  • Примитивы, массивы, енумы, ItemStack, BlockPos, NBT тэги, ResourceLocation, UUID
  • Коллекции
    • Поддерживаются все стандартные коллекции и часто используемые коллекции из доступных в Forge библиотек
    • Кастомные коллекции пока не поддерживаются
  • Регистрируемые объекты
    • Item, Block, Fluid, Potion и прочие. Конкретный список зависит от версии игры, его можно посмотреть в hohserg.elegant.networking.impl.RegistrableSingletonSerializer.RegistryHandler
  • Абстрактные классы, интерфейсы, не финальные классы
    • Каждый такой абстрактный тип должен иметь хотя бы одну не абстрактную реализацию
    • Если реализаций много - будет как ADT сумма.
Java:
interface Location {

}

public class FixedPos implement Location {
    final BlockPos pos;
    public FixedPos(BlockPos pos) {
        this.pos=pos;
    }
}

public class VolatilePos implement Location {
    final UUID playerId;
    public VolatilePos(UUID playerId) {
        this.playerId=playerId;
    }
}
  • Эти правила применяются рекурсивно
    • Элементы коллекций должны им соответствовать
    • Типы полей дата-классов тоже
У аннотации @ElegantPacket есть необязательный параметр [B]channel[/B], определяющий имя канала, в котором будет отправляться пакет. По умолчанию параметр равен вашему modid и вам почти никогда не нужно о нем беспокоиться.

Благодарности :
@hohserg за доработку статьи и создание либы ElegantNetworking

Источники :
Minecraft Modding: Networking
Forge Documentation
Автор
GlassSpirit
Просмотры
7,521
Первый выпуск
Обновление
Оценка
4.80 звёзд 5 оценок

Другие ресурсы пользователя GlassSpirit

Последние обновления

  1. Добавление ElegantNetworking и актуализация информации

    Актуализирована информация, поправлены ошибки в оформлении. Добавлена информация про библиотеку...

Последние рецензии

Отличный гайд! Один из фундаментальных, без знаний которого затруднительно делать сложные фичи
5 т. к. всё написано понятным и читаемым языком. Думаю, полезно будет для людей которые хотят не только копипастить, но и понимать принцып работы скопированного. Да и вообще, конструктивных туторов сейчас днём с огнём не сыщешь.
Хороший и полезный и нужный гайд
Очень много полезной инфы, и ещё всё распределено
Отлично! Но четыре за то что не полностью свое.
Сверху