1. Этот сайт использует файлы cookie. Продолжая пользоваться данным сайтом, Вы соглашаетесь на использование нами Ваших файлов cookie. Узнать больше.
  2. Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.
    Скрыть объявление
Скрыть объявление
В преддверии глобального обновления, мы проводим исследования, которые помогут нам сделать опыт пользования форумом ещё удобнее. Помогите нам, примите участие!

Туториал Пишем своё ядро на Java

Тема в разделе "Руководства, инструкции, утилиты", создана пользователем ifxandy, 7 май 2021.

  1. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Когда-то давно я выпускал тему про написание ядра на Python. Из за Python'а её и захейтили, так что теперь пишем ядро на Java 8. *Туториал не блещет правильной терминологией*

    Мой проект по которому писался туториал на гитхабе: [ТЫК]

    Навигация:
    Часть 2 - [ТЫК]

    "Пишем своё ядро на Java"
    [​IMG]

    1. Подготавливаем среду
    Ядро мы будем писать с использованием небезызвестного фреймворка Spring. Для нашей работы нам понадобятся:
    1. Прямые руки
    2. Любое IDE (Здесь примеры на Eclipse)
    3. Интернет
    4. Майнкрафт клиент
    Spring - это современный фреймворк, почти что для всего. Изначально, он был нацелен на создание веб-сайтов, но в итоге, может использоваться для чего удобно. Так же он предоставляет очень удобный способ организации TCP-серверов и включает в себя много удобностей.

    Необходимо понимать, что тема предназначена для тех, кто уже имеет понимание основ Java 8 и может более менее самостоятельно на ней писать.
    Если вы уже потянулись создавать проект в своей IDE - не спешите этого делать. Для удобства, мы настроим и импортируем свой проект с Spring Initializr. Наш проект будет построен на Maven, поэтому смело выбираем Maven как сборщик. Далее, нам необходимо выбрать версию Spring, мы будем использовать 2.4.5. После этого, нам надо задать основную конфигурацию Maven'а, заполнять её следует используя объяснение приведённое ниже.

    groupId - это общая часть пакета всех ваших проектов. Например у меня это net.ifxandy, т.к. в своих проектов я указываю пакет net.ifxandy.[проект].
    artifactId - служебное название проекта, необходимое для Maven Central Repo.
    Name - название проекта.
    Description - небольшое описание проекта.
    Package - пакет, в котором будет находится главный класс Spring-приложения. Авто-дополняется используя groupId и artifactId, но можно изменить. [!] ВНИМАНИЕ [!] Если ваш artifactId начинается с заглавной буквы - измените её в Package, иначе, это может привести к тому, что название проекта в Package будет распознаваться как название класса и вызовет некоторые проблемы и лишние заморочки.

    Далее идёт поле Packaging, оно отвечает за то, во что будет собираться наш проект(формат файла), нам необходимо. чтобы проект собирался в Jar-файл, выбираем "Jar". Наконец версия Java, выбираем 8-ую.

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

    Теперь нам нужно добавить некоторые зависимости. Для этого есть панель справа. Нажимаем "ADD DEPENDENCY". В строке поиска вбиваем Lombok [ТЫК] - это библиотека для удобной работы над проектами основанная на аннотациях, которая автоматически дорабатывает ваш код. Она должна быть установлена у вас в IDE.


    Далее, нажимаем на кнопку GENERATE снизу и скачиваем ZIP-Файл с шаблоном проекта. Осталось только импортировать проект в нашу среду разработки. Вот пример для Eclipse IDE.
    File/Файл -> Import/Импорт -> Maven/Мавен -> Import an existing project/Импортировать существующий проект
    И находим через Browse папку своего проекта.

    Подготовка среды завершена!

    2. Подготовка конфигурации
    Изначально, Spring предоставляет минимум 2 способа конфигурации проекта - конфигурацию через application.properties и конфигурацию через класс аннотируемый @Configuration. Мы же совместим их по максимуму.
    Для этого, зайдём в main/resources в нужный файл .properties и зададим там 2 настройки. Первая - tcp.server.port=25565 отвечает за порт, на котором будет поднят сервер при запуске. Вторая - spring.main.web-environment=false отвечает за то, чтобы отключить запуск TomCat сервера(WEB обёртку Spring'а), ведь нам нужен TCP сервер.

    3. Пишем серверную логику
    3.1 Класс конфигурации
    Самое время создать тот обещанный класс конфигурации. Ниже представлен его код и разъяснения к нему.
    PHP:
    @Configuration

    public class TcpServerConfig {

        @
    Value("${tcp.server.port}")
        private 
    int port;

        @
    Bean
        
    public AbstractServerConnectionFactory serverConnectionFactory() {
            
    TcpNioServerConnectionFactory serverConnectionFactory = new TcpNioServerConnectionFactory(port);

            
    serverConnectionFactory.setUsingDirectBuffers(true);
            
    serverConnectionFactory.setSerializer(new PacketSerializer());
            
    serverConnectionFactory.setDeserializer(new PacketDeserializer());

            return 
    serverConnectionFactory;
        }

        @
    Bean
        
    public MessageChannel inboundChannel() {
            return new 
    DirectChannel();
        }

        @
    Bean
        
    public TcpInboundGateway inboundGateway(AbstractServerConnectionFactory serverConnectionFactory,
                                                
    MessageChannel inboundChannel) {
            
    TcpInboundGateway tcpInboundGateway = new TcpInboundGateway();
            
    tcpInboundGateway.setConnectionFactory(serverConnectionFactory);
            
    tcpInboundGateway.setRequestChannel(inboundChannel);
            return 
    tcpInboundGateway;
        }

    }
    Это - классовая конфигурация приложения, она помечается аннотацией @Configuration и находится Spring'ом автоматически. Перейдём к разбору переменных и аннотаций внутри.

    Переменная port и аннотация @Value("${tcp.server.port}") необходимы для того, чтобы показать TCP серверу порт для запуска. Аннотация же, определяет, что переменная будет равна переменной из .properties файла приложения, которую мы задавали чуточку ранее.

    @Bean аннотация / Бины - это всё те же объекты, но отличие составляет лишь то, что они живут и управляются Spring'ом внутри его DI-Контейнера. Почти все в Spring'е является бинами.

    Всё, что аннотировано @Bean является методом настройки*, т.е. методом, который возвращает уже готовый и настроенный инстанс нужного класса. TCP в SpringIntegration построено на фабрике подключений, она управляет

    3.2 Как это работает?
    Здесь будет описан алгоритм работы данной системы. Для хорошей реализации всегда нужно понимать алгоритм и то, как работает ваш код.

    У нас алгоритм довольно прост. Есть фабрика подключений TcpConnectionFactory. При каждом подключении она создаёт, настраивает и возвращает новый инстанс подключения. Клиент посылает по подключению пакеты.

    Для удобной работы, мы поднимаемся на 1 абстракцию вверх и работаем не напрямую с байтами(это было бы жуть как неудобно), поэтому мы и задавали в нашем @Bean методе-настройке ConnectionFactory сериалайзер и десериалайзер. Они то нам и помогут в этом деле.

    Serializer - класс имплементирующий интерфейс Serializer<?> (в нашем случае Serializer<Packet>). В нём имеется метод serialize принимающий как аргументы сериализуемый объект (у нас это Packet) и OutputStream, в который и отправляет после сериализации результат, т.е. клиенту.

    Deserializer - такой же как и Serializer класс, только имплементирует интерфейс Deserializer<?>. Только принимает он в свой метод deserialize только InputStream, а вот возвращает уже сериализуемый класс.

    Любой пакет от клиента сначала проходить через Deserializer и превращается в наш сообственный интерфейс Packet, после этого он попадает в TcpEndpoint. Это так скажем диспетчер, он определяет как и почему обрабатывать, что либо. У нас он будет состоять из Сервиса-Обработчика и метода, принимающего Packet класс и возвращающего ответ (или null, если ответа нет).

    Сервис-Обработчик - класс, аннотированый как @Service. Чаще всего его реализуют используя интерфейс(без аннотации) и класс, унаследованный от него(имплементацию, которую уже стоить пометить аннотацией). Spring сам находит и обрабатывает его.

    TcpEndpoint вызывает обработчик, получает ответ и возвращает его. Далее, наш экземпляр Packet'а проходит через Serializer, коим и отправляется клиенту.

    Вторая часть в комментариях. Coming Soon...
     
    Последнее редактирование: 8 май 2021
  2. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Тема противоречивая и интересная, не заявляю, что идеальная: прошу конструктивной критики
     
  3. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    Это версия Spring Boot. Версия Sprong Framework давно перевалила за 5.

    "Смешались люди, кони...."
    application.properties - файл с настройками переменных
    @Configuration - конфигурация контекста и бинов в нём
    Это разные вещи.

    Вы бы хоть объяснили людям, что это за загадочный "DI-контейнер" и вообще что такое Dependency Injection в двух словах.

    Не "настройки", а "провайдерами" (Provide), "фабриками" (Factory) "конструкторами" (Builder). Они не настраивают, а создают инстансы. Причём создают единожды (Singleton Scope).

    Что еще за "SpringIntegration" тоже не пояснено даже в двух словах.


    Подача сумбурная и даже мне, знакомому со спрингом не один год, слабо понятно что тут в итоге должно получится.
    Так же не раскрыт вопрос зачем был использован Спринг. В плане того, какие он даёт преимущества в данном проекте.
    И прошу не путать терминологии и обозначения, либо напишите в самом начале статьи, что это туториал "для самых маленьких".
     
  4. TaoGunner

    TaoGunner Активный участник Пользователь

    Баллы:
    66
    Имя в Minecraft:
    TaoGunner
    [​IMG]
    Один лишь вопрос: ты уже реализовал хотя бы авторизацию на самописном "ядре" или будешь это делать в процессе написания туториала?

    А вот почитать про сетевой обмен между сервером и клиентом было бы здорово.
     
  5. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Хорошо, учту и укажу. Но на счёт "провайдеров", "фабрик" и "конструкторов", они скорее всего совершают какие-то махинации перед тем как заретурнить инстанс, а это уже можно назвать какой-никакой настройкой этого инстанса.
    Да, конечно, пилю, как далее сов рисовать напишу. Если это был наезд за то, что надо реализовывать всё самому, то это проблема всех ядер, которые не являются форком чего либо другого.
     
    Последнее редактирование: 8 май 2021
  6. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    Внезапно - нет. Настройкой это можна было бы назвать, если на вход метода уже подаётся инстанс и ты в него данные загоняешь. Либо - как исключение - метод возвращает некоторый SomeConfiguration (привет hibernate), который является настройкой для чего-то бОльшего.

    По статье мне вот что еще не понятно: под "ядром" ты подразумеваешь какую-то распределённую систему управления множеством реалмов или это какой-то аналог BungeeCord, который будет частично работать с протоколом Minecraft?

    P.S.: Не забрасывай статью. Мне интересно что из твоего исследования по итогу выйдет.
     
  7. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Как раз писал продолжение, я её не заброшу, т.к. написана она по моему сообственному опыту (я параллельно пишу ядро на продакшн). Ну вообще, метод-провайдер же не состоит из обычного
    Код:
    return new MyShinyClassWhichNeedsProviderMethod();
    Он устанавливает какие-либо поля у этого инстанса нужные для его работы. Согласен, терминология в статье хромает, но если говорить грубо, настройкой это всё же назвать можно.

    Под ядром подразумеваю именно такое же ядро как ванилла ядро майна, пусть и делать долго и трудно, но если надо будет написать к примеру мини игру, где от Spigot'а того же надо будет 2% возможностей - сойдёт.
     
    Последнее редактирование: 8 май 2021
  8. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    3.3 Пишем TcpEndpoint
    Когда понимания работы, того, что мы хотим от себя и то, как мы будем это реализовывать мы уже набрались. Проекту мы уже сказали, что и как делать и закончили с конфигурацией. Стоило бы наконец написать тот самый "диспетчер". От него нам надо на самом деле немного, всего то 1 метод. Сложностью работы он тоже блестать не будет.

    TcpEndpoint-класс будет принимать в конструктор инстанс имплементации(класс implements интерфейс) MessageService, его напишем мы чуть позже. Сохранять его как private переменную и в своём единственном методе process дёргать его метод process и делать return значения. Звучит не очень понятно, поэтому ниже я пожалуй просто приведу код и объяснение к нему.
    Код:
    @MessageEndpoint
    public class TcpEndpoint {
       
        private MessageService service;
       
        @Autowired
        public TcpEndpoint(MessageService service) {
            this.service = service;
        }
       
        @ServiceActivator(inputChannel = "inboundChannel")
        public Packet process(Message<Packet> message) {
           
            return service.process(message);
           
        }
    
    }
    Это - наш класс TcpEndpoint, он помечен для Spring Integration как MessageEndpoint, т.е. место обработки сообщений (Message End Point - конечная точка сообщений) (но это не точно). Для удобства мы позже допишем ещё 1 абстракцию в виде сервиса обработчика.

    @Autowired - Очень удобная аннотация. Без неё, нам бы приходилось думать и плясать с бубном, чтобы передать в конструктор нужный Bean, но в данном случае, Spring сделал нам максимально удобно. Нам достаточно указать @Autowired аннотацию и Spring сам найдёт нужный Bean и передаст нам его!

    @ServiceActivator - Помечает метод, который аннотирован, как метод-обработчик (не бейте за такие супер объяснения) и задаёт, из какого канала получать наши сообщения.

    Message<Packet> - Интересная структура, мы бы могли получать и обычный Packet класс и всё бы работало, но это давало бы нам слишком мало информации о самом "сообщении" и о его отправителе. Spring Integration позволяет нам использовать и тот и тот вариант, но позже нам понадобится эта информация.

    3.4 Пишем MessageService
    Так как мы - люди дальновидные, мы не будем сразу бежать и создавать один единственный класс MessageService, мы поступим по умному и создадим интерфейс. А нам понадобится всего лишь 1 метод, да, всё вот так просто. Этот метод просто будет принимать Message<Packet> переданный сюда из TcpEndpoint и возвращать Packet инстанс (или null, если ответа на пакет нету). Выглядит это очень легко, даже аннотаций никаких нету, поэтому я думаю объяснения для приведённого кода не понадобится.

    PHP:
    public interface MessageService {

        
    Packet process(Message<Packetmessage);

    }

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

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

    Следующая часть в комментариях. Coming soon...
     
  9. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    И опять таки, внезапно - может)
     
  10. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    может, но не значит, что всегда и везде состоит
     
  11. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    Кстати, было бы хорошо, если б оставил ссылочку на github/gitlab/etc. с кодом сего продукта. Было бы удобно взять да изучить
     
  12. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Да, создам на досуге репозиторий (скорее я выложу проект, который делаю себе, т.к. нету кода на конкретный туториал).
     
  13. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    * Гитхаб теперь есть в начале статьи.
     
  14. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    Ну на данный момент оно даже Server Info не показывает.

    (это я для остальных мимокрокодилов пишу)
     
  15. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Потому что работа начата была день назад
     
  16. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    да я же не в укор твоему делу)
     
  17. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    Сделал, чтобы показывал, скоро залью на гитхаб.
     
  18. DmitriyMX

    DmitriyMX Старожил Пользователь

    Баллы:
    153
    Skype:
    dmn550
    ...и пропал. Ни статьи, ни кода.
     
  19. Автор темы
    ifxandy

    ifxandy Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    witwar
    3.5 PacketDeserializer
    Сейчас, мы находимся на стадии разработки, когда ничего нового сообственно писать уже почти и не надо, когда достаточно просто добавлять новые классы-имплементации по 1-ому и тому-же принципу. Единственное, что сейчас нам нужно написать это PacketDeserializer, PacketSerializer и обработку результатов их работы.

    * Вся информация была взята с github'ов других пользователей и вики [ТЫК] [ТЫК] [ТЫК].

    Начнём с того, что разберём протокол майнкрафта и сами пакеты. Для тех, кто не очень сильно знаком с сокетами и передачей информации в интернете, объясню. Пакет - основная форматированная единица информации во время передачи данных (Ссылка на подробную статью на ВИКИ: [ТЫК]). Любая передача данных построена на "перекидывании" этих пакетов между клиентом и сервером. Майнкрафт - не исключение. Вся сеть здесь построена на библиотеке / сетевом движке Netty.

    Любой пакет в протоколе майнкрафта имеет несколько ключевых переменных. Эти переменные - длина пакета(Length) и id пакета. У каждой переменной как и полагается, есть свой тип, по большей части они совпадают с обычными представлениями о типах переменных (как пример - Long, Int, Short, Byte, Boolean), но есть и специальные типы (к примеру - VarInt, VarLong, Chat и прочие).

    VarInt - Это integer, использующийся для того, что бы сообщить длину переменной. Но есть у него 1 особенность. Вкратце, его 7 младших битов - само значение, а вот самый старший байт - указывает на то, есть ли следующий байт для чтения (продолжение VarInt). VarInt не может быть длинной более 5 байт.

    VarLong - Это тоже самое, что и VarInt, но только с большей возможной длинной (у них она не больше 10 байт).

    Длина пакета (Length) идёт самой первой и определяет, сколько ещё необходимо считать байт для полного чтения пакета (т.е. чтобы считать всё его содержимое). Он определяется типом VarInt. С него начинается чтение абсолютно любого пакета.

    Id пакета по порядку идёт сразу после длины и определяет, что это за пакет и как его читать. Оно определяется 1 байтом. Вы могли бы подумать, что оно уникальное, но нет. Определение пакета зависит не только от ID пакета, но ещё и от состояния подключения. Вот такая вот кривость. Представлен он unsigned int'ом, тоже небольшая кривость, т.к. в Java unsigned - очень спорная тема.

    Далее идут сами переменные, т.е. само содержание пакета, которое всегда у всех разное.

    В PacketDeserializer'е нам доступ InputStream, из которого мы легко можем читать байты. Поэтому, первым делом давайте определим переменные.

    PHP:
    // Состояние чтения, от него зависит, что мы читаем
    int state 0;

    // Длина пакета, будет прочитана далее
    int length 0;
    // Массив байтов, считанное содержимое, прочитано будет далее
    byte[] packetData null;
    // ID пакета, считаем позже
    int id 0;

    Теперь, надо бы прочитать сами эти переменные. В InputStream у нас есть метод available() он возвращает int - сколько ещё осталось байт. Нам надо будет использовать его лишь потому, что изначально мы не знаем, какая длинна у пакета. Поэтому будем делать мы это в цикле while с условием, что available() > 0, т.е., что остались ещё байты для чтения.

    PHP:
    while (inputStream.available()) {

      
    // Тут будет наша логика чтения

    }

    Если state у нас 0, это значит, что мы только начали читать пакет, значит нам необходимо прочитать его длину. После того, как мы прочитали длину, мы можем читать всё остальное, поэтому сменяем state на 1.
    Если state = 1, значит, мы уже прочитали длину и можем читать пакет, поэтому давайте прочитаем его содержимое в packetData и прочитаем оттуда id. Для этого нам надо определить этот самый packetData и положить туда новый массив байтов с длинной length (т.е. длинной пакета). После этого мы вызовом метода прочитаем туда следующие length байт.

    PHP:
    while (inputStream.available()) {

      
    // Тут будет наша логика чтения
      
    if (state == 0) {

        
    // Здесь мы читаем длину, для этого воспользуемся классом утилит.

        
    length PacketUtil.readVarInt(stream);

        
    state 1;

      }

      if (
    state == 1) {

        
    // Читаем содержимое
        
    packetData = new byte[length];

        
    // InputStream.read(byte[] array, int offset, int len);
        
    inputStream.read(packetData0length);

        
    id Byte.toUnsignedInt(packetData[0]);

      }

    }

    В коде фигурирует непонятный файл утилит. Объяснять его думаю смысла нету, всё присутствует на ВИКИ по Протоколу и по большей части оттуда и скопировано (зачем писать своё если есть уже рабочее?). Просто оставлю ссылку на его обзор [ТЫК].

    Далее нам остаётся только проверять id и подбирать пакеты. Это уже относится к имплементации протокола и будет описано в следующей части.

    Продолжение в комментариях. Coming Soon...
     
  20. alexandrage

    alexandrage Старожил Пользователь

    Баллы:
    173
    Вот это костыли, netty спит в сторонке :D
     

Поделиться этой страницей