Перевод статьи начального уровня в блоге проекта Textile от 12 декабря 2019 г.

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

В сегодняшнем посте мы пойдем немного дальше и представим «игрушечное» приложение, чтобы почувствовать, как на самом деле можно что-то разрабатывать с помощью libp2p, и, надеюсь, мотивировать вас создать собственное p2p-приложение. Серьезно, вы удивитесь, насколько это просто!

Приложение

Сразу оговоримся, наша программа нынче будет написана на языке Go, с использованием библиотеки go-libp2p. Если вы ещё не знакомы с этим языком, настоятельно рекомендуем ознакомиться. Он действительно хорош для приложений, имеющих дело с параллелизмом и сетевыми взаимодействиями (такими, как например, обработка множества p2p-соединений). Большинство библиотек IPFS/libp2p имеют свои базовые реализации, написанные на Go. Прекрасным введением в Go является тур на golang.org.

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

  • По умолчанию приложение цепляется к свободному TCP-порту.

  • Если указан флаг quic, оно также подключится к прослушиваемому порту QUIC, который станет предпочтительным адресом узла для игры в пинг-понг.

  • Узел будет использовать службу mDNS для обнаружения новых узлов в локальной сети.

  • На каждом вновь обнаруженном узле (скажем, узле A) наше приложение будет запускать собственный протокол sayMyAddr (мы его реализуем), который будет узнавать для нас предпочтительный адрес для игры в пинг-понг этого узла.

  • Мы подключаемся к узлу А, используя предпочитаемый им адрес - и запускаем «танец» Пинг-Понг. Другими словами, мы запустим ещё один наш самопальный протокол, посредством которого отправим сообщение Ping, а узел A ответит нам сообщением Pong. Круть!

Даже для такой простой системы (если мы хотим сделать p2p-приложение) потребуется принять ряд отдельных решений. Для начала, надо будет ответить на следующие вопросы:

  • Какой транспортный протокол (TCP, QUIC и т.п.) использовать?

  • Какой механизм обнаружения других узлов в сети (например, mDNS) применить - то есть, как мы узнаем о других узлах, использующих наше приложение?

  • Как наши собственные протоколы (Streams) будут работать? - то есть, как мы будем поддерживать двунаправленную связь с другими узлами?

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

Ныряем в код!

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

КОД: ВЫДЕЛИТЬ ВСЁ

git clone git@github.com:textileio/go-libp2p-primer-article.git
cd go-libp2p-primer-article
code . // Нам нравится VSCode, ну а вы - сами с усами ;)

Далее: начнём с main.go, где вы можете лицезреть, как создаётся и запускается хост libp2p. Дополнительно здесь мы указываем, какие сетевые транспортные протоколы будет поддерживать наш хост. Заметьте, что если для флага -quic установлено значение true, мы добавляем новую привязку для транспорта QUIC. Добавление в работу транспортных протоколов сводится к простому добавлению параметров в конструктор хоста! Также обратите внимание, что мы регистрируем здесь все обработчики наших собственных протоколов: RegisterSayPreferAddr и RegisterPingPong. Наконец, мы регистрируем встроенную службу mDNS.

Теперь заглянем в discovery.go, где у нас находится настройка mDNS. Здесь, по сути, надо определить частоту широковещательной рассылки mDNS и строковый идентификатор, который в нашем случае не требуется и потому пустой. Последний шаг здесь - регистрация обработки уведомления discovery.Notifee, которая будет вызываться всякий раз, когда mDNS запускает обнаружение пиров, предоставляя нам их информацию. Логика у нас тут будет такая:

  1. Если мы уже знаем об этом узле - ничего не делаем; мы уже играли в пинг-понг. Иначе же…

  2. открываем поток нашего протокола SayPreferAddr, чтобы узнать у обнаруженного узла, по какому адресу (addr) он предпочитает играть в пинг-понг. Ну, и наконец…

  3. добавляем адрес узла в нашу адресную книгу, закрываем текущее соединение с ним и запускаем наш второй протокол PingPong, который повторно подключится к узлу, используя его предпочитаемый адрес (который мы добавили на предыдущем шаге).

Наконец, в pingpong.go мы можем увидеть упомянутый ранее метод RegisterPingPong, вызываемый из main.go, и еще два метода:

  • Handler: этот метод будет вызываться, когда сторонний узел зовёт нас играть в PingPong. Вы можете думать о Handler как об обработчике HTTP REST. В этом обработчике мы получаем Stream, реализующий io.ReadWriteCloser, из которого мы можем запускать наш протокол для отправки и получения информации, чтобы делать что-то полезное.

  • playPingPong: Это другая сторона медали; клиент запускает новый Stream для внешнего узла для запуска протокола PingPong.

Как видите, реализация своих протоколов довольно-таки проста и полностью абстрагирована от других, инфраструктурных задач. Единственное, о чем нам нужно позаботиться, так это о написании полезного для нашего проекта прикладного кода. Заметьте также, что добавление нового протокола, например, в saymyaddr.go, очень похоже на pingpong.go.

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

Чтобы протестировать нашу демо-программу, можно открыть два терминала и просто запустить: go run * .go , go run * .go -quic или их комбинации. Ниже вы можете видеть иллюстрацию с двумя терминалами, работающими с флагом -quic:

Обратите внимание, как, сразу после запуска, узел в нижнем терминале обнаруживает узел в верхнем, ибо mDNS немедленно находит существующие узлы. Затем "нижний" сразу переходит к игре в пинг-понг. "Верхний" узел тоже, но с определённой задержкой (из-за 5-секундного интервала, который мы установили для нашей службы mDNS) обнаружит "нижний" собственными средствами, что, в свою очередь, вызовет новую игру в пинг-понг.

Заметим также, что когда каждая из сторон отправляет сообщение PingPong или отвечает на него, она выдает полную информацию о мульти-адресе (multiaddr), на который обращается, где можно увидеть, что используется протокол QUIC. Попробуйте запустить этот пример без флага -quic для обоих партнеров и посмотрите, как это повлияет на результат!

И ещё отметим для себя, что если запустить один терминал с флагом -quic, а другой - без него, последний партнер не сможет играть в PingPong с первым, потому что у него не включена поддержка QUIC. В более реалистичном сценарии вы могли бы использовать все доступные вам адреса узла-собеседника, чтобы иметь больше возможностей общаться в рамках базового транспортного протокола, который оба понимают. Изящно, не правда ли?

Что дальше?

Важным качеством приложения является его пригодность для дальнейшего развития. В p2p-проектах в основе прикладной логики находится сетевое взаимодействие. Если однажды в будущем мы захотим модернизировать наш протокол PingPong добавлением новых функций или возможностей, мы должны учитывать, что некоторые узлы будут по-прежнему работать со старой версией протокола! Это звучит как ночной кошмар, однако отставить бояться, мы с этим справились. И тут надо приметить следующий фрагмент кода из pingpong.go:

КОД: ВЫДЕЛИТЬ ВСЁ

const (
    protoPingPong = "/pingpong/1.0.0"
)
...
func RegisterPingPong(h host.Host) {
    pp := &pingPong{host: h}
    // Здесь мы регистрируем наш _pingpong_ протокол.
    // В будущем, если решите достраивать/исправлять ваш протокол,
    // вы можете либо делать текущую версию обратно совместимой,
    // либо зарегистрировать новый обработчик, 
    // с указанием новой главной версии протокола.
    // Если хотите, можете также использовать логику semver,
    // см. здесь: http://bit.ly/2YaJsJr
    h.SetStreamHandler(protoPingPong, pp.Handler)
}

Комментарии прекрасно всё объясняют.

Другой важный вопрос связан с механизмом обнаружения других узлов, в нашем случае это mDNS. Этот протокол делает свою работу в локальных сетях, но как насчет обнаружения пиров в Интернете? Позднее вы можете добавить в своё приложение Kademlia DHT или использовать один из механизмов pubsub - также, чтобы находить новые узлы.

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

Заключительные слова

Libp2p имеет множество встроенных инструментов для решения большинства сложных проблем, с которыми вы можете столкнуться в p2p-системах. Рекомендуем вам заглянуть в раздел реализаций на официальной веб-странице libp2p, чтобы узнать, что уже доступно и что ещё в работе. Всё быстро меняется, поэтому неплохо быть в курсе всего самого нового и лучшего.

Важно: имейте также в виду, если вы используете libp2p с включенными Go-модулями, вам нужно явно указывать тег версии в go get, поскольку иначе вы можете получить не то, что ожидали по умолчанию. Больше информации об этом вы можете найти в секции Usage readme-файла go-libp2p.

Надеемся, что вам понравился наш игрушечный проект, и надеемся, что он вселит в вас уверенность в том, что писать p2p-приложения не так сложно, как могло бы показаться! На самом-то деле, это может быть довольно кайфово и вдохновляюще! Если вам по нраву сей материал, присоединяйтесь к нам на нашем канале в Slack, чтобы пообщаться на тему p2p-протоколов, или подписывайтесь на нас в Twitter, чтобы узнавать о самых свежих и славных разработках Textile. Ну, или хотя бы гляньте некоторые другие наши статьи и демонстрации, чтобы полнее прочувствовать, что возможно в этом захватывающем мире приложений P2P!

Автор оригинального текста: Ignacio Hagopian

Перевод: Алексей Силин (StarVer)