Иконка ресурса

Перевод SimpleImpl - Пакетная Система

Версия(и) Minecraft
1.7-1.16
Источник
https://mcforge.readthedocs.io/en/1.12.x/networking/simpleimpl/
SimpleImpl
SimpleImpl - это имя, данное этой пакетной системе, которая вращается вокруг класса SimpleNetworkWrapper.
Использование этой системы - это, безусловно, самый простой способ пересылки пользовательских данных между клиентами и сервером.

Начало
Мы рассмотрим создание пакетов на всех версиях, кроме 1.14. Сначала вам нужно создать объект SimpleNetworkWrapper. Я рекомендую сделать это в отдельном классе, возможно, что-то вроде ModidPacketHandler.

Создайте свой SimpleNetworkWrapper как статическое поле в этом классе, например:
ModidPacketHandler.java:
public static final SimpleNetworkWrapper INSTANCE = NetworkRegistry.INSTANCE.newSimpleChannel("mymodid");
  • mymodid - это короткий идентификатор для вашего пакетного канала, обычно это просто идентификатор вашего мода. Он не должен быть слишком длинным.
ModidPacketHandler.java:
private static final String PROTOCOL_VERSION = "1";

public static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel(
    new ResourceLocation("mymodid", "main"),
    () -> PROTOCOL_VERSION,
    PROTOCOL_VERSION::equals,
    PROTOCOL_VERSION::equals
);
  • main - Supplier<String> - возвращающий текущую версию сетевого протокола.
  • PROTOCOL_VERSION - Predicate<String> - проверяющий, совместима ли версия протокола входящего соединения сети с клиентом или сервером соответственно. Здесь мы просто сравниваем напрямую с полем PROTOCOL_VERSION, что означает, что PROTOCOL_VERSION клиента и сервера всегда должны совпадать, иначе FML откажется от входа в систему.

Создание пакетов
IMessage:
Пакет определяется с помощью интерфейса IMessage. Этот интерфейс определяет 2 метода: toBytes и fromBytes. Эти методы, соответственно, записывают и считывают данные в вашем пакете в объект ByteBuf и из него.

ByteBuf является объектом, используемым для хранения потока (массива) байтов, отправляемых по сети.

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

MyMessage.java:
public class MyMessage implements IMessage
{
    // Конструктор по умолчанию. Всегда требуется
    public MyMessage(){}

    int toSend;

    public MyMessage(int toSend)
    {
      this.toSend = toSend;
    }

    @Override public void toBytes(ByteBuf buf)
    {
      // Записывает int в buf
      buf.writeInt(toSend);
    }

    @Override public void fromBytes(ByteBuf buf)
    {
      // Читает int обратно из buf.
      // Обратите внимание: если у вас несколько значений,
      // вы должны читать их в том же порядке, в котором написали.
      toSend = buf.readInt();
    }
}

IMessageHandler:
Теперь, как мы можем использовать этот пакет?

Сначала у нас должен быть класс, который может обрабатывать этот пакет. Он создается с помощью интерфейса IMessageHandler.

Допустим, мы хотим использовать это целое число, которое мы отправили, чтобы дать игроку столько-то алмазов на сервере. Сделаем этот обработчик:
MyMessageHandler.java:
// Параметры IMessageHandler: <REQ, REPLY>
// Первый параметр - это пакет, который вы получаете, а второй - это пакет, который вы возвращаете.
// Возвращенный пакет можно использовать как "ответ" на отправленный пакет.
public class MyMessageHandler implements IMessageHandler<MyMessage, IMessage>
{
    // Обратите внимание, что требуется конструктор по умолчанию, но в этом случае он определяется неявно.

    @Override public IMessage onMessage(MyMessage message, MessageContext ctx)
    {
        // Игрок, от которого на сервер был отправлен пакет
        EntityPlayerMP serverPlayer = ctx.getServerHandler().player;

        // Значение, которое было отправлено
        int amount = message.toSend;

        // Выполнение действия в основном потоке сервера, добавив его как запланированную задачу
        serverPlayer.getServerWorld().addScheduledTask(() -> {
          serverPlayer.inventory.addItemStackToInventory(new ItemStack(Items.DIAMOND, amount));
        });

        // Если от пакета не последовал ответ
        return null;
    }
}

Рекомендуется (но не обязательно), чтобы этот класс был внутренним классом вашего класса MyMessage. В таком случае - класс также должен быть объявлен статическим.

Всё аналогично 1.12.2.

Средство проверки совместимости (для 1.15 и выше)
Если ваш мод не требует, чтобы другая сторона имела определенный сетевой канал или вообще была экземпляром Forge, вам следует позаботиться о том, чтобы правильно определить средства проверки совместимости версий (параметры Predicate<String> ) для обработки дополнительных «мета-версий» (определенных в NetworkRegistry), которые могут быть получены средством проверки версий:
  • ABSENT - если этот канал отсутствует на другой конечной точке. Обратите внимание, что в этом случае конечная точка по-прежнему является конечной точкой Forge и может иметь другие моды.
  • ACCEPTVANILLA - если конечная точка является исходной (или не Forge) конечной точкой.
Возвращение false для обоих означает, что этот канал должен присутствовать на другой конечной точке. Если вы просто скопируете приведенный выше код, он сделает вот что. Обратите внимание, что эти значения также используются во время проверки совместимости ping списка, которая отвечает за отображение зеленой галочки / красного креста на экране выбора многопользовательского сервера.

Регистрация пакетов
Итак, теперь у нас есть пакет и обработчик для этого пакета. Но для работы SimpleNetworkWrapper нужна еще одна вещь!

Для использования пакета - пакет должен быть зарегистрирован с помощью дискриминатора, который представляет уникальный идентификатор для каждого канала для пакета. (Мы рекомендуем использовать статическую переменную для хранения идентификатора, а затем вызывать registerMessage с помощью id++. Это гарантирует 100% уникальность идентификаторов).

Для регистрации пакета, вызовите:
Main.java:
ModidPacketHandler.INSTANCE.registerMessage(MyMessageHandler.class, MyMessage.class, 0, Side.SERVER);
  • INSTANCE - это класс SimpleNetworkWrapper, который мы определили ранее.
Это довольно сложный метод, поэтому как насчёт разобрать его:
  • messageHandler - класс, обрабатывающий ваш пакет. Этот класс всегда должен иметь конструктор по умолчанию и должен иметь REQ с привязкой к типу, который соответствует следующему аргументу.
  • requestMessageType - является фактическим классом пакета. Этот класс также должен иметь конструктор по умолчанию и соответствовать привязке типа REQ предыдущего параметра.
  • Side.SERVER - это сторона, на которую будет получен ваш пакет (сервер/клиент). Если вы планируете отправить пакет в обе стороны, его необходимо зарегистрировать дважды, по одному на каждую сторону. (Дискриминаторы между сторонами могут быть одинаковыми, но это не обязательно)
Для версий 1.13 и выше метод INSTANCE.registerMessage отличается. Он принимает следующие параметры:
  1. Уникальный идентификатор Дескриминатора. ( Тоже, что и в 1.12.2 )
  2. Этот параметр - фактический MSG класса пакета.
  3. BiConsumer<MSG, PacketBuffer> - отвечающий за кодирование сообщения в предоставленном PacketBuffer.
  4. Function<PacketBuffer, MSG> - отвечающий за декодирование сообщения из предоставленного PacketBuffer.
  5. BiConsumer<MSG, Supplier<NetworkEvent.Context>> - отвечает за обработку самого сообщения.
Последние три параметра могут быть ссылками на статические методы или методы экземпляра в Java. Помните, что метод экземпляра MSG.encode(PacketBuffer) по-прежнему удовлетворяет BiConsumer<MSG, PacketBuffer>, MSG просто становится неявным первым аргументом.

Начиная с Minecraft 1.8 пакеты по умолчанию обрабатываются в сетевом потоке.

Это означает, что ваш IMessageHandler не может напрямую взаимодействовать с большинством игровых объектов. Minecraft предоставляет удобный способ заставить ваш код выполняться в основном потоке вместо этого с помощью IThreadListener.addScheduledTask.

Способ получения IThreadListener - заключается в использовании экземпляра Minecraft (на стороне клиента) или установки WorldServer (на стороне сервера). Приведенный выше код показывает пример этого, получая экземпляр WorldServer из EntityPlayerMP.
Будьте осторожны при обработке пакетов на сервере. Клиент может попытаться воспользоваться обработкой пакетов, отправив неожиданные данные.

Распространенной проблемой является уязвимость к генерации произвольных блоков. Обычно это происходит, когда сервер доверяет позиции блока, отправленной клиентом для доступа к блокам и объектам плитки. При доступе к блокам и Tile Entitiies в незагруженных областях мира сервер либо создаст, либо загрузит эту область с диска, а затем быстро запишет ее на диск. Это может быть использовано для нанесения катастрофического ущерба производительности сервера и объему хранилища.

Чтобы избежать этой проблемы, общее практическое правило - обращаться к блокам и объектам тайлов только в том случае, если world.isBlockLoaded(pos) истинно.

Обработка пакетов (для 1.13.2)
В обработчике пакетов следует выделить несколько моментов. Обработчику пакетов доступны как объект сообщения, так и сетевой контекст. Контекст позволяет получить доступ к игроку, отправившему пакет (если он находится на сервере), а также возможность поставить в очередь выполнения потоко-безопасную работу.
MyMessage.java:
public static void handle(MyMessage msg, Supplier<NetworkEvent.Context> ctx)
{
    ctx.get().enqueueWork(() -> {
        // Клиент, отправивший пакет.
        EntityPlayerMP sender = ctx.get().getSender();
      
        // Ваш код.
    });
  
    ctx.get().setPacketHandled(true);
}
Обратите внимание на наличие setPacketHandled, который используется для отправки сообщения сетевой системе, что пакет успешно завершил обработку.

Начиная с Minecraft 1.8 пакеты по умолчанию обрабатываются в сетевом потоке.

Это означает, что ваш обработчик не может напрямую взаимодействовать с большинством игровых объектов. Forge предоставляет удобный способ заставить ваш код выполняться в основном потоке вместо использования IThreadListener.addScheduledTask. Просто вызовите ctx.get().enqueueWork(Runnable), которая вызовет данный Runnable в основном потоке при следующей возможности.

Обработка пакетов (для 1.15 и выше)
В обработчике пакетов следует выделить несколько моментов. Обработчику пакетов доступны как объект сообщения, так и сетевой контекст. Контекст позволяет получить доступ к игроку, отправившему пакет (если он находится на сервере), а также возможность поставить в очередь выполнения потоко-безопасную работу.
MyMessage.java:
public static void handle(MyMessage msg, Supplier<NetworkEvent.Context> ctx) {
    ctx.get().enqueueWork(() -> {
        // Работа, которая должна быть потокобезопасной (большая часть работы)
        ServerPlayerEntity sender = ctx.get().getSender(); // the client that sent this packet
        // Ваш код
    });
    ctx.get().setPacketHandled(true);
}
Пакеты, отправленные с сервера на клиент, должны обрабатываться в другом классе и упаковываться через DistExecutor#unsafeRunWhenOn.
Java:
// В классе пакетов
public static void handle(MyClientMessage msg, Supplier<NetworkEvent.Context> ctx) {
    ctx.get().enqueueWork(() ->
        // Убедитесь, что он выполняется только на физическом клиенте
        DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> ClientPacketHandlerClass.handlePacket(msg, ctx))
    );
    ctx.get().setPacketHandled(true);
}

// В классе ClientPacketHandlerClass
public static void handlePacket(MyClientMessage msg, Supplier<NetworkEvent.Context> ctx) {
    // Ваш код
}
Обратите внимание на наличие setPacketHandled, который используется, чтобы сообщить сетевой системе, что пакет успешно завершил обработку.

Использование пакетов
При отправке пакетов убедитесь, что на принимающей стороне зарегистрирован обработчик указанного пакета. Иначе - пакет будет отправлен по сети, а затем выброшен, что приведет к "утечке" пакета (Это безвредно, за исключением ненужного использования сети).

Есть только один способ отправить пакет на сервер. Это потому, что существует только один сервер и, конечно же, только один способ отправки на этот сервер. Для этого нам снова нужен SimpleNetworkWrapper, который был определен ранее. Нужно просто вызвать:
Java:
ModidPacketHandler.INSTANCE.sendToServer(new MyMessage(*значение для отправки*))
Существует четыре различных метода отправки пакетов клиентам:
  • sendToAll - вызов INSTANCE.sendToAll, который отправит пакет один раз каждому игроку на сервере, независимо от того, в каком месте или измерении они находятся.
  • sendToDimension - вызов INSTANCE.sendToDimension, который принимает два аргумента: IMessage и integer. Integer - это идентификатор измерения для отправки, который можно получить с помощью world.provider.getDimension(). Пакет будет отправлен всем игрокам, находящимся в измерении, чей идентификатор был указан.
  • sendTo - вызов INSTANCE.sendTo, который служит для отправки пакетов одному клиенту. Для этого требуются IMessage и EntityPlayerMP для отправки пакета. Обратите внимание, что, хотя это не более универсальный EntityPlayer, пока вы находитесь на сервере, вы можете безопасно использовать любой EntityPlayer в EntityPlayerMP.
  • sendToAllTracking - вызов INSTANCE.sendToAllTracking, который отправляет пакеты всем игрокам на сервере, которые "отслеживают" цель. Есть два варианта: один принимает TargetPoint, а другой принимает Entity. В первом случае все клиенты, у которых загружен блок, расположенный в TargetPoint, получат пакет; для последнего все клиенты, которые находятся в пределах диапазона отслеживания предоставленной Entity, получат пакет. Обратите внимание, что EntityPlayer's не отслеживают себя, поэтому это не может заменить использование sendTo; если EntityPlayer, который используется в качестве цели, также требует синхронизации, вам необходимо вызвать sendTo прежде чем вызывать версию сущности sendToAllTracking.

Тоже, что и для 1.12.2.
Пакеты могут быть отправлены напрямую клиенту с помощью SimpleChannel: HANDLER.sendTo(MSG, entityPlayerMP.connection.getNetworkManager(), NetworkDirection.PLAY_TO_CLIENT).
Однако это может быть довольно неудобно. В Forge есть несколько удобных функций, которые можно использовать:
Java:
// Отправка одному игроку
INSTANCE.send(PacketDistributor.PLAYER.with(playerMP), new MyMessage(*значение для отправки*));

// Отправить всем игрокам, отслеживающим чанк
INSTANCE.send(PacketDistributor.TRACKING_CHUNK.with(chunk), new MyMessage(*значение для отправки*));

// Отправка всем подключенным игрокам
INSTANCE.send(PacketDistributor.ALL.noArg(), new MyMessage(*значение для отправки*));

Тоже, что и для 1.13.
Тоже, что и для 1.13.


Дополнение:
Если вы спросите, зачем нужна пакетная система и как она работает, то вот простой пример:

Допустим, в игрока пустили стрелу. Естественно у себя на клиенте он будет её видеть. А вот для других стрел в игроке - нет. Для этого Minecraft отправляет пакет на сервер с информацией о том, что в игроке есть стрела.
(Клиент отправляет на Сервер -> Сервер отправляет остальным Клиентам в сети)
Этот пакет вы также можете отправить и сами. Однако делать это надо обязательно с серверной стороны:
Java:
if(!world.isRemote)
{
    ((EntityPlayerMP) player).connection.sendPacket(new CPacket<TYPE>(...arguments));
}

Готовый код:
ModidPacketHandler.java:
public class ModidPacketHandler
{
    // Определение приватной переменной
    private static short id;
    // Тут указываем короткий идентификатор вашего пакетного канала
    // В данном случае он "8Ghdgimw3bt".
    public static final SimpleNetworkWrapper INSTANCE = NetworkRegistry.INSTANCE.newSimpleChannel("8Ghdgimw3bt");


    public static void register(){
        INSTANCE.registerMessage(MyMessageHandler.class, MyMessage.class, id++, Side.SERVER);
    }
}
MyMessage.java:
public class MyMessage implements IMessage
{
    // Конструктор по умолчанию. Всегда требуется
    public MyMessage(){}

    // Определяем Integer переменную toSend
    int toSend;

    public MyMessage(int toSend)
    {
      // Присваиваем заместо передаваемой Integer переменной - нашу
      this.toSend = toSend;
    }

    @Override public void toBytes(ByteBuf buf)
    {
      // Записывает int в buf
      buf.writeInt(toSend);
    }

    @Override public void fromBytes(ByteBuf buf)
    {
      // Читает int обратно из buf.
      // Обратите внимание: если у вас несколько значений,
      // вы должны читать их в том же порядке, в котором написали.
      toSend = buf.readInt();
    }
}
MyMessageHandler.java:
// Параметры IMessageHandler: <REQ, REPLY>
// Первый параметр - это пакет, который вы получаете, а второй - это пакет, который вы возвращаете.
// Возвращенный пакет можно использовать как "ответ" на отправленный пакет.
public class MyMessageHandler implements IMessageHandler<MyMessage, IMessage>
{
    // Обратите внимание, что требуется конструктор по умолчанию, но в этом случае он определяется неявно.

    @Override public IMessage onMessage(MyMessage message, MessageContext ctx)
    {
        // Игрок, от которого на сервер был отправлен пакет
        EntityPlayerMP serverPlayer = ctx.getServerHandler().player;

        // Значение, которое было отправлено
        int amount = message.toSend;

        // Выполнение действия в основном потоке сервера, добавив его как запланированную задачу
        serverPlayer.getServerWorld().addScheduledTask(() -> {
          serverPlayer.inventory.addItemStackToInventory(new ItemStack(Items.DIAMOND, amount));
        });

        // Если от пакета не последовал ответ
        return null;
    }
}
Автор
sk9zist :l
Просмотры
356
Первый выпуск
Обновление
Оценка
4.50 звёзд 2 оценок

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

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

  1. Обновление:

    Обновлён код, теперь он для версий 1.15 и 1.16
  2. Обновление для 1.13.x:

    Обновил код для 1.13.2. Поправил несколько косяков.

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

То что обзоры постоянно появляются, означает лишь, то что ктото зановос познает это. Ну и можно еще самооценку поднять, показав это всем :)
А так то норм, коль не идет в разрез с этим местом.
Ну не знаю, как по мне пакетка это настолько забитая тема, особенно для такой старой версии, как 1.12.
Даже в моей «устаревшей» статье про пакетку 2018 года инфы побольше чем здесь, я уже не говорю про ElegantNetwork и существующую статью господина Icosaider про пакетку.
Старания конечно это хорошо, но незнаю, незнаю…
Сверху