- PVSM.RU - https://www.pvsm.ru -
Недавно в разговоре с коллегами обсуждали различные игры жанра RTS, и я задумался, почему же релиз третьих «Казаков» прошёл мимо меня. Пару минут и один поисковый запрос спустя я вспомнил — помимо крайне сырого раннего релиза, реинкарнация этой классической стратегии отличилась невозможностью многопользовательской игры без постоянного соединения с официальным сервером. Многочисленные просьбы игроков «добавить LAN» на форумах разной степени свежести намекают, что изменений ждать не стоит.
Что ж, если гора не идёт к Магомету…
TL;DR: Сервер, инструкция по применению и исходный код доступны на GitHub [1].
Для того, чтобы понять общую структуру протокола, вполне достаточно первых четырёх пакетов, подслушанных между клиентом игры и официальным сервером. Итак, мы имеем:
Заголовок пакета всегда занимает ровно 14 байт и содержит размер полезной нагрузки (1), командный код (2) и два идентификатора игроков для адресации пакетов (3,4). Возьмём простой пример — личное сообщение в чате игрового лобби:
Здесь также видно, что строкам предшествует их длина (5). Примечательно то, что в зависимости от конкретного командного кода, формат данных различается и размер строк указывается то одним, то двумя, а то и четырьмя байтами.
Рассмотрим публичное оповещение о создании новой игровой комнаты:
Заголовок так же начинается с размера, команды и отправителя (1,2,3), а вот идентификатор получателя отсутствует (4). В данном сообщении это значит «отправить всем» но для других команд может означать и «всем в своей комнате». Первая строка (5) содержит название, пароль и информацию о типе игры и версии клиента у создателя (хоста) комнаты. Для разделения значений используется знак табуляции t, он же 09h. Затем следует строка с информацией о комнате (6), которая требуется для её отображения в списке. Она содержит статус, количество живых игроков, компьютерных противников, закрытых слотов и ещё два значения. Здесь роль разделителя играет вертикальная черта. После неё следуют две константы размером по 4 байта (далее — число типа Int), затем строка с именем хоста [4] компьютера создателя комнаты (7).
А теперь перейдём к более интересным моментам.
Изначально я анализировал пакеты в Wireshark [6] с фильтрами [7] tcp && data, чтобы перед глазами не мелькали «пустые» пакеты типа ACK [8]. В какой-то момент мне это вышло боком: выяснилось, что Wireshark ошибочно принимает пакеты, полезная нагрузка TCP которых начинается с байтов 05h 00h, за пакеты протокола DCERPC [9]. В частности, это коснулось пакетов с оповещением о входе игрока в комнату, т.к. они всегда содержат ровно пять байтов после заголовка. Это приводит к тому, что Wireshark помечает нагрузку TCP не как Data, а как [Malformed Packet: DCERPC] и скрывает пакет:
Правильным в данном случае было бы применение более нового фильтра tcp.payload. Он отображает все пакеты TCP с полезной нагрузкой, вне зависимости от того, как Wireshark эту нагрузку интерпретирует.
При реверсе сетевого протокола было очевидно, что над ним в разное время работали разные люди, имеющие различные приоритеты. Можно выделить три вида строк, передающих переменные:
ps=1337|pw=42|pg=12
"MyRoom"t"secret"t0AFFE
Последний пример взят из пакета, который регулирует транзит роли хоста на одного из игроков при выходе создателя комнаты из игры. Насколько я понимаю, этот функционал дорабатывали после релиза игры. Очевидно, что тогда вопрос об эффективности передачи данных уже не стоял так остро, как на момент написания сетевого протокола в оригинальном движке.
В конце многих пакетов полезная нагрузка завершается четырьмя или шестью нулевыми байтами. Но в определённых пакетах (в частности, с командными кодами 0xc8 и 0x19d) среди них иногда неожиданно всплывают данные. Я никак не мог понять, откуда они берутся и зачем нужны, пока в одном из таких пакетов не обнаружил в них фрагмент одного из сообщений в чате лобби.
По всей видимости, официальный сервер не всегда обнуляет буфер, в который пишет ответный пакет перед отправкой, и в заключающих байтах могут затеряться остатки прошлых пакетов. К счастью, в самом клиенте игры это не приводит к ошибкам. Но кое-что о темпах разработки и уровне контроля качества это всё же говорит.
…или «чем хуже, тем лучше [12]». После набора критической массы знаний о используемом протоколе, объём исходного кода перестал расти и начал таять. Интерфейсы упрощались, а данные, которые сервер вынужден сохранять (например, для передачи состояния лобби новоприбывшим игрокам), перестали анализироваться и стали храниться в виде строк. Не обязательно знать происхождение и назначение каждого байта в пакете; достаточно понимать, как и кому его перенаправить.
На большинство запросов сервер отвечает данными, полученными от других клиентов ранее, или же провоцирует нужного клиента специальным пакетом и перенаправляет ответ запросившему. Где возможно, я опускал необязательные строки, заменяя их нулевым байтом. В первую очередь это касается данных о рейтинге игроков, количестве очков и побед.
В отдельных случаях слишком широкая ретрансляция пакетов даже порождала ошибки на стороне клиента. В частности, я заметил это на паре «запрос — ответ» с командными кодами 0x1b3 и 0x1b4, дублирующими информацию об очках игрока и системе клиента.
Отдельную сложность для меня представил аспект запуска и синхронизации игры. Разобравшись с ретрансляцией пакетов при загрузке, я заметил, что во время игры все клиенты посылают пакеты с командным кодом 0х4b0. При этом в одном поле идентификатора стоит номер создателя комнаты, а второе поле пустует. Но если подходить логически и понимать это как «клиент — всем в указанной комнате», то возникает рассинхронизация игры.
Вместо этого сервер должен сам следить за источником пакета и проверять, хост это или обычный игрок. В первом случае пакет отправляется всем игрокам в комнате кроме самого хоста, в последнем — исключительно хосту. При этом пакет с игровой командой, видимо, вносится в очередь происходящего и затем рассылается обратно уже от хоста, но в контексте всех остальных событий игры. Это гарантирует одинаковый порядок исполнения команд у всех игроков. Хост рассылает игровые пакеты постоянно и в определённом такте, вне зависимости от количества событий, а все остальные — только при действиях со стороны игрока.
Кстати, именно поэтому при смене хоста во время игры показывается предупреждение о том, что ваши приказы могут быть потеряны — если старый хост не успел внести их в общую очередь перед отключением, то у нового хоста нет никакой возможности узнать об этих командах.
Вопреки общественному мнению, в клиенте всё же присутствуют функции, необходимые для полноценной игры в локальной сети. При анализе сетевого трафика я заметил, что среди многочисленных пакетов TCP создатель комнаты также рассылает уведомления о количестве игроков и статусе комнаты через UDP.
Помимо этого, при дизассемблировании клиента я наткнулся на функцию, которая вызывается при возникновении исключений и отображает текст ошибки. Пройдясь по её вызовам и анализируя передаваемый ей текст можно определить настоящие названия тех процедур, в которых возникают исключения. Немного удивившись результату, я поигрался с strings [13],
function LanPublicServerGetRegIDFrom
function LanPublicServerGetRegIDTo
function LanPublicServerGetRegMessage
function LanPublicServerGetClientTeamByIndex
function LanPublicServerGetClientTeamByClientID
function LanPublicServerGetClientSpecByIndex
function LanPublicServerGetClientSpecByClientID
function LanPublicServerGetClientInfoToParserByIndex
function LanPublicServerGetClientInfoToParserByClientID
function LanPublicServerGetSessionInfoToParserByIndex
function LanPublicServerGetSessionInfoToParserByClientID
function LanPublicServerGetClientsCount
function LanPublicServerGetSessionsCount
function LanPublicServerGetClientIndexByClientID
function LanPublicServerGetClientIndexByClientNick
function LanPublicServerGetSessionIndexByClientID
function LanPublicServerProfScore
function LanPublicServerProfCountry
function LanPublicServerProfGamesPlayed
function LanPublicServerProfGamesWin
function LanPublicServerProfLastGameTime
function LanPublicServerProfInfo
function LanMyInfoHost
function LanMyInfoIP
function LanMyInfoID
function LanMyInfoSpec
function LanMyInfoName
function LanMyInfoPlayer
function LanGetServerInfoToParser
function LanIpToString
function LanIpToInt
function LanGetClientsCount
function LanGetClientIDByIndex
function LanGetClientHostByIndex
function LanGetClientNameByIndex
function LanGetClientSpecByIndex
function LanGetClientIndexByID
function LanGetClientPlayerNameByIndex
function LanSelectParser
function LanGetParserID
function LanGetSendDataThreadCount
function LanGetSendDataThreadEnabled
function LanGetNoDelayOption
function LanGetOptimizedPackage
function LanGetOptimizedPackageDef
Тут два варианта: либо это у разработчиков юмор такой, либо они повсеместно использовали аббревиатуру LAN для всего, что связано с многопользовательской игрой.
Так как я изначально ставил для себя цель написать кроссплатформенный сервер и расширить познания в сетевом программировании, то для реализации я выбрал язык C++ и библиотеку Asio [14]. Последняя также позволила мне отказаться от многопоточности и связанных с ней особенностей [15] доступа к данным в пользу асинхронности и более простого кода. За основу мною был взят исходный код одного из примеров в репозитории [16] библиотеки.
Наиболее интересным для меня аспектом разработки стала проблема доступности буфера данных во время асинхронной отправки пакетов. В это же время я стремился минимизировать количество аллокаций и копирования данных в памяти. В дополнение ко всему, сервер должен иметь довольно большой буфер для приёма пакетов, т.к. размер полезной нагрузки TCP при передаче данных о карте перед стартом игры может превышать 800 килобайт.
В итоге я реализовал процесс чтения, создания и отправления пакетов следующим образом:
Как ни суди, а добавление в стандарт языка C++ лямбда-выражений, разнообразных контейнеров и умных указателей значительно снизило сложность проектирования подобных систем; при написании сервера ни одной ноги прострелено не было.
Закончив работу над сервером и опробовав основные функции, я решил проверить стабильность [20] сервера и синхронизацию после транзита хоста спустя длительное время. Для этого я создал в локальной сети комнату с тремя игроками и с пятью сложнейшими ИИ. Для теста я выбрал максимальные параметры касательно размера карты, количества месторождений, населения и времени ненападения. Я запустил игру и через пару минут остановил в диспетчере задач процесс игры на компьютере хоста, чтобы симулировать неожиданное отключение. Затем два оставшихся клиента были предоставлены самим себе на всю ночь.
Опытные игроки, наверное, уже догадались, что было дальше. Если говорить коротко, то сервер прошёл тест, а вот клиент — нет. На следующий день экран приветствовал меня застывшим около восьми часов игровым таймером, а так же несколькими сообщениями об ошибках, среди которых было и всем знакомое [21] «Out of memory». Всё логично, два оставшихся ИИ разделили карту пополам и сражались многотысячными армиями. Процесс игры занял около 3,5 ГБ оперативной памяти и упёрся в свои 32-битные границы [22]. Сервер же, в свою очередь, продолжал работать на своих 11 МБ оперативной памяти.
Надеюсь, вам было интересно проследить за процессом копирования «чёрного ящика [23]» официального сервера. Также я надеюсь, что разработчики игры сжалятся над игроками и наконец-то добавят возможность многопользовательской игры в локальной сети, с блэкджеком и с быстрым UDP и без необходимости иметь низкий пинг до недавних событий [24].
Если есть вопросы или предложения касательно реверса или исходного кода [25] сервера — добро пожаловать в комментарии. До новых встреч!
Автор: Ereb
Источник [26]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/278572
Ссылки в тексте:
[1] GitHub: https://github.com/ereb-thanatos/cossacks3-lan-server
[2] обфускации: https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%84%D1%83%D1%81%D0%BA%D0%B0%D1%86%D0%B8%D1%8F_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%BD%D0%BE%D0%B5_%D0%BE%D0%B1%D0%B5%D1%81%D0%BF%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5)
[3] порядке от младшего к старшему: https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D0%BA_%D0%B1%D0%B0%D0%B9%D1%82%D0%BE%D0%B2#%D0%9F%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D0%BA_%D0%BE%D1%82_%D0%BC%D0%BB%D0%B0%D0%B4%D1%88%D0%B5%D0%B3%D0%BE_%D0%BA_%D1%81%D1%82%D0%B0%D1%80%D1%88%D0%B5%D0%BC%D1%83
[4] именем хоста: http://ru.smart-ip.net/what-is-a-hostname
[5] У „Казаков“ секретов нет: https://habrahabr.ru/post/318870/
[6] Wireshark: https://www.wireshark.org/
[7] фильтрами: https://www.wireshark.org/docs/dfref/t/tcp.html
[8] ACK: https://ru.wikipedia.org/wiki/Transmission_Control_Protocol#%D0%9D%D0%BE%D0%BC%D0%B5%D1%80_%D0%BF%D0%BE%D0%B4%D1%82%D0%B2%D0%B5%D1%80%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D1%8F
[9] DCERPC: https://ru.wikipedia.org/wiki/DCE/RPC
[10] ассоциативного массива: https://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2
[11] комментариях: https://github.com/ereb-thanatos/cossacks3-lan-server/blob/master/src/Lobby.cpp#L635
[12] чем хуже, тем лучше: https://ru.wikipedia.org/wiki/%D0%A7%D0%B5%D0%BC_%D1%85%D1%83%D0%B6%D0%B5,_%D1%82%D0%B5%D0%BC_%D0%BB%D1%83%D1%87%D1%88%D0%B5
[13] strings: https://ru.wikipedia.org/wiki/Strings
[14] Asio: https://think-async.com/
[15] особенностей: https://ru.wikipedia.org/wiki/%D0%9C%D1%8C%D1%8E%D1%82%D0%B5%D0%BA%D1%81
[16] репозитории: https://github.com/chriskohlhoff/asio/tree/master/asio/src/examples
[17] Session: https://github.com/ereb-thanatos/cossacks3-lan-server/blob/master/src/Session.hpp
[18] Packet: https://github.com/ereb-thanatos/cossacks3-lan-server/blob/master/src/Packet.hpp
[19] за один раз: http://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared
[20] проверить стабильность: https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BE%D1%87%D0%BD%D0%BE%D0%B5_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5
[21] всем знакомое: https://www.google.de/search?q=%5B%D0%BA%D0%B0%D0%B7%D0%B0%D0%BA%D0%B8%7Ccossacks%5D+3+out+of+memory
[22] границы: https://ru.wikipedia.org/wiki/32_%D0%B1%D0%B8%D1%82%D0%B0
[23] чёрного ящика: https://ru.wikipedia.org/wiki/%D0%A7%D1%91%D1%80%D0%BD%D1%8B%D0%B9_%D1%8F%D1%89%D0%B8%D0%BA
[24] недавних событий: https://habrahabr.ru/post/353822/
[25] исходного кода: https://github.com/ereb-thanatos/cossacks3-lan-server/
[26] Источник: https://habrahabr.ru/post/353678/?utm_campaign=353678
Нажмите здесь для печати.