- Версия(и) Minecraft
- 1.7.10, 1.12, 1.15, 1.16, 1.17
Разделение Клиент-Сервер и пакетная система
В этой статье описаны базовые принципы коммуникации между клиентом и сервером Minecraft, а также предложен удобный фреймворк для создания своих пакетов.Разделение Клиент-Сервер и пакетная система
Оглавление:
- Разделение Клиент-Сервер
- Коммуникация ванильный Клиент ←→ Сервер
- Безопасность при обработке пакетов с клиента
- Коммуникация Клиент ←→ Сервер с помощью SimpleNetworkWrapper
- Коммуникация Клиент ←→ Сервер с помощью библиотеки ElegantNetworking
Разделение Клиент-Сервер
Когда мы говорим "клиент" или "сервер", интуитивно понятно, о какой части игры мы говорим. В конце концов, клиент - это то, с чем пользователь взаимодействует, а сервер - это куда он подключается к многопользовательской игре. Легко, правда?Оказывается, существует двусмысленность даже с двумя такими терминами. Поясним четыре возможных значения "клиент" и "сервер" :
- Физический клиент - это программа, которая выполняется всякий раз при запуске Minecraft из лаунчера. Все потоки, процессы и службы, которые выполняются во время графического, интерактивного времени жизни игры, являются частью физического клиента.
- Физический сервер - часто известный как выделенный сервер (Dedicated server), это программа, которая выполняется всякий раз, когда вы запускаете какой-либо файл minecraft_server.jar, который не создает графический интерфейс.
- Логический сервер - это то, что запускает логику игры : спавн мобов, погода, обновление инвентарей, здоровье, AI и все другие игровые механики. Логический сервер присутствует на физическом сервере, но также может работать внутри физического клиента вместе с логическим клиентом в одиночном мире (Integrated server). Логический сервер всегда работает в потоке с именем Server Thread.
- Логический клиент - это то, что принимает ввод от игрока и передает его на логический сервер. Кроме того, он получает информацию с логического сервера и делает ее графически доступной игроку. Логический клиент работает в Client Thread, хотя часто другие потоки порождаются для для обработки таких вещей, как обработка звуков и рендер чанков.
Далее мы будем рассматривать только логические клиент и сервер. Код майнкрафта может быть разделен на две "стороны" - Клиентскую и Серверную.
- Серверная сторона отвечает за поддержание оригинала мира - обновление блоков и сущностей основываясь на пакетах, полученных от клиентов, отправление обновленной информации всем клиентам.
- Клиентская сторона в первую очередь отвечает за обработку входной информации (нажатие клавиш, клики мыши) и отрисовку экрана.
Некоторые части кода используются и клиентом, и сервером, например методы Блоков. Если вы пишете код, который может быть вызван либо Клиентом, либо Сервером, как можно определить с какой стороны он вызывается?
Вашему методу почти всегда доступен объект World, или объект, содержащий в себе ссылку на него (обычно называется worldObj). Если это так, то
- Если World.isRemote() == true , то это сторона Клиента
- Если World.isRemote() == false, то это сторона Сервера
Используйте эту проверку, когда вам нужно определить, следует ли запускать логику игры или другую механику. Например, если вы хотите повредить игрока каждый раз, когда он нажимает на ваш блок, или ваш механизм превращает землю в алмазы, вы должны сделать это только после того как убедитесь, что работаете с серверной стороной. Применение логики игры к клиенту может привести к десинхронизации (сущностям-призракам, десинхронизированной статистике и тд) в лучшем случае и крашам в худшем.
В других случаях можно использовать
FMLcommonHandler.instance().getEffectiveSide()
(или EffectiveSide.get()
на новых версиях), который угадывает текущую сторону, анализируя поток, в котором вызывается. Этот способ не является эффективным и надежным, поэтому используйте его с осторожностью.Для определения физической стороны выполнения, используйте
FMLCommonHandler.instance().getSide()
(или FMLEnvironment.dist
на новых версиях). Поскольку физическая сторона определяется при запуске, метод не полагается на угадывание, однако число вариантов использования этого метода ограничено (Пример : выпадение специального предмета при ломании блока, но только если мод запущен на выделенном сервере, а не в одиночной игре).Основные ошибки
Когда клиент и сервер должны синхронизироваться друг с другом, они обмениваются информацией по сети. Даже когда мы находимся в одиночной игре, клиент и сервер все еще полностью разделены и не имеют доступа к объектом друг друга. Если ваш код, работающий (скажем) на стороне сервера, обратился к объектам, принадлежащим стороне клиента, это приведет к случайным крашам и странному поведению. Это также приведет к крашу вашего кода сразу после установки на выделенный сервер.
Всякий раз, когда вы хотите отправлять информацию с одной логической стороны на другую, вы должны использовать сетевые пакеты. Невероятно заманчиво в одиночной игре напрямую передавать данные с логического сервера на логический клиент, что очень часто непреднамеренно делается через статические поля, но поскольку логический клиент и логический сервер используют одну и ту же JVM в одиночной игре, оба потока, записывающие и считывающие из статический полей, будут вызывать разного рода состояния гонки и классические проблемы, связанные с потоковой обработкой.
Эта ошибка также может быть совершена при попытке обратиться к client-only классам, таким как
net.minecraft.client.Minecraft
, существующем только на логическом клиенте, из общего кода, выполняющегося на логическом сервере. Код будет работать в одиночной игре, но крашнется на физическом сервере.Коммуникация ванильный Клиент ←→ Сервер
Ванильная связь между клиентскими и серверными сторонами происходит через пакеты, которые отправляются туда и обратно с помощью NetHandlerPlayClient
и NetHandlerPlayServer
. Основные этапы показаны на диаграмме ниже.Пакеты - это набор информации (числа, строки, ItemStack'и и др), конвертированный в байты для передачи по сети.
Пакеты, отправленные с Клиента на Сервер, начинаются с C, например
C07PacketPlayerDigging
.Пакеты, отправленные с сервера на клиент, начинаются с S, например
S06PacketUpdateHealth
.Можно создать ванильные пакеты и отправлять их с помощью NetHandler, однако это почти никогда не требуется. Если вы вызовете правильные существующие методы, они отправят вам пакеты (например,
ServerConfigurationManager.sendChatMsg()
вместо S02PacketChat
).Безопасность при обработке пакетов с клиента
При обработке пакетов с клиента не пренебрегайте дополнительными проверками полученных данных! Клиент может попытаться отправить специально созданные пакеты с неожиданными для вас данными, чтобы вызвать лаги, дюпы и краши сервера.Основной проблемой является уязвимость к произвольной генерации чанков. Обычно это происходит, когда сервер доверяет позиции блока, указанной в полученном пакете, чтобы взаимодействовать с блоком или тайлом на этом месте. При попытке доступа к блокам в не загруженных участках мира, сервер либо загрузит этот участок с диска, либо сгенерирует его. Это может привести к катастрофическому падению производительности и захламлению места на диске, совершенно не оставляя следов.
Чтобы избежать этой проблемы, общим правилом является ограничение доступа блокам и сущностям, удаленных от него на определенное расстояние - проверка квадрата расстояния игрока до блока (рекомендовано), либо доступ только к загруженным участкам мира - когда world.isBlockLoaded(pos) истинно.
Главное помнить, что если ваш пакет отсылается с клиента, и в зависимости его содержимого сервер производит какие-то действия, необходимо делать проверки на корректность полученных данных.
Коммуникация Клиент ←→ Сервер с помощью SimpleNetworkWrapper
Forge предоставляет SimpleNetworkWrapper для создания и обмена пакетами.Инструкции, как использовать этот метод вы найдете здесь:
Статья из англоязычного источника
Статья с форума
Коммуникация Клиент ←→ Сервер с помощью библиотеки ElegantNetworking
Во многих случаях вам не нужно создавать собственные пакеты, чтобы синхронизировать код клиента и сервера. Код ванили и Forge уже синхронизируют некоторые вещи автоматически :- Создание, удаление, перемещение, здоровье, и другие действия для сущностей.
- Удаление и размещение блоков.
- Добавление переменных DataWatcher в вашу сущность.
- Контейнеры (инвентарь, печь и тд).
NetHandlerPlayClient
и NetHandlerPlayServer
для отправки пакетов, вы не должны использовать их в модах. Для этой цели рекомендуется использовать библиотеку от нашего земляка - ElegantNetworking.Инструкцию, как добавить библиотеку себе в проект, найдете в его статье.
Каждый элегантный пакет представляет из себя отдельный класс:
- Аннотация @ElegantPacket обязательна, она сообщает фреймворку о существовании пакета.
- Имя пакета, оно же имя класса - позволяет отличить один пакет от другого.
- Базовый интерфейс может быть
ClientToServerPacket
илиServerToClientPacket
, он определяет возможное направление отправки пакета - с клиента на сервер или с сервера на клиент. - Передаваемые данные - поля в классе пакета, могут быть примитивами, дата-классами, коллекциями, массивами, енумами, интерфейсами, подробнее ниже.
- Обработчик пакета - абстрактный метод из базового интерфейса, который вы должны реализовать в пакете. Он вызывается при получении пакета и содержит логику обработки конкретного пакета.
ClientToServerPacket#
sendToServer()
- с клиента на серверServerToClientPacket#
sendToPlayer(player)
- с сервера на клиент игрокаsendToClients()
- с сервера на все клиентыsendPacketToAllAround(world, x,y,z, range)
- всем игрокам в радиусе вокруг точкиsendToDimension(world)
- всем игрокам в измеренииsendToChunk(world, chunkX, chunkZ)
- всем игрокам, которые находятся в пределах view distance вокруг чанкаГенерация, передача и получение сообщения (от клиента к серверу) проиллюстрированы на диаграмме ниже. Отправка сообщения от сервера к клиенту осуществляется по той же схеме.
Чтобы авто-сериализация работала, стоит учесть некоторые детали:
- Дата-классы - это просто классы с полями, конструкторами, геттерами и сеттерами
- Каждое поле пакета должно быть доступно для чтения в пределах 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
- Item, Block, Fluid, Potion и прочие. Конкретный список зависит от версии игры, его можно посмотреть в
- Абстрактные классы, интерфейсы, не финальные классы
- Каждый такой абстрактный тип должен иметь хотя бы одну не абстрактную реализацию
- Если реализаций много - будет как 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