Русский
Русский
English
Статистика
Реклама

Protobuf

JPoint 2020 новый формат, новые возможности

04.07.2020 20:20:46 | Автор: admin
С 29 июня по 3 июля 2020 года в онлайн-формате прошла Java-конференция JPoint 2020. Информация о докладах, спикерах, особенностях проведения, впечатления от конференции всё это можно прочитать далее.



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

В предверии летнего блока конференций участники команды JUG Ru Group проделали титанический объём работы как административного, так и технического характера. Была создана онлайн-платформа для трансляции митапов и конференций. Также было проведено множество онлайн-встреч, в том числе Java-серия Первая чашка кофе с JPoint с интервью с участниками программного комитета и спикерами: Владимиром Ситниковым, Маргаритой Недзельской, Тагиром Валеевым, Олегом Докукой, Иваном Углянским и Алексеем Шипилёвым.

В блоге компании JUG Ru Group до летних конференций появилось множество интересных статей и интервью:

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

Открытие


Непосредственно перед открытием Алексей Фёдоров предоставил возможность увидеть пультовую Всевластья, помогающую организовать онлайн-конференции. После вводных слов и приветствия спонсоров конфереция была объявлена открытой.



Первый день


Прекрасным предисловием к интервью с James Gosling, отцом языка Java, стала статья, написанная phillennium. Беседу вели и задавали вопросы Андрей Дмитриев и Volker Simonis. Интервью получилось живое и эмоциональное, вызвавшее большой интерес у самого Джеймса. Было задано множество вопросов, от касающихся подробностей его прошлых проектов до отношения к популярному в настоящее время JVM-языку Kotlin. Безусловно, Джеймс является личностью, колоссальным образом повлиявшей на индустрию и внёсшей огромный вклад. Его присутствие в числе спикеров большая удача для конференции.



В перерыве между большими докладами можно было посмотреть познавательные интервью, одним из которых стало ML и AI: Как сейчас выглядит разработка решений в крупных компаниях Андрея Дмитриева с Дмитрием Бугайченко про машинное обучение и искусственный интеллект. Достаточно интересно было послушать мнение Дмитрия, являющегося экспертом в этой области и докладчиком этой и других конференций JUG Ru Group.



Доклад Precomputed data access with Micronaut Data от Graeme Rocher, автора Micronaut Framework. У данного спикера на конференции два доклада (доклад Micronaut deep dive был в этот же день чуть раньше, его я ещё планирую посмотреть). Очень полезным оказалось предварительное ознакомление с интервью, взятым недавно. В данном докладе было рассказано про Micronaut Data, легковесное решение для доступа к базам данных, выглядящее чрезвычайно привлекательно. После доклада Грэму вопросы слушателей и свои задавал Антон Архипов. На интересующий многих заданный Антоном вопрос, возможно ли использование Micronaut Data без всего остального из Micronaut Framework, был дан положительный ответ.



Второй день


В нативный код из уютного мира Java: Путешествие туда и обратно блестящий доклад Ивана Углянского на тему возможностей вызова из Java-кода процедур и функций нативных (native) библиотек. Всеобъемлющая ретроспектива существовавших до JNI альтернатив (JDK 1.0 NMI, RNI, JRI), популярных существующих сейчас (JNA, JNR, JavaCPP) и перспективных пока что экспериментальных (Panama, Sulong). Подробное сравнение всего современного вышеперечисленного (начиная с JNI) с большим количеством слайдов говорит об огромной проделанной работе. Очень удачные выбранные аналогии на тему произведений Толкиена: левый слайд (Шир) иллюстрирует милый и безопасный Java-код, правый слайд опасный нативный код (Мордор).



How to develop a successful Kubernetes native application using Quarkus небольшой пятнадцатиминутный доклад Alex Soto Bueno от компании RedHat, спонсора конференции. Доклад о разработке микросервисов с использованием Kubernetes и фреймворка Quarkus, детища RedHat.



Олег Шелаев является одним из тех спикеров, доклады которых всегда можно смело выбирать, зная, что совершенно точно будет интересно, увлекательно и полезно. Обладает редкой способностью просто объяснять очень сложные с технической точки зрения вещи. Доклад под названием Polyglot done right with GraalVM не стал исключением в этом смысле. В нём Олег продолжил раскрывать тему GraalVM, являясь developer advocate проекта GraalVM в OracleLabs. В данном докладе более полно была раскрыта направленность продукта на возможность одновременного применения различных языков программирования: API, шаблоны взаимодействия и прочие детали GraalVM. Ожидания от прослушанного полностью оправдались, отличный доклад.



Третий день


Всеволод Брекелов входит в команду JUG Ru Group, активно участвуя в проведении летнего блока конференций, к которому относится и конференция JPoint. Тем интереснее, регулярно видя его в роли ведущего конференций, было посмотреть доклад в его исполнении под названием Contract testing: Should or shouldn't? Ему очень удачно помогали Андрей Дмитриев, Владимир Плизга и Алексей Виноградов например, представление Владимиром докладчика в самом начале просто восхищает оригинальностью. Обсуждение было посвящено контрактным тестам, были последовательно продемонстрированы несколько подходов с использованием Spring Cloud Contract, Pact и Protocol Buffers. Получилось зажигательно и интересно.



Доклад Страх и ненависть в Scala и Kotlin interop от Маргариты Недзельской был посвящён проблемам взаимодействия кода, написанного на двух JVM-языках Kotlin и Scala. Название доклада является аллюзией на фильм Fear and Loathing in Las Vegas, им же достаточно оригинально был проиллюстрирован весь рассказ. Проблемы вызвали искреннее сочувствие, технические подробности были приведены весьма убедительные. Маргарите помогали Паша Финкельштейн и Евгений Мандриков, ведя беседу, озвучивая результаты голосований и задавая вопросы слушателей.



Четвёртый день


Ещё немного маленьких оптимизаций стал своеобразным продолжением доклада, сделанным на конференции Joker 2019 тем же автором, Тагиром Валеевым. Доклад первой части был посвящён улучшениям в строках, коллекциях и операциям с числами, в этот раз уже другим оптимизациям тоже в строках, коллекциях и теперь ещё и в reflection. Изменения, о которых было рассказано, произошли в версиях Java с 9 по 16. Традиционное глубокое понимание темы, множество результатов сравнений, характерные для докладов Тагира всё это было и в этот раз.



На Интервью и Q&A с Алексеем Шипилёвым интервьюеры Алексей Фёдоров и Иван Крылов поговорили и задали вопросы Алексею Шипилёву об особенностях работы в Red Hat, про используемые инструменты performance-инженера, про различия сборщиков мусора в Java, историю создания Shenandoah GC, об отношении к статьям с замерами производительности, мнении о GraalVM, про совместное использование jmh и async-profiler, о советах для молодых разработчиков и инженеров.



Пятый день


Совместный доклад настоящих звёзд конференций Баруха Садогурского и Евгения Борисова, озаглавленный ими Вырасти своего работодателя в условиях коронавируса, Или как сделать так, чтобы вас не уволили в кризис об особенностях удалённой работы, типах руководителей, проблемах при человеческих коммуникациях с рекомендациями для решения всех возникающих при этом проблем. Хороший нетехнический доклад в завершающий день конференции, демонстрация помех для работы при участии семьи Евгения Борисова в конце доклада была просто великолепна.



Внедрение open source-решений на примере Одноклассников: интервью Дмитрия Чуйко с Андреем Паньгиным. Одной из тем разговора стал переход компанией Одноклассники на использование дистрибутива Liberica JDK компании BellSoft, поэтому представляющий BellSoft Дмитрий Чуйко в качестве берущего интервью был весьма уместен. Также были упомянуты популярные проекты Андрея one-nio и async-profile, тоже являющиеся open source-решениями и вызывающие интерес и уважение.



Доклад Valhalla is coming от Сергея Куксенко был продолжение его же предыдущего доклада, сделанного им на Joker 2019. С конца октября 2019 года в разработке инлайн-типов произошли значительные изменения, подробно о которых было рассказано примерно с середины данного доклада. Сергей харизматичный спикер и высококвалифицированный инженер, доклады которого безошибочно всегда можно выбирать. Отлично дополнил доклад Иван Углянский, задававший вопросы и помогавший Сергею во взаимодействии со слушателями.



Прочие события


Кроме впечатляющей онлайн-платформы для стриминга конференций, всевозможных активностей во время их проведения к летним конференциям была выпущена новая версия веб-приложения, о котором ранее уже писалось в обзорах про конференции TechTrain 2019 и Joker 2019. Приложение доступно по ссылке, в репозитории на GitHub (ставьте звёздочки) имеется описание с информацией, включающей актуальную ссылку на веб-сайт.

Приложение, ранее бывшее только игрой по угадыванию спикера, теперь разделено на две части. В первой из них можно произвести поиск и просмотр информации обо всех конференциях JUG Ru Group, а также митапах Java-сообществ JUG.ru, JUG.MSK, JUGNsk. Содержится абсолютно та же информация, что и представленная на сайтах конференций и митапов. Доступны для удобного просмотра уже опубликованные видео и презентации докладов (ниже для примера показано отображение сведений об Антоне Архипове и об одном из его докладов).



В разделе со статистикой приведены сведения, которые могут заинтересовать как организаторов конференций, так и их участников: с какого времени проводится каждая из конференций или каждый из митапов, общая их длительность, количество конференций, докладов и спикеров, сколько из спикеров удостоено звания Java Champion или Most Valuable Professional (MVP). Можно щёлкнуть по картинкам для их увеличения (или посмотреть то же самостоятельно в веб-приложении по ссылке, приведённой выше).

Второй и третий скриншоты ниже показывают топ спикеров по количеству сделанных ими докладов (скриншот слева без учёта митапов, справа конференции вместе с митапами). Уверенную победу в обоих случаях (только конференции и конференции с митапами) одерживает Барух Садогурский, на втором месте Евгений Борисов. Третье месте в случае только конференций Кирилл Толкачёв, конференции с митапами Алексей Шипилёв.



В игре Угадай спикера, второй части веб-приложения, после загрузки данных обо всех конференциях и митапах стало возможным использовать все ранее доступные режимы угадывания для конкретной конференции (например, JPoint 2020). По умолчанию для угадывания предлагается в данный момент идущая либо ближайшая конференция. Дополнительно были реализованы возможности попытаться угадать Twitter, GitHub спикеров и, наоборот, спикера по представленному их Twitter, GitHub.



Закрытие


Процедура закрытия, сопровождавшаяся ранее полным зрителей залом и традиционным выходом на сцену спикеров, участников программного комитета и организаторов, в связи с онлайн-форматом претерпела существенные изменения. Кроме закрывающих конференцию Всеволода Брекелова и Алексея Фёдорова на экране можно было увидеть в прямом эфире многих организаторов и спикеров.



Совершенно точно, что в сложившейся сегодня ситуации команда JUG Ru Group сделала абсолютно всё возможное (мне кажется, что даже чуточку больше), чтобы праздник, которым конференция всегда бывает и для спикеров, и для обычных участников, состоялся. В какой-то степени, праздник можно продлить у каждого осталось много замечательных докладов, оставшихся пока непросмотренными, и имеется возможность поделиться своими впечатлениями от конференции в комментариях к статье.

Сезон летних конференций JUG Ru Group продолжается по-прежнему можно успеть присоединиться к оставшимся двум онлайн-конференциям DevOops (6-10 июля 2020 года) и Hydra (6-9 июля 2020 года). Есть возможность купить единый билет на все восемь конференций, видео докладов в этом случае становятся доступны сразу же после завершения конференций.
Подробнее..

Protobuf vs Avro. Как сделать выбор?

29.11.2020 16:21:40 | Автор: admin

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

Размер и скорость

В сети можно найти сравнительные тесты форматов сериализации. Не стоит придавать значение конкретным числам, так как скорость сериализации/десериализации, как и размер получающихся двоичных данных, зависит от конкретной схемы данных и от реализации сериализатора. Отметим лишь, что авро и протобаф занимают лидирующие позиции в подобных тестах.

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

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

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

Типы данных

Примитивные типы, представленные в обоих форматах: bool, string, int32(int), int64(long), float, double, byte[]. Протобаф также поддерживает uint32, uint64.

В протобафе, по-умолчанию, целые числа кодируются в формате varint, эффективном для небольших положительных чисел. Вы можете изменить это, указав : sint32, sint64, fixed32, fixed64, sfixed32, sixed64.

Из коллекций вам доступны массивы и отображения (map). Ключом в отображении должна быть строка (в случае протобафа также допускается целое число).

Оба формата поддерживают перечисления (enumerations).

Сложные типы конструируются с помощью алгебраического умножения (records в авро, message в протобафе) и сложения (union в авро, oneof в протобафе).

Для того, чтобы описать опциональное поле, в авро, необходимо использовать union с двумя вариантами, один из которых null, а в протобафе - oneof из одного варианта.

Оба формата поддерживают механизмы расширения системы типов (logical types в авро и well known types в протобафе). Таким образом обе схемы дополнительно поддерживают сериализацию даты и времени (timestamp) и продолжительности времени (duration).

В отличии от авро, протобаф не поддерживает decimal и UUID. Также авро поддерживает тип fixed - массив байт определенной длины.

Итого, хотя отсутствие поддержки decimal в протобафе - досадное упущение, система типов, тоже не будет определяющей при выборе.

Эволюция данных

Обе схемы поддерживают механизмы обратной совместимости (backward compatibility) за счет заполнения новых полей значениями по-умолчанию. В авро можно указать любое, допустимое значение, в протобафе это значение задано жестко, в зависимости от типа (0, пустая строка, false). В авро также поддерживаются альтернативные имена (aliases) для полей и именованных типов (record, enum, fixed). В протобафе имя поля не используется в двоичной сериализации, но номер поля не может быть изменен.

Для числовых типов, в авро допускаются только преобразования без потери (например int в long, float в double, но не наоборот). Протобаф более толерантен к изменению числовых типов и применяет правила преобразования, идентичные C++. Также протобаф допускает преобразования из bool в число и обратно, из целого числа в enum и обратно.

В случае, когда потребитель данных использует старую схему, а производитель более новую, важно, чтобы новый формат данных не сломал работу потребителя. Такое свойство схемы данных называется упреждающей совместимостью (forward compatibility).

Неизвестное поле в записи игнорируется обоими форматами.

В случае неизвестного значения enum, авро подставляет значение по-умолчанию, если оно задано, протобаф - нулевое значение.

Неизвестный вариант (case) в объединении (union) протобаф помечает признаком unknown. Авро же, в этом случае, выдает исключение и прерывает десериализацию.

Это серьезное ограничение авро. Если вы используете алгебраические типы данных (ADT), то авро, скорее всего, вам не подходит, так как не поддерживает упреждающую совместимость при добавлении нового варианта в объединение.

Представление в Json

Как авро, так и протобаф, поддерживают сериализацию в Json. Это может быть полезно, как для отладочных целей, так и для долгосрочного хранения данных (например, в MongoDB).

В случае протобафа, нарушится обратная совместимость, если вы поменяете название поля (на самом деле есть трюк, и вы можете переименовать поле один раз, использовав атрибут json_name для сохранения информации о прежнем имени поля). Авро же позволяет давать несколько альтернативных названий (aliases) полям.

Неприятным свойством авро является то, что массив байт (типы bytes, fixed) сохраняется в виде UTF16 строки. Это не только порождает визуальный мусор (псевдографика, переводы строк и т.п), но и может сделать Json нечитаемым, так как не все библиотеки корректно транслируют UTF16. Протобаф же сохраняет массив байт в base64.

Представление в формате Json не должно повлиять на ваш выбор, не забывайте только, что в случае протобафа, вы можете переименовать поле только один раз, а в случае авро, нужно проверить ваш стек на корректность обработки UTF16.

Влияние на архитектуру

Выбрав авро, вы должны будете решить, где хранить схему. В том случае, если вы работаете с большим количеством сообщений малого размера (например, отправляете сообщения через кафку), вам нужно хранить схемы данных в отдельном хранилище (например, использовать Schema Registry). Вам потребуется реализовать кеширование схем, что привнесет в вашу систему состояние (statefullness), будет забирать время на прогрев при запуске.

Если ваш язык программирования обладает динамической системой типов (например, python), то протобаф, с его требованием заранее задать типы всех полей, может стать для вас слишком обременительным. Авро же, наоборот, может стать хорошим решением, так как позволяет десериализировать данные на лету, зная лишь схему по которой эти данные были сохранены. Для случаев динамической типизации, в протобафе появился тип Any, но, согласно официального сайта, его поддержка в процессе разработки.

RPC

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

Протокол авро поддерживает синхронный вызов процедуры и вызов без ожидания ответа (one-way). Протокол включает в себя этап рукопожатия (handshake), в время которого стороны обмениваются схемами.

Сервис протобафа допускает как синхронный вызов, так и отправку потока данных (streaming) в обоих направлениях.

Флагманская реализация RPC для протобафа - gRPC. Несмотря на то, что gRPC, по-умолчанию, использует протобаф, теоретически он позволяет использовать и другие механизмы сериализации, включая авро. На практике же, все зависит от конкретной реализации, так, например, в дотнете, Майкрософт прилагает много усилий для того, чтобы выжать максимальную производительность из реализации gRPC в связке с протобафом, но вы вступаете на скользкую дорожку, пытаясь подменить сериалайзер, например, на авро.

Прежде чем сделать выбор, убедитесь, что в вашем стеке существует реализация RPC, удовлетворяющая ваши потребности.

Kafka

Изначально схема данных в кафке описывалась с помощью авро. В настоящее время добавлена поддержка протобафа.

Hadoop

Ситуация зеркальна gRPC. Несмотря на то, что Hadoop - это вотчина авро, с помощью библиотеки elephant-bird вы можете использовать протобаф в связке с хадупом.

Комьюнити

Оба продукта доступны в открытом коде.

https://github.com/apache/avro (1.7K звезд, 1.1К форков)

https://github.com/protocolbuffers/protobuf (45K звезд, 12.1К форков)

Подробнее..

Оптимизация хранимых данных на 93 (Redis)

12.03.2021 16:12:58 | Автор: admin

Хотелось бы поделиться опытом оптимизации данных с целью уменьшения расходов на ресурсы.

В системе рано или поздно встает вопрос об оптимизации хранимых данных, особенно если данные хранятся в оперативной памяти, как это БД Redis.

Как временное решение, можно увеличить RAM тем самым можно выиграть время.

Redis это no-sql база данных, профилировать ее можно с помощью встроенной команды redis-cli --bigkeys, которая покажет кол-во ключей и сколько в среднем занимает каждый ключ.

Объемными данными оказались исторические данные типо sorted sets. У них была ротация 10 дней из приложения.

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

Данные из себя представляли события измененияцены / даты доставки у оффера. Офферов было очень много порядка 15000 в каждом фиде (прайслисте).

Рассмотрим следующий пример данных события по офферу:

Сделано с помощью http://json.parser.online.fr/Сделано с помощью http://json.parser.online.fr/

{"EventName":"DELIVERY_CHANGED","DateTime":"2021-02-22T00:04:00.112593982+03:00","OfferId":"109703","OfferFrom":{"Id":"109703","Name":"Саундбар LG SN11R","Url":"https://www.example.ru/saundbar-lg-sn11r/?utm_source=yandex_market&utm_medium=cpc&utm_content=948&utm_campaign=3&utm_term=109703","Price":99990,"DeliveryAvailable":true,"DeliveryCost":0,"DeliveryDate":"2021-02-24T23:49:00+03:00"},"OfferTo":{"Id":"109703","Name":"Саундбар LG SN11R","Url":"https://www.example.ru/saundbar-lg-sn11r/?utm_source=yandex_market&utm_medium=cpc&utm_content=948&utm_campaign=3&utm_term=109703","Price":99990,"DeliveryAvailable":true,"DeliveryCost":0,"DeliveryDate":"2021-02-23T00:04:00.112593982+03:00"}}

Такое событие занимает 706 байт.

Оптимизация

  1. Для начала я уменьшил ротацию до 7 дней, так как использовалась именно последняя неделя. Здесь стоит отметить, что шаг весьма легкий(в исходном коде изменил 10 на 7), сразу сокращает размер RAM на 30%.

  2. Удалил из хранилища все данные, которые записывались, но не использовались во время чтения, такие как name, url, offerId что сократило еще примерно на 50%.

    Cтало:

    {"EventName":"DELIVERY_CHANGED","DateTime":"2021-02-22T00:04:00.112593982+03:00","OfferId":"109703","OfferFrom":{"Price":99990,"DeliveryAvailable":true,"DeliveryCost":0,"DeliveryDate":"2021-02-24T23:49:00+03:00"},"OfferTo":{"Price":99990,"DeliveryAvailable":true,"DeliveryCost":0,"DeliveryDate":"2021-02-23T00:04:00.112593982+03:00"}}

    Теперь событие занимает 334 байта.

    1. Переделал формат хранение с json в бинарный protobuf.

      Об этом шаге хотелось бы рассказать подробнее

      1. Составил схему хранение данных, в случее с protobuf это proto - файл:

        syntax = "proto3";import "google/protobuf/timestamp.proto";message OfferEvent {  enum EventType {    PRICE_CHANGED = 0;    DELIVERY_CHANGED = 1;    DELIVERY_SWITCHED = 2;    APPEARED = 3;    DISAPPEARED = 4;  }  EventType event_name = 1;  google.protobuf.Timestamp date_time = 2;  string offer_id = 3;  message Offer {    int32 price = 1;    bool delivery_available = 2;    int32 delivery_cost = 3;    google.protobuf.Timestamp  delivery_date = 4;  }  Offer offer_from = 4;  Offer offer_to = 5;} 
        
      2. Исходное сообщение в текстовом protobuf формате будет выглядеть так

        event_name: DELIVERY_CHANGEDdate_time {  seconds: 1613941440}offer_id: "109703"offer_from {  price: 99990  delivery_available: true  delivery_date {    seconds: 1614199740  }}offer_to {  price: 99990  delivery_available: true  delivery_date {    seconds: 1614027840  }}
        
      3. Сообщение в итоговом бинарном protobuf формате будет выглядеть так

        echo 'event_name: DELIVERY_CHANGEDdate_time {  seconds: 1613941440}offer_id: "109703"offer_from {  price: 99990  delivery_available: true  delivery_date {    seconds: 1614199740  }}offer_to {  price: 99990  delivery_available: true  delivery_date {    seconds: 1614027840  }}' | protoc --encode=OfferEvent offerevent.proto | xxd -p | tr -d "\n"0801120608c095cb81061a06313039373033220e08968d061001220608bcf7da81062a0e08968d061001220608c0b8d08106
        

      Теперь событие занимает 50 байт. Это сократило потребление памяти на 85%.

      Бинарное сообщение без proto-схемы можно посмотреть с помощью онлайн-сервиса https://protogen.marcgravell.com/

Итого

Оптимизация места более, чем в 14 раз (50 байт против 706 байт изначальных), то есть на 93%.

Подробнее..

Кроссплатформенный мультиплеер на Godot без боли

30.01.2021 14:20:43 | Автор: admin

Что хотим сделать?

Синхронизацию действий игроков в игре с клиент-серверной архитектурой. Должна быть возможность играть из браузера.

Для примера реализуем простую чат-комнату:

  1. При соединении:

    1. Клиент получает уникальный ID;

    2. Клиент получает информацию о всех остальных игроках (ID + имя);

    3. Все остальные игроки получают информацию о новом игроке (ID + имя по умолчанию);

    4. В консоли появляется сообщение о входе.

  2. При потере соединения:

    1. Все остальные игроки получают информацию о выходе игрока с сервера (ID);

    2. В консоли появляется сообщение о выходе.

  3. При изменении имени:

    1. Если имя уже занято - игрок получает ошибку;

    2. Все игроки уведомляются об изменении имени;

    3. В консоли появляется сообщение.

  4. При отправке сообщения в чат:

    1. Все игроки видят сообщение в логе/консоли.

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

Что получилось?

Готовый проект можно изучить здесь: https://github.com/ktori/godobuf-over-websocket-demo

Скриншоты можно посмотреть в конце статьи.

Что будем использовать?

  • Godot - free and open source кроссплатформенный игровой движок;

  • Protobuf - механизм для эффективной сериализации/десериализации данных;

  • Godobuf - плагин для Godot, позволяющий генерировать .gd (GDScript) файлы из .proto;

  • Ktor - фреймворк для создания асинхронных сервисов Kotlin (в этой статье я буду использовать Kotlin - но бэкэнд может быть написан на любом другом языке, главное - иметь в фреймворке возможность принимать вебсокет-соединения и желательно - генератор кода из Protobuf, эти генераторы существуют для множества языков).

Плюсы этого подхода

  • Все сообщения, которыми обмениваются клиент и сервер, описываются в одном месте:

    • Из этих файлов можно сразу сгенерировать код и для сервера и для клиента;

    • В них же можно вести документацию, оставляя комментарии;

    • Описание протокола можно легко хранить в любой VCS, т.к. по сути это просто текстовые файлы;

    • Можно точно знать что обе стороны будут сериализовывать и десериализовывать сообщения одинаково - генерация кода обеспечит отсутствие забытых полей и каких-либо других ошибок свойственных при ручном чтении/записи.

  • Protobuf - бинарный формат, и в отличие от, например, JSON - будет использоваться меньший объем трафика для передачи одного и того же объема данных;

  • Protobuf позволяет добавлять новые поля, не ломая совместимость со старыми клиентами.

Минусы этого подхода

Совсем явных минусов я назвать не могу - но:

  • Сериализация/десериализация в protobuf будет проходить медленнее, чем, например, прямая запись в буфер в собственном формате;

  • Код, который генерируется из protobuf часто получается довольно громоздким и, соответственно, имеет определенную стоимость в рантайме.

Описание протокола

Готовый протофайл можно посмотреть здесь: game.proto

Создадим пустой .proto-файл, например - game.proto. В этом файле нужно описывать все сообщения, которыми будут обмениваться сервер и клиент (если сообщений будет много - можно выносить их в отдельные файлы и импортировать из основного).

В этот файл следует сразу прописать опции для парсера и кодогенератора:

syntax = "proto3";// Название пакетаoption java_package = "me.ktori.game.proto";// Название класса в котором будут находиться подклассы сообщенийoption java_outer_classname = "GameProto";

А теперь определимся, какие сообщения нам вообще нужны:

Сообщения клиент-сервер

Это сообщения, которые клиент отправляет серверу - часто они будут по сути RPC вызовами с ответом в сообщении Cl**Result от сервера. Здесь был бы очень кстати gRPC - возможно в будущем с помощью godobuf можно будет делать и gRPC-сервисы. Но пока:

//// Сообщения клиент-сервер//// Запрос на изменение имениmessage ClSetName {  string name = 1;}// Отправка сообщения в чатmessage ClSendChatMessage {  string text = 1;}// Объединение всех сообщений, отсылаемых клиентомmessage ClMessage {  // Только одно из этих полей может быть заполнено, таким образом сервер  // может быстро определить, что именно хочет сделать клиент  oneof data {    ClSetName set_name = 1;    ClSendChatMessage send_chat_message = 2;  }}

Сообщения сервер-клиент

//// Сообщения сервер-клиент//// Результат выполнения команды ClSetNamemessage ClSetNameResult {  // Удалось ли изменить имя - имя нельзя изменить на уже занятое  bool success = 1;}// Отсылается сервером - объединение всех возможных результатов выполнения команды от клиентаmessage ClMessageResult {  oneof result {    ClSetNameResult set_name = 1;  }}// Отсылается клиенту один раз при соединении// Получатель этого сообщения сохраняет у себя полученный ID и выданное сервером имяmessage SvConnected {  int32 id = 1;  string name = 2;}// Уведомление о подключении нового клиента// Получатель должен сохранить имя клиента по IDmessage SvClientConnected {  int32 id = 1;  string name = 2;}// Уведомление об отключении клиента// Получатель может удалить у себя информацию о клиенте по IDmessage SvClientDisconnected {  int32 id = 1;}// Уведомление об изменении имени// Получатель должен изменить имя клиента по ID на новоеmessage SvNameChanged {  int32 id = 1;  string name = 2;}// Сообщение в чатеmessage SvChatMessage {  int32 from = 1;  string text = 2;}// Объединение всех сообщений которые сервер посылает клиентуmessage SvMessage {  // Только одно из этих полей будет заполнено в одном SvMessage  oneof data {    ClMessageResult result = 1;    SvConnected connected = 2;    SvClientConnected client_connected = 3;    SvClientDisconnected client_disconnected = 4;    SvNameChanged name_changed = 5;    SvChatMessage chat_message = 6;  }}

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

  • Все возможные сообщения от клиента обернуты в ClMessage;

  • Все возможные сообщения от сервера обернуты в SvMessage;

    • Ответы на вызовы клиента обернуты в поле result - сообщение ClMessageResult.

Лично для себя я определилась с такой naming convention:

  • ClFooBar для сообщений, которые шлёт клиент серверу;

  • SvFooBar для сообщений, которые шлёт сервер клиенту, за исключением:

  • ClFooBarResult для передачи результата обработки ClFooBar.

Создание клиентской части на Godot

Для начала нужно создать проект и основную сцену (обычную пустую 2D сцену).

Добавление плагина Godobuf

Плагин можно скачать здесь: https://github.com/oniksan/godobuf, инструкция по установке есть в README репозитория - нужно распаковать себе в проект папку addons.

Проект после установки аддона godobuf Проект после установки аддона godobuf

Открытие соединения

Для соединения с сервером используется класс WebSocketClient (документация по WebSocketClient). Работать с ним просто: устанавливаем обработчики событий, а затем указываем URL сервера для соединения.

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

extends Node2Dvar ws: WebSocketClient# Вызывается при загрузке сценыfunc _ready():    # Создаем WebSocketClient и подключаем обработчики событий    ws = WebSocketClient.new()    ws.connect("connection_established", self, "_on_ws_connection_established")    ws.connect("data_received", self, "_on_ws_data_received")    # Подключаемся к локалхосту по порту 8080    ws.connect_to_url("ws://127.0.0.1:8080")# Будет вызываться при установке соединенияfunc _on_ws_connection_established(_protocol):    pass# Будет вызываться при получении сообщений из вебсокетаfunc _on_ws_data_received():    pass

Генерация биндингов protobuf:GDScript

Здесь всё очень просто! Во вкладке Godobuf указываем путь до нашего proto-файла и путь куда будет сохранен получившийся скрипт:

Окно GodobufОкно Godobuf

Если в прото-файле нет ошибок, то мы увидим сообщение об успешной компиляции и в папке проекта появится нужный скрипт.

Отправка сообщений

Настройка сцены

 Сцена Сцена

В своей сцене я сделала отдельный контейнер для сообщений и два поля - для ввода текста и имени. Сигналы pressed от кнопок Send и Rename я подключила в скрипт на корневой ноде. Также для вывода сообщений на сцену я сделала функцию show_message, она просто добавляет новый объект Label с текстом сообщения в VBoxContainer, который располагает объекты вертикально.

Отправка запросов на сервер

После создания этих полей ввода и кнопок нужно сделать так чтобы они что-то делали.

Сперва загрузим получившиеся биндинги в наш скрипт:

const GameProto = preload("res://game_proto.gd") 

Теперь можно добавить код создания ClMessage при нажатии на кнопки Send/Rename:

# Изменяем имя на введенное в $Namefunc _on_SetName_pressed():    var msg = GameProto.ClMessage.new()    var sn = msg.new_set_name()    sn.set_name(name_input.text)    send_msg(msg)# Отправляем сообщение из $Message и очищаем полеfunc _on_SendMessage_pressed():    var msg = GameProto.ClMessage.new()    var scm = msg.new_send_chat_message()    scm.set_text(message_input.text)    message_input.clear()    send_msg(msg)

Самое интересное - сама отправка сообщения по вебсокету происходит в функции send_msg. Вот она:

# Отправляет ClMessage на серверfunc send_msg(msg: GameProto.ClMessage):    # Конвертируем ClMessage в PoolByteArray и отправляем его по соединению ws    ws.get_peer(1).put_packet(msg.to_bytes())

Функция to_bytes (как и весь класс ClMessage) сгенерированы плагином godobuf - и никаких операций с буферами руками нам делать не надо!

Обработка сообщений

Теперь наш клиент может отправлять сообщения - но он ещё не способен их принимать. Сейчас мы это исправим, добавив обработку входящих сообщений - этот блок кода будет объемнее, но по большей части код там повторяется.

Код получения и обработки сообщений
# Вызывается часто по интервалуfunc _process(_delta):    # Производит чтение из вебсокета, читает входящие сообщения    ws.poll()# Будет вызываться при установке соединенияfunc _on_ws_connection_established(_protocol):    show_message("Connection established!")# Будет вызываться при получении сообщений из вебсокетаfunc _on_ws_data_received():    # Обработка каждого пакета в очереди    for i in range(ws.get_peer(1).get_available_packet_count()):        # Сырые данные из пакета        var bytes = ws.get_peer(1).get_packet()        var sv_msg = GameProto.SvMessage.new()        # Превращение массива байтов в структурированное сообщение        sv_msg.from_bytes(bytes)        # Обрабатываем уже сконвертированное сообщение        _on_proto_msg_received(sv_msg)# Будет вызываться после чтения и конвертации сообщения из вебсокетаfunc _on_proto_msg_received(msg: GameProto.SvMessage):    # т.к. все эти поля находятся в блоке oneof - заполнено может быть только    # одно из них    if msg.has_connected():        pass    elif msg.has_client_connected():        pass    elif msg.has_client_disconnected():        pass    elif msg.has_chat_message():        pass    elif msg.has_name_changed():        pass    elif msg.has_result():        pass    else:        push_warning("Received unknown message: %s" % msg.to_string())

Важно периодически вызывать poll на WebSocketClient, иначе сигналы о входящих сообщениях никогда не придут. В данном случае это происходит в _process

После этого остается только заполнить логику обработки конкретных сообщений - но сначала добавим хранилище известных клиенту имён и переменную для ID текущего клиента:

# Хранит ID этого клиентаvar own_id: int# Хранит пары ID <> Имяvar names = Dictionary()

И обработку одного из возможных сообщений с сервера:

# Внутри _on_proto_msg_received  if msg.has_connected():var c = msg.get_connected()own_id = c.get_id()name_input.text = c.get_name()show_message("Welcome! Your ID is %d and your assigned name is '%s'." % [c.get_id(), c.get_name()])

Остальные блоки в этом if/elif примерно одинаковы. Получившийся код для каждого отдельного сообщения можно посмотреть на GitHub: Main.gd

Серверная часть

Серверная часть очень подробно разбираться не будет. Её можно написать на любом языке - и в данном случае это будет Kotlin с фреймворком Ktor. Напоминаю, что весь код этого проекта доступен на GitHub - сервер там достаточно простой. Но в двух словах выделю основные моменты моей

сервера:

Структура проекта

Основной gradle-проект состоит из двух модулей:

  • server - сам сервер;

  • proto - прото-файлы и сгенерированные из них биндинги:

    • Стоит обратить внимание на плагин com.google.protobuf, зависимость com.google.protobuf:protobuf-java и их конфигурацию;

    • В процессе сборки этого модуля генерируются классы, позволяющие сериализовывать/десериализовывать сообщения описанные в прото-файле.

Сам сервер работает по простому алгоритму - хранит открытые соединения, broadcast-канал для уведомлений и сообщений, принимает сообщения от клиента пока это возможно и отвечает на его запросы.

Результаты

Получившийся Godot-проект может работать как из браузера, так и с нативных сборок под Linux/Windows/Android и т.д. - всё взаимодействие клиента с сервером описывается в одном месте и в протокол легко вносить изменения.

Скриншоты

Нативный клиентНативный клиентWebSocket-клиентWebSocket-клиент

Заключение

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

  • Обработку ошибок (например, передавать отдельное сообщение error в ClMessageResult);

  • Обработку потери/восстановления соединения;

  • Многое другое.

Я надеюсь эта статья оказалась полезной и помогла разобраться в Godot, вебсокетах и protobuf.

Подробнее..

GRPC Dart, Сервис Клиент, напишем

18.06.2021 16:08:17 | Автор: admin

Привет! Меня зовуте Андрей и я работаю разработчиком Flutter.

Написание материала вызвано желанием показать пример создания сервиса c использованием технологии gRPC в экосистеме Dart и, соответственно, Flutter. Желание периодически возникает, когда приходится испытывать "боль", при переключении на проекты, в которых до сих пор применяется REST + JSON.

Планирую сделать серию из 3-4 статей.

Кратко о gRPC

gRPC (Remote Procedure Calls от Гугл) технология для создания информационных систем (сервисов и клиентских приложений).

Для сериализации данных и их передачи по сети, как правило, в связке с gRPC используется Protocol Buffers (Protobuf).

Protobuf применяется и как IDL (Interface Definition Language) для описания типов данных и вызываемых процедур.

Технология gRPC является достойной альтернативой широко распространённым подходам, при которых сетевые вызовы используют HTTP методы, а обмен данными происходит в формате JSON или XML.

Основные преимущества gRPC это:

  1. HTTP/2 в качестве транспорта

  2. Отсутствие привязок к HTTP-методам при взаимодействии компонентов системы

  3. Возможность использования Protocol Buffers (Protobuf) для сериализации / десериализации данных и их передачи по сети

  4. Protobuf IDL удобен для описания системы

  5. Нет необходимости вручную писать модели, сериализацию / десериализацию данных, интерфейсы вызовов процедур. Применяется кодогенерация для популярных языков программирования, в том числе и для Dart

Упрощённый пример gRPC системыУпрощённый пример gRPC системы

Пример написания сервиса и клиента

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

Подготовка среды разработки

Если на машине нет Dart SDK его нужно установить. Пример команды установки для Mac brew install dart, для Ubuntu 20.4 sudo apt install dart.

Проверить, что Dart успешно установлен dart --version.

Установить protobuf (пример для Mac brew install protobuf, пример для Ubuntu 20.4 sudo apt install -y protobuf-compiler).

Проверить, что все прошло успешно protoc --version.

Установить плагин для кодогенерации .proto файлов, описывающих систему, в Dart:

dart pub global activate protoc_plugin.

Pub устанавливает утилиты в $HOME/.pub-cache/bin.

Чтобы плагин был доступен из любой директории в вашем терминале, добавьте в его конфигурационный файл (.bashrc, .bash_profile, .zshrc и т.п.) строчку export PATH="$PATH":"$HOME/.pub-cache/bin" и перезагрузите терминал (или выполните команду source на обновленный файл).

Подготовка проекта

В качестве примера давайте сделаем сервис, который будет задавать "Клиентам" вопросы и получать от них ответы. Название пусть будет "Umka".

В выбранной папке создаем проект:

dart create umka

Перейдя в папку проекта добавим директорию protos/ и в неё файл umka.proto, в котором мы и опишем нашу систему:

mkdir protos && touch protos/umka.proto

Для исходного кода сделаем папку lib/ с файлами service.dart и client.dart:

mkdir lib && touch lib/service.dart lib/client.dart

Создадим также папку для сгенерированного кода:

mkdir lib/generated

В результате структура проекта выглядит следующим образом:

Добавление зависимостей

В качестве сторонней библиотеки нам пока потребуется только grpc. Остальные зависимости она "подтянет" сама.

Добавим её в pubspec.yaml и удалим из файла все лишнее:

Для загрузки из pub.dev репозитория библиотеки и её зависимостей в папке проекта выполним команду: dart pub get

Описание системы в с помощью IDL proto3

Опишем наш сервис Umka следующим образом:

Кому лень печатать, прогуляйтесь по ссылке на код.

В первой строчке обязательно нужно указать версию IDL syntax="proto3";.

Строки с 3 по 24 содержат описание типов передаваемых данных:

  • Ученик

  • Вопрос

  • Ответ

  • Оценка

Обратите внимание, что записи подобные string text = 2; выглядят как присваивание значения, но на самом деле это номера полей, которые используются для их идентификации в бинарном потоке данных при сериализации / десериализации.

Типы выглядят как в привычных языках программирования:

  • встроенные (Scalar Value Types) int32 и string

  • созданные Student, Question

В конце файла описан сам сервис, который содержит пока только два вызова:

  • получить вопрос

  • отправить ответ

Структура записи вызова rpc sendAnswer(Answer) returns(Evaluation) {} следующая:

  • sendAnswer - название удаленного вызова

  • Answer - тип запроса

  • Evaluation - тип ответа

Генерируем код сервиса на основе его описания в umka.proto

Для этого из папки проекта запустим в терминале команду:

protoc -I protos/ protos/umka.proto --dart_out=grpc:lib/generated

Разберем команду:

  • protoc утилита генерации (мы установили ее ранее)

  • -I protos/ указание расположения файлов .proto

  • protos/umka.proto файл описания сервиса

  • --dart_out=grpc:lib/generated grpc - указание плагина, lib/generated - директория для сгенерированного кода

В результате её выполнения в проекте появится 4 новых файла:

Это основа нашего сервиса.

Эмуляция работы с данными

Добавим в корень проекта папку с файлом db/questions_db.json со списком вопросов:

[    {        "id": 0,        "text": "7 x 5 = ?"    },    {        "id": 1,        "text": "12 x 13 = ?"    },    {        "id": 3,        "text": "2 ** 5 = ?"    },    {        "id": 4,        "text": "2 ** 10 = ?"    },    {        "id": 5,        "text": "2 ** 11 = ?"    }]

В папку lib добавим файл lib/questions_db_driver.dart с кодом для получения списка вопросов из нашей импровизированной базы данных:

import 'dart:io';import 'dart:convert';import 'generated/umka.pb.dart';final List<Question> questionsDb = _readDb();List<Question> _readDb() {  final jsonString = File('data/questions_db.json').readAsStringSync();  final List db = jsonDecode(jsonString);  return db      .map((entry) => Question()        ..id = entry['id']        ..text = entry['text'])      .toList();}

Пишем код для сервера

В файле lib/service.dart создадим класс UmkaService, расширив UmkaServiceBase, находящийся в сгенерированном файле lib/generated/umka.pbgrpc.dart:

class UmkaService extends UmkaServiceBase {}

Добавим реализацию одного из двух обязательных методов абстрактного родительского класса getQuestion, а для второго sendAnswer оставим пока заглушку TODO:

@overrideFuture<Question> getQuestion(ServiceCall call, Student request) async {  print('Received question request from: $request');  return questionsDb[Random().nextInt(questionsDb.length)];}@overrideFuture<Evaluation> sendAnswer(ServiceCall call, Answer request) {  // TODO: implement sendAnswer  throw UnimplementedError();}

Я намеренно оставил имя второго параметра обоих вызовов request - каждый удаленный вызов должен содержать объект запроса, соответствующий типу, описанному в файле umka.proto.

В этот же файл lib/service.dart добавим код запуска нашего сервиса на сервере:

class Server {  Future<void> run() async {    final server = grpc.Server([UmkaService()]);    await server.serve(port: 5555);    print('Serving on the port: ${server.port}');  }}Future<void> main() async {  await Server().run();}

Теперь наш сервис готов "служить клиентам на 5555 порту", отвечая пока только на один вызов getQuestion.

код сервиса

Пишем код клиентского приложения для терминала

Файл lib/client.dart

import 'package:grpc/grpc.dart';import 'generated/umka.pbgrpc.dart';class UmkaTerminalClient {  late final ClientChannel channel;  late final UmkaClient stub;  UmkaTerminalClient() {    channel = ClientChannel(      '127.0.0.1',      port: 5555,      options: ChannelOptions(credentials: ChannelCredentials.insecure()),    );    stub = UmkaClient(channel);  }  Future<Question> getQuestion(Student student) async {    final question = await stub.getQuestion(student);    print('Received question: $question');    return question;  }  Future<void> callService(Student student) async {    await getQuestion(student);    await channel.shutdown();  }}Future<void> main(List<String> args) async {  final clientApp = UmkaTerminalClient();  final student = Student()    ..id = 42    ..name = 'Alice Bobich';  await clientApp.callService(student);}

ClientChannel channel; является абстракцией сетевых вызовов по протоколу HTTP/2. Можно представить его как канал к виртуальному "gRPC endpoint".

stub - экземпляр "клиента" любезно сгенерированного нам утилитой protoc. Вызовы его методов фактически и являются RPC - удалёнными вызовами процедур.

В конструкторе мы инициализируем channel передав ему адрес localhost (для запуска локально), произвольный порт, и отключаем для простоты демонстрации "секьюрность".

Далее инициализируем stub передав ему созданный channel.

Метод запроса случайного вопроса getQuestion очень прост - вызываем соответствующий метод у нашего экземпляра stub, ждём пока вопрос не "прилетит", печатаем его и "возвращаем".

Метод callService в классе UmkaTerminalClient присутствует для демонстрации работы.

Также для запуска примера в файл client.dart добавлен метод main в котором "создаётся студент" и от его имени запрашивается вопрос у нашего сервиса./

Запускаем сервис

Для запуска сервиса на localhost из директории проекта выполним команду:

dart lib/service.dart

Стартуем клиентское приложение

Командой dart lib/client.dart в другом окне терминала из папки проекта запустим нашего "клиента", который создаст канал, установит соединение с сервисом, запросит случайный вопрос, получит его и разорвёт соединение, заглушив канал.

Заключение

На этом первая часть закончена, мы проделали отличную работу:

  • Подготовили среду разработки

  • Создали Dart проект

  • Добавили все необходимые зависимости

  • Описали нашу систему с помощью IDL proto3

  • Сгенерировали базовый Dart код системы утилитой protoc

  • Добавили "базу вопросов" и код для чтения из неё

  • Написали код для запуска сервиса на сервере

  • Создали терминального "клиента"

  • Запустили сервис на локальной машине и обратились к нему получив запрошенные данные

Далее можно "получать удовольствие" развивая наш сервис и клиентское приложение.

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

До встречи в следующей части!

Подробнее..
Категории: Dart , Flutter , Protobuf , Grpc , Дарт , Флаттер

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru