Лучшая практика для создания настраиваемой расширяемой архитектуры протокола? - PullRequest
0 голосов
/ 03 августа 2020

Заявление об ограничении ответственности: У меня нет опыта создания настраиваемых протоколов с большим масштабированием.

Я собираюсь начать новый проект для развлечения (желательно в java), состоящий из одного Мастер-сервер (MS), несколько меньших серверов (SS) в одной сети и несколько клиентов. Все эти три стороны должны передавать информацию друг другу.

Примеры:

  • Клиент «входит» в MS.
  • MS отправляет клиента на SS. (SS должен быть запущен, MS отправляет IP / PORT SS клиенту и сообщает ему о подключении, SS ожидает подключения клиента, ...)
  • SS и клиент обмениваются информацией друг с другом (например, игра сервер и клиент)

Самый большой опыт работы с настраиваемыми протоколами и пакетами в более крупном масштабе у меня есть на серверах Minecraft (Spigot, et al c.). При чтении серверной пакетной системы я все еще немного запутываюсь.

Во время исследования большую часть времени я нашел только базовые c учебные пособия о том, как создать модель TCP / UDP сервер-клиент на различных языках программирования. , который меня не интересует.

Что я хочу знать:

  • Я хочу создать свою собственную архитектуру протокола, но у меня нет идея с чего начать. Я хочу, чтобы он был очень расширяемым, но не слишком сложным.
  • Существуют ли какие-нибудь общие методы создания хороших пакетов -> «Как должно выглядеть пакетное сообщение?»

Простой ответ или рекомендация по ссылке уже могут мне очень сильно помочь! Я знаю, что это очень широкий вопрос, но мне нужно начать с какого-то момента.

1 Ответ

1 голос
/ 04 августа 2020

В основном то, что вы описываете, является прокси-сервером.

На данный момент это то, что мне пришло в голову. Сообщите мне о любых сомнениях, чтобы я мог решить их, расширив ответ.

Что такое прокси-сервер?

Прокси-сервер - это сервер, который маршрутизирует входящий трафик c к другим серверам (внутренним или внешним) и действует как посредник между клиентом и конечным сервером.

Есть несколько подходов к вашей проблеме.

Подход 1: Nginx + JSON

В этом случае я бы порекомендовал вам использовать прокси-сервер, например Nginx, который использует протокол HTTP. Затем информация будет передана в виде JSON строк вместо использования сырых двоичных пакетов, что значительно упростит проблему.

Для получения дополнительной информации о NGINX:

Подробнее информация о JSON:

Подход 2: Создание собственного прокси-сервера и использование бинарных пакетов

Для прокси-части вы можете использовать Java Сокеты и класс, который распространяет соединения путем чтения и открытия пакета формируют клиента, где он указывает желаемое место назначения. Тогда у вас будет два варианта:

  1. Перенаправить потоки сокета (Client-Proxy) на сокет (Proxy-WantedDestination).
  2. Сообщите WantedDestination, чтобы открыть соединение с клиентом . (ServerSocket на клиенте и Socket на WantedDestination) Таким образом, WantedDestination будет открывать соединение сокета с клиентом вместо того, чтобы клиент открывал соединение с местом назначения Wanted.

Первый метод позволяет вам для регистрации всех входящих и исходящих данных. Второй метод позволяет сохранить WantedDestination в безопасности.

Первый метод:

Client  <-->  Proxy  <-->  WantedDestination          (2 Sockets)

Второй способ:

Step 1: Client  <-->  Proxy

Step 2:               Proxy  <-->  WantedDestination  

Step 3: Client  <--------------->  WantedDestination  (1 socket)

Как структурировать пакеты

Я обычно структурирую пакеты следующим образом:

  1. Заголовок пакета
  2. Длина пакета
  3. Полезная нагрузка пакета
  4. Контрольная сумма пакета

Заголовок пакета может использоваться, чтобы определить, исходит ли пакет от вашего программного обеспечения и что вы начинаете считывать данные справа position.

Длина пакета указывает, сколько байтов поток должен прочитать перед попыткой десериализации пакета в его класс оболочки. Представим, что заголовок имеет длину 2 байта и длину 3 байта. Затем, если длина указывает, что пакет имеет длину 30 байт, вы будете знать, что конец пакета - (30 - 3 - 2) = 25 bytes away.

Полезная нагрузка пакета будет иметь переменный размер и будет содержать несколько байтов фиксированного размера в начало с указанием типа пакета. Тип пакета можно выбрать произвольно. Например, вы можете определить, что пакет типа (byte) 12 должен интерпретироваться как пакет, содержащий данные о совпадении понга.

Наконец, контрольная сумма пакета указывает сумму байтов пакета, который вы может проверить целостность пакета. Java уже предоставляет некоторые алгоритмы контрольной суммы, например CRC32. Если Packet Checksum = CRC32(Packet header, Packet length, and Packet Payload), то данные не повреждены.

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

package me.PauMAVA.DBAR.common.protocol;

import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.Checksum;

import static me.PauMAVA.DBAR.common.util.ConversionUtils.*;

public abstract class Packet implements Serializable {

    public static final byte[] DEFAULT_HEADER = new byte[]{(byte) 0xAB, (byte) 0xBA};

    private final byte[] header;

    private final byte packetType;

    private byte[] packetParameter;

    private byte[] packetData;

    private byte[] packetCheckSum;

    Packet(PacketType type, PacketParameter parameter) {
        this(type, parameter, new byte[0]);
    }

    Packet(PacketType type, PacketParameter parameter, byte[] data) {
        this.header = DEFAULT_HEADER;
        this.packetType = type.getCode();
        this.packetParameter = parameter.getData();
        this.packetData = data;
        recalculateChecksum();
    }

    public byte[] getParameterBytes() {
        return packetParameter;
    }

    public PacketParameter getPacketParameter() {
        return PacketParameter.getByData(packetParameter);
    }

    public byte[] getPacketData() {
        return packetData;
    }

    public void setParameter(PacketParameter parameter) {
        this.packetParameter = parameter.getData();
        recalculateChecksum();
    }

    public void setPacketData(byte[] packetData) {
        this.packetData = packetData;
        recalculateChecksum();
    }

    public void recalculateChecksum() {
        Checksum checksum = new CRC32();
        checksum.update(header);
        checksum.update(packetParameter);
        checksum.update(packetType);
        if (packetData.length > 0) {
            checksum.update(packetData);
        }
        this.packetCheckSum = longToBytes(checksum.getValue());
    }

    public byte[] toByteArray() {
        return concatArrays(header, new byte[]{packetType}, packetParameter, packetData, packetCheckSum);
    }

И тогда пользовательский пакет может быть:

package me.PauMAVA.DBAR.common.protocol;

import java.nio.charset.StandardCharsets;

import static me.PauMAVA.DBAR.common.util.ConversionUtils.subArray;

public class PacketSendPassword extends Packet {

    private String passwordHash;

    public PacketSendPassword() {
        super(PacketType.SEND_PASSWORD, PacketParameter.NO_PARAM);
    }

    public PacketSendPassword(String passwordHash) {
        super(PacketType.SEND_PASSWORD, PacketParameter.NO_PARAM);
        super.setPacketData(passwordHash.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public byte[] serialize() {
        return toByteArray();
    }

    @Override
    public void deserialize(byte[] data) throws ProtocolException {
        validate(data, PacketType.SEND_PASSWORD, PacketParameter.NO_PARAM);
        PacketParameter packetParameter = PacketParameter.getByData(subArray(data, 3, 6));
        if (packetParameter != null) {
            super.setParameter(packetParameter);
        }
        byte[] passwordHash = subArray(data, 7, data.length - 9);
        super.setPacketData(passwordHash);
        this.passwordHash = new String(passwordHash, StandardCharsets.UTF_8);
    }

    public String getPasswordHash() {
        return passwordHash;
    }
}

Отправить пакет в потоке будет так же просто, как:

byte[] buffer = packet.serialize();
dout.write(buffer);

Вы можете взглянуть на небольшой протокол, который я разработал для автоматического перезагрузчика сервера Bukkit здесь .

Обратите внимание, что этот метод потребует от вас преобразования между разными типами данных и байтовые массивы, поэтому вам потребуется хорошее понимание чисел c и представления символов в двоичном формате.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...