Алгоритм обновления игры
- Проверка версии на необходимость обновления.
- Скачивание списка файлов актуальной версии.
- Скачивание новых или изменённых файлов во временную папку.
- Установка обновления — приведение файлов установленного клиента в соответствие со списком.
- Запуск обновлённого клиента.
Проверка версии
Первым делом при запуске клиент спрашивает у сервера номер актуальной версии (X) и номер минимально допустимой без обновления (Y). Если версия клиента не ниже Y, то обновление не требуется, в противном случае клиент запускает утилиту обновления "GetNewVersion.exe X", а сам завершает работу.
Как видим, номер версии передаётся параметром — это позволяет при желании обновить игру до любой доступной на сервере версии, и даже понизить её. Если параметр не передать — утилита сама запросит у сервера номер актуальной версии. Номер версии — это просто целое число, схема нумерации может быть любой, например у меня версия 1.12 соответствует номеру 1120.
Ответ от сервера не приходит мгновенно, а до его получения мы не можем создать окно игры, ведь возможно придётся его тут же закрыть, а непонятные мерцания на экране — это совсем не то, что нам нужно. Время ожидания ответа надо бы чем-то занять, и клиент занимает его загрузкой/распаковкой наиболее тяжелых JPEG'ов. Слишком долго ждать тоже нельзя: игрок запустил игру — а на экране ничего не происходит, непорядок. Поэтому если в течение 1.0 сек. ответ от сервера так и не поступил — загрузка игры продолжается в обычном порядке. В этом нет ничего страшного: как только игрок попытается залогиниться на сервер, он получит сообщение о необходимости обновить клиент, либо о том, что сервер недоступен.
Скачивание списка файлов
Зная номер версии, утилита обновления скачивает список файлов по адресу:
[base_ur]>/[версия]/filelist
Это просто список файлов в формате CSV с указанием контрольных сумм, а также размеров в сжатом и несжатом виде, каждая строчка выглядит в нём примерно так:
18*Priest.tga;1053151921D9;91719;107372
Здесь «18*» означает, что 18 символов в имени файла такие же как и у предыдущего файла. Поскольку файлы обычно идут в алфафитном порядке, а пути могут быть длинными — это существенно экономит размер файла-списка. Для веб-сервера, на котором не включена компрессия, это означает, что файл скачается быстрее и обновление начнётся раньше.
Скачивание новых или изменённых файлов
Мы не знаем насколько устарел клиент игры, возможно какие-то файлы изменены или удалены вручную. Скачивать лишнее мы тоже не хотим, поэтому получив список файлов, утилита начинает проверять их по порядку на необходимость обновления: если в папке игры файл отсутствует или его контрольная сумма отличается — файл добавляется в очередь на скачивание. Параллельно может загружаться не более 2-х файлов — этого вполне достаточно, чтобы с одной стороны загрузка не тормозила, а с другой, происходила последовательно.
Особая тема — отображение прогресса. Пока не обработан весь список, мы точно не знаем сколько файлов предстоит скачать и какого они размера. Однако как только первый файл поставлен на загрузку, мы уже можем отобразить какую-то информацию. Фактически, прогресс отображает очередь загрузки: сколько всего предстоит скачать и сколько уже скачано.
Скачанные файлы сразу же распаковываются и сохраняются во временной папке, для сжатия я использую библиотеку
zlib
.Когда весь список файлов обработан и все загрузки завершились, утилита проверяет наличие файла
changes.txt
и если он есть — отображает его. Пользователю предлагается начать процедуру обновления. До нажатия кнопки «Update» никаких изменений в папке игры ещё не сделано, так что можно без проблем отказаться. Кстати, если пользователь прервёт загрузку или откажется от установки, то в следующий раз ему не придётся скачивать все файлы заново: перед скачиванием очередного файла утилита проверяет его наличие во временной папке и если контрольная сумма совпадает — загрузка считается состоявшейся.
А вот при нажатии на «Update» утилита запускает другую утилиту — "InstallUpdate.exe", а сама завершает работу.
Установка обновления
Зачем нужна ещё одна утилита? Всё просто: для обновления файлов игры нужно выполняться с правами администратора. А для скачивания обновления это, наоборот, противопоказано. Потому что, если только вы не счастливый обладатель EV-сертификата подписи кода, запуск процесса с правами администратора приводит к показу окна UAC. А если при запуске игры, вместо привычного интерфейса игрок видит такое:
… то это, как минимум, повод насторожиться, а то и вовсе отказаться от запуска. Другое дело, при ручном согласии на установку обновления — в таком контексте окно UAC воспринимается нормально. К сожалению, процесс в Windows не может повысить свои права во время выполнения — это свойство неизменно с момента запуска. Поэтому я использую два отдельных файла. На самом деле
GetNewVersion.exe
и InstallUpdate.exe
— это и вовсе одна и та же утилита, файлы идентичны. А действие определяется передаваемыми параметрами и именем исполняемого файла.Итак, будучи запущенным, InstallUpdate копирует файлы клиента игры из временной папки в папку игры, а затем запускает обновлённый клиент и завершает работу. При этом может быть обновлён и файл
GetNewVersion.exe
.Все действия, а также возникающие ошибки, подробно логируются в журнале, это весьма полезно для отладки.
Процесс подготовки новой версии
Мы рассмотрели схему работы обновления с точки зрения клиента игры, но как заставить всё это работать? Для подготовки новых билдов я написал ещё одну утилиту — CompressBuild. Она рекурсивно сканирует папку, сжимает файлы методом Deflate, а информацию о них заносит в список файлов —
filelist
. После сжатия к имени файла дописывается символ "_". Сжатые файлы повторно не сжимаются, поэтому при необходимости в папке билда можно обновить лишь отдельные файлы, CompressBuild обновит только их.Некоторые файлы в клиенте игры изменяются в процессе работы, например, содержат настройки. Такие файлы нужно игнорировать, соответствующие шаблоны утилита берет из файла exclude. То есть эти файлы просто не попадают в
filelist
и не портятся на клиенте при обновлении.Таким образом, чтобы подготовить новый билд, мне нужно:
1. Скопировать папку
\master
в папку \[номер_версии]
2. Запустить CompressBuild, который запакует в ней файлы и составит их список.
3. Закачать всё это на сайт игры.
4. Изменить на игровом сервере номер актуальной версии на номер только что закачанной. Вуаля!
С этого момента при обновлении люди будут получать новую версию.
Ну а папки со старыми билдами на сервере можно удалить, чтобы не занимали место.
Заключение
Конечно, моя система обновления не идеальна и не лишена недостатков. Например, если в клиенте какой-то файл был удалён — у игроков он останется. Если файл был переименован — он будет загружен как новый, а старый экземпляр не будет удалён. Можно, конечно, доработать утилиту обновления, добавив в список файлов команды для удаления/переименования файлов, но вообще такие проблемы для моей игры неактуальны, так что я не стал заморачиваться.
Ну а исходники можно взять тут: astralheroes.com/files/UpdaterSrc.zip
(компилируется в Delphi-2006 / Turbo Delphi, за другие компиляторы не ручаюсь).
Комментарии (25)
Fox_exe
13.09.2018 19:14Я както натыкался на «обновлялку», внутри которой был завернут GIT-клиент… Это было эпично, но работало великолепно! (Переименованные файлы именно переименовывались, удаленное удалялось, а измененное докачивало лиш измененные части, а не файл целиком).
А ещё советую взглянуть в сторону Torrent-клиента. Можно использовать для более быстрого скачивания обновлений — Клиенты будут делиться друг с другом, снижая нагрузку на ваши сервера.
Я-же писал более сложный алгоритм создания «Патчей»: Архивов, содержащих в себе .diff файлы (для текста и конфигов) и кое-что аналогичное для бинарных данных. Благо файлы на клиенте хранились не единым архивом, а практически в открытую.
Бонусом была кнопочка «Перепроверить всё», заставляющая клиента просчитать CRC для всех файлов и сравнить с данными сервера (И скачивать, в случае различий).APXEOLOG
13.09.2018 21:33А насколько имеет смысл заморачиваться с diff'ами ради текста и конфигов (я так понимаю ваших, а не тех, что бы изменены пользователем)? Обычно размер подобных файлов достаточно мал и можно скачать целиком
Fox_exe
14.09.2018 10:03Там были файлы локализации по 10-20 мегабайт. Такчто имело смысл, хотя и небольшой.
Но основная причина — это механизм обновления бинарных файлов кусками, который использовал практически туже механику, что и diff-файлы.roryorangepants
14.09.2018 11:14Я както натыкался на «обновлялку», внутри которой был завернут GIT-клиент… а измененное докачивало лиш измененные части, а не файл целиком).
Но основная причина — это механизм обновления бинарных файлов кусками, который использовал практически туже механику, что и diff-файлы.
Я надеюсь, вы в курсе, что Git как раз не докачивает диффы, а делает снепшоты файлов?playermet
15.09.2018 15:32А вы сами в курсе, что то что git является snapshot-based а не diff-based VCS не значит что при работе он не использует дельта-компрессию для блобов? И снапшоты в git относятся не к файлам, а к коммитам.
Taraflex
13.09.2018 21:53Тогда уж сразу на основе aria2 делать, что не только torrent умеет, но и еще кучу всего.
На ней например DriverPack Solution работает.
Taraflex
13.09.2018 21:51ещё одна утилита? Всё просто: для обновления файлов игры нужно выполняться с правами администратора.
Программу можно научить перезапускать себя с запросом UAC
Например такvoid execAdmin(const wchar_t *path, const wchar_t *params) { SHELLEXECUTEINFOW sei = {0}; sei.cbSize = sizeof (sei); sei.fMask = SEE_MASK_NOCLOSEPROCESS; sei.hwnd = 0; sei.lpVerb = L"runas"; sei.lpFile = path; sei.lpParameters = params; sei.lpDirectory = 0; sei.nShow = SW_SHOW; sei.hInstApp = 0; if (ShellExecuteExW (&sei)) { WaitForSingleObject (sei.hProcess, INFINITE); CloseHandle (sei.hProcess); } }
0xf0a00
13.09.2018 23:11Ммм Delphi, приятно видеть что ты не одинок. Как раз кстати обдумывал как реализовать автообновление в проекте.
Cooler2 Автор
13.09.2018 23:32Да, можно и так, спасибо за инфу. В следующий раз оставлю один файл.
JediPhilosopher
14.09.2018 12:47А как тогда обновлять сам исполняемый файл, если он же и запущен? В линуксе-то это можно сделать, а вот в виндовсе оно же скажет что «не могу записать файл, он занят, попробуйте снова». Поэтому наверное везде где я видел используются все-таки два файла и апдейтер отдельно от основного приложения.
Fox_exe
14.09.2018 15:45В *nix'ах приложение почти всегда целиком помещается в оперативку и с файлом на диске можно творить всё, что вздумается.
В Винде, по любому, можно провернуть туже операцию. Просто это чуть сложнее.Virgo_Style
14.09.2018 19:07+1В апдейтере можно переименовать свой собственный файл и сохранить новый под старым именем.
zirix
14.09.2018 02:40Попробуйте этим сжимать файлы:
Port of LZMA SDK to Pascal (Delphi, Kylix and Freepascal) (https://www.7-zip.org/sdk.html)
Разница такая же, как между zip и 7zUranic2
14.09.2018 19:07+1Замечу, что для 7z есть обертка для 7z.dll в JCL (JclCompression). Тоже сжимаем файлы для автообоновлений в Delphi
ErgoZru
14.09.2018 13:19очень напоминает апдейтер от клиентов к «неофициальным» серверам lineage 2, который был распространен в 2008-2009 годах
sixeL
14.09.2018 19:08Cooler2 Автор
14.09.2018 19:11По ссылке там не процесс повышает права, а запускает новый процесс из того же exe-шника. Это как-раз то, что выше описал Taraflex Годный вариант, но всё же это не elevation самого процесса.
sixeL
14.09.2018 22:58Прошу вас обратить пристальное внимание на третий пункт. Приводится последовательность вызовов:
CredUIPromptForCredentials() или CredUIPromptForWindowsCredentials()
LogonUser()
ImpersonateLoggedOnUser()
RevertToSelf()
CloseHandle()
О запуске нового процесса здесь речи не идет. Магия заключается в том, что полученный в результате LogonUser токен может быть использован не только для запуска процесса но и для процесса имперсонации в текущем контексте выполнения и защиты процесса.
Этот механизм чаще я применял в ASP.NET в случае если web приложение работает от одного пользователя, а действия в БД типа MS SQL нужно выполнить от имени другого, который работает с приложением на данный момент.
.net пример и msdn
Suvitruf
rip.zip )=