Bash: автообнаружение противника для сетевой игры на «Маках»

в 7:49, , рубрики: bash, macosx, ненормальное программирование, метки: ,

Bash: автообнаружение противника для сетевой игры на «Маках» Время от времени мне нравится писать на bash какие-нибудь не слишком тривиальные вещи. Вроде сетевых шахмат, о которых я уже рассказывал на «Хабре».

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

Сканировать вообще все адреса невозможно долго. Есть два некрасивых решения «в лоб» — просканировать текущую подсеть или посмотреть в таблицу ARP, подсоединиться к тем, кто там есть. Но, во-первых, такой перебор всё равно будет медленным, а во-вторых, не найдёт всех потенциальных соперников (соперники могут быть в других подсетях, а в таблице ARP вообще далеко не все участники сети).

Вообще, подобная проблема давным-давно решена в операционках — например, когда я настраивал дома сетевой принтер, операционная система нашла его сама, я не указывал IP или что-то ещё. В «Маках» для этого есть технология «Бонжур» (реализация «Зероконфа»).

Нельзя ли использовать эту технологию в «Баше»?

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

На «Маках» есть утилита dns-sd (или её более старая версия mDNS), которая занимается анонсированием сетевых сервисов и их обнаружением. Т.е. будучи запущенной с одними ключами она говорит «этот компьютер умеет то-то», с другими — ищет не умеет ли указанное какой-то компьютер в сети.

Вот как это происходит.

Сервисы указываются по имени, имя сервиса — юникодная строка (в формате UTF-8), длиной до 63 байт (не символов), задаётся в ввиде «_app-proto._tcp» или «_app-proto._udp», где строку (которая и является названием сервиса) «app-proto» нужно регистрировать на специальном сайте, но так как утилита принимает любое значение, то сойдёт и без этого шага.

В общем, если запустить dns-sd с ключом „-R“, то она зарегистрирует в сети новый сервис:

Новый сервис

который потом можно поискать в сети, запустив ту же команду с ключом „-B“:

Ищем созданный сервис

Как видно, кроме имени сервиса есть и другие параметры, но кроме строки «Привет!» там, собственно, ничего интересного, они ни на что не влияют в данной реализации. Вместо «привета» в эту строку можно записать что-то более полезное. Я записываю туда информацию об IP машины, анонсировавшей сервис и контрольную сумму передаваемого файла.

Немного кода.

Примерно так выглядит у меня в коде анонс сервера:

# эта функция сработает при выходе из скрипта и прибъёт работающую в фоне dns-sd
# так же она проверяет действительно ли прибиваемая программа — dns-sd, анонсирующая наш сервис 
function _ClearServer {
    ps -f | awk"$2==$1 && /_bolk-fileshare._tcp/ { print $2 }" | xargs kill
}

dns-sd -R "$myownip $checksum" "_bolk-fileshare._tcp" . 1 >/dev/null &
trap "_ClearServer $!" EXIT

Как видно, я в фоне запускаю анонс сервиса, а на выходе из утилиты прибиваю работающий в фоне dns-sd. Если я бы не использовал запуск в фоне, то выполнение моего скрипта остановилось на этом месте — dns-sd после запуска не возвращает управления и при выходе удаляет сервис из анонсированных.

Теперь обнаружение сервиса. В этом режиме dns-sd похожа на утилиту top — консоль так же не отпускается, события о анонсируемых и деанонсируемых сервисах появляются на экране. Поэтому мне пришлось использовать очень полезную в таких случаях утилиту expect:

    local info=($(expect <<CMDS | awk 'NR>2 {print $7 " " $8}' | sort -u | tr -d 'r' | head -n1
        spawn -noecho dns-sd -B _bolk-fileshare._tcp
        expect Timestamp
        expect -- "_bolk-fileshare._tcp"
        exit
CMDS))

Смысл тут такой: я прошу expect вернуть мне управление, когда dns-sd выведет строку с названием моего сервиса. После чего я выделяю нужное мне — ту самую строку, где у меня передаются IP-адрес сервера и контрольная сумма передаваемого файла. В переменной info у меня получится массив из двух элементов — суммы и IP.

Остальное не очень интересно, на «ГитХабе» лежит получившийся код — дальше я просто соединяюсь по указанном адресу и получаю файл утилитой netcat.

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

Автор: bolk

* - обязательные к заполнению поля