Перевод статьи начального уровня в блоге проекта Textile от 19 ноября 2019 г.
Первые шаги к созданию децентрализованного приложения могут быть трудными. Изменить привычный при разработке централизованных приложений образ мышления не легко, поскольку распределённый дизайн ломает большинство допущений, устоявшихся в наших мозгах (и программных инструментов, которые мы используем). Чтобы проиллюстрировать то, как наша команда представляет себе одноранговую коммуникацию, мы выпускаем серию из двух статей, посвященную стеку протоколов libp2p. В этой первой статье мы размышляем о некоторых сложностях, связанных с созданием децентрализованных приложений, и выясняем, как абстракция уровня сетевого протокола, такая как libp2p, может нам помочь. В следующей статье (которая скоро появится) мы будем использовать эти же концепции для создания простого примера, написанного на языке Go, чтобы понять, как различные компоненты, о которых мы здесь говорим, связаны друг с другом.
Стремительно нарастающая сложность
Внедрить распределённое (p2p) взаимодействие в какое бы то ни было приложение - задача не из простых. Попредставляйте лишь пару минут, как должно работать ваше творение - и всё начинает усложняться прямо на глазах. Рассмотрим две основные проблемы для p2p-приложений: состояние приложения и инфраструктура взаимодействия. Управление текущим состоянием системы не тривиально. Нет центрального органа, который бы его определял. Состояние системы - производное от состояний множества других узлов, которое имеет взрывную сложность в ненадежных сетях и сложных протоколах. Что касается инфраструктуры связи, ваше приложение должно взаимодействовать со многими равноправными узлами, поэтому вам придется столкнуться с изрядным количеством проблем. Поразмыслите на следующие темы касательно удалённых узлов:
У них ненадежное оборудование и сеть.
У них неопределённые технические параметры, такие, как вычислительная мощность и доступный объём долговременной памяти.
Брандмауэры могут блокировать их либо они могут находиться в сетях с NAT.
Они могут использовать старые версии приложений.
Прежде, чем вы начнёте писать свою первую строку кода, вы можете просто замёрзнуть из-за огромного объёма работы, которая вам предстоит. Даже если вам удастся выпустить первую версию приложения, - насколько легко затем будет что-то изменить без особых разрушений вокруг? Не правда ли, было бы неплохо иметь некую библиотеку, которая облегчила бы нашу задачу?!
Libp2p приходит на помощь!
Libp2p - библиотека, появившаяся в результате работы Protocol Labs над IPFS. Если вы хоть немного следите за нашим блогом, то знаете, что мы их большие поклонники! Когда вы берётесь за написание p2p-приложения производственного уровня с нуля, то понимаете, что создаёте не только приложение, но и его инфраструктуру - и до вас очень быстро доходит, что требуется дополнительно изобрести ещё кучу колёс. Не весело, однако. С другой стороны, libp2p позволяет вам встать на плечи неких не хилых гигантов, дабы уменьшить сложность инфраструктуры - и чтобы вы могли сосредоточиться на бизнес-логике. А вот это уже веселее! Конечно, libp2p - не панацея в борьбе со всеми нашими p2p-чудищами, но она определённо облегчает бремя реализации инфраструктуры взаимодействия.
Основные концепции Libp2p
Сердцевиной libp2p является объект Хост (Host), который представляет наш локальный узел в сети p2p. Общее описание его компонентов:
Идентификатор, по которому наш узел опознаётся другими узлами.
Набор локальных адресов, по которым к нам можно обращаться.
Журналы сведений о других узлах: об их идентификаторах, ключах, адресах и т. д.
Сетевой интерфейс для управления соединениями с другими узлами.
Muxer, который позволяет единичному соединению работать с несколькими протоколами (подробнее об этом позже).
Следующая базовая абстракция в libp2p - это Потоки. Поток (Stream) - это канал прямой связи с другим узлом. Важно понимать разницу между Stream и "грубым", "сырым" ("raw") сетевым протоколом, таким как TCP или UDP - ибо здесь открывается вся мощь libp2p: сами по себе сетевые протоколы - лишь средства отправки байтов по сети. Если вам нужна высокая надежность доставки пакетов, вы можете использовать TCP, в других случаях UDP может оказаться предпочтительнее. Короче, сами сетевые протоколы не заботятся о том, какие данные передают; для них это просто байты.
Stream же - это поточный канал связи между двумя одноранговыми узлами, имеющий определенную семантику. Тут уже не просто байты, но байты, соответствующие протоколу (более высокого уровня), определённому разработчиком. Протокол этот помечается идентификатором, например /sumtwointegers/v1.0.0. А сам Stream - диалог в рамках данного протокола. К примеру, одна сторона отправляет два целых числа, а другая отвечает их суммой. Вот что мы подразумеваем под потоком байтов с семантикой.
Потоки работают поверх сетевых протоколов, таких как TCP или UDP - по сути, идея состоит в том, чтобы отделить p2p-коммуникацию от сетевых протоколов (более низкого уровня). Нам нужно думать лишь о прикладном использовании поточного канала - для отправки значимой информации, а гибкость работы на любом сетевом протоколе, доступном между узлами, уже заложена в нём (в Stream). Это действительно здорово и мощно. И это также означает, что мы можем оптимизировать функционал раздельно на каждом уровне стека. Нужна лучшая реализация сетевого протокола? Отлично, оставьте это libp2p. Более выразительный семантически дизайн протокола p2p? Круто, это тоже!
Более того, отделение семантики p2p от базовых сетевых протоколов позволяет libp2p пойти дальше и мультиплексировать Потоки в одном и том же сетевом протоколе. Мы можем использовать одно и то же TCP-соединение для многих Потоков. Мы можем запустить протокол умножения двух целых чисел в том же соединении, в котором работали наши протоколы суммы двух целых чисел. Но Streams не обязаны справляться с этим самостоятельно. В действительности Libp2p полагается на мультиплексоры ("Muxers") для исполнения сей магии. Задача мультиплексора - разделить данные (последовательности байтов) на соответствующие потоки внутри одного потока сетевого протокола более низкого уровня. Вот краткая диаграмма, которая немного поясняет сказанное.
Как мы видели выше, у нашего Хоста есть Muxer, который мультиплексирует (объединяет) множество потоков одного узла в одном и том же соединении. Можно сказать, что Максер предваряет сообщения разных потоков некоторыми идентификаторами, чтобы принимающая сторона могла идентифицировать байты разных потоков. Конечно, используемый "нижележащий" сетевой протокол может ограничивать некоторые аспекты мультиплексирования, например, блокировку заголовка. Или же, напротив, сам транспорт (нижележащий сетевой протокол) может включать в себя полностью реализованный механизм мультиплексирования (как, например, QUIC). Но это уже другой разговор...
Возвращаясь на шаг назад
Давайте попробуем отделить то, о чем мы должны думать при написании приложения, от того, что libp2p делает для нас. Со своей стороны (разработчиков) мы хотим иметь ясное представление обо всех вещах, стараясь как можно больше отделить логику нашего приложения от концепций инфраструктуры. Нам следует думать о взаимодействии узлов, а не об адресах. И затем просто реализовать задуманное взаимодействие на уровне абстракции Stream.
На заднем плане libp2p выполняет тяжелую работу по соединению с удалённым узлом, используя информацию, хранящуюся в адресной книге. Она старается определить, какой сетевой протокол оба узла понимают, чтобы установить сетевое соединение. Ловко, однако!
Она, libp2p, может определить, что соединяться вновь не нужно, если у нас уже есть открытое соединение. Когда мы запускаем новый Поток в общении с неким узлом, мультиплексор выполняет рутинную работу по объединению его с другими существующими Потоками в уже установленном соединении.
Зачем так стремиться к множественному использованию одного соединения? Может статься, что наши p2p-приложения должны будут работать в очень ограниченных сетевых средах, где существуют брандмауэры, NAT и ограничения на количество подключений. Поэтому, после того, как мы установим соединение, мы должны выжать из него максимальную пользу! При переходе на образ мышления p2p, вы можете встретить много неизвестного, поэтому использование libp2p поможет вам сосредоточиться на хорошем дизайне, даже если вы не являетесь экспертом.
Но это ещё не всё!
Libp2p разрабатывалась как модульная библиотека. Такой дизайн упрощает изменения в реализации отдельных компонентов без влияния на остальные. Я думаю, что рациональность такого подхода - помимо хорошей инженерной практики - даёт подтверждение разнообразия сред, в которых будет работать наше приложение. И вы можете держать пари, что так оно и есть в приложениях p2p. Наша программа будет способна работать в самых разных средах выполнения, с разными возможностями или ограничениями сети. Кроме того, развитие вашего приложения при сохранении обратной совместимости со старыми версиями программы не является тривиальным делом.
По этой причине, когда два приложения libp2p впервые общаются друг с другом, большая часть их работы заключается в определении того, какой совместимостью они обладают, дабы достичь успеха в коммуникации. Надо выяснить, каким сетевым транспортом оба собеседника пользуются (TCP, UDP, QUIC, Websockets), какие версии протокола мы можем обрабатывать (/myprotocol/v1.0.0, /myprotocol/v1.1.0), какие реализации мультиплексора можем использовать, надо согласовать параметры безопасности, и т. д. И если этого было недостаточно, libp2p имеет ещё целый ряд встроенных протоколов для решения самых разных повседневных задач p2p-приложений, таких как:
Обход NAT: это больное место у p2p-приложений
Обнаружение узлов в сети: действительно, как вы обнаруживаете новые узлы вне централизованной модели?
Pubsub: наличие механизма публикации-подписки для отправки сообщений в нашем приложении без необходимости знать все существующие узлы и без зафлуживания сети
И многое, многое другое!
Небольшой бонус на заметку: Libp2p очень быстро растёт, поэтому вы можете ожидать появления новых мощных инструментов, которые можно будет использовать с небольшой добавкой собственного кода. Помните, что приложения p2p предполагают разнообразие, поэтому libp2p будет иметь в разных языковых реализациях свои особенности. Скажем, некоторые реализации мультиплексора могут быть недоступны в JS, но вполне себе - в Go или Rust.
Подведём итоги
Отныне есть надежда, что мы не будем обескуражены необходимостью вновь изобретать колеса в нашем приложении. Мы можем сосредоточиться на создании прикладного функционала, а libp2p доверить тянуть инфраструктурную рутину. Более того, дальнейшие усовершенствования в libp2p будут улучшать и наше приложение, не затрагивая его бизнес-логику.
Ну, пока что всё. Следите за обновлениями в нашем следующем «эпизоде», где мы переведем некоторые из этих концепций в код на практическом примере. Кроме того, если вам интересны такие вещи или вы хотите интегрировать p2p-коммуникацию в свое приложение или проект, свяжитесь с нами, и давайте поэкспериментируем вместе! Вы также можете попробовать одну из «облегченных» библиотек, если вам нужен простой способ использования однорангового узла libp2p в браузере, iOS, Android и/или на рабочем столе. Наконец, если вам нравятся такого типа обзорные статьи, сообщите нам об этом или запросите дополнительные темы ... а тем временем, удачного кодирования!
Автор оригинального текста: Ignacio Hagopian
Перевод: Алексей Силин (StarVer)