Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Перевод статьи начального уровня в блоге проекта 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 запускает обнаружение пиров, предоставляя нам их информацию. Логика у нас тут будет такая:
Если мы уже знаем об этом узле - ничего не делаем; мы уже играли в пинг-понг. Иначе же…
открываем поток нашего протокола SayPreferAddr, чтобы узнать у обнаруженного узла, по какому адресу (addr) он предпочитает играть в пинг-понг. Ну, и наконец…
добавляем адрес узла в нашу адресную книгу, закрываем текущее соединение с ним и запускаем наш второй протокол 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)