Bagaimana saya menambal Semesta

gambar

Ada banyak artikel tentang pengembangan game di Habré, tetapi di antara mereka ada beberapa artikel yang berhubungan dengan topik "di belakang layar". Salah satu topik ini adalah organisasi pengiriman, pada kenyataannya, dari permainan ke sejumlah besar pengguna untuk waktu yang lama (satu, dua, tiga). Terlepas dari kenyataan bahwa untuk beberapa tugas mungkin tampak sepele, saya memutuskan untuk berbagi pengalaman saya berjalan menyapu dalam hal ini untuk satu proyek tertentu. Siapa pun yang tertarik - silakan.

Penyimpangan kecil tentang pengungkapan informasi. Sebagian besar perusahaan sangat iri bahwa "dapur dalam" tidak dapat diakses oleh masyarakat umum. Kenapa - saya tidak tahu, tapi apa - itu. Dalam proyek khusus ini - The Universim - saya beruntung dan CEO dari Crytivo Inc. (sebelumnya Crytivo Games Inc.) Alex Wallet ternyata benar-benar waras dalam masalah seperti itu, jadi saya memiliki kesempatan untuk berbagi pengalaman dengan orang lain.

Sedikit tentang patcher itu sendiri


Saya sudah lama terlibat dalam pengembangan game. Dalam beberapa - sebagai perancang permainan dan programmer, dalam beberapa - sebagai perpaduan dari administrator sistem dan programmer (saya tidak suka istilah "devops", karena tidak secara akurat mencerminkan esensi dari tugas yang saya lakukan dalam proyek-proyek tersebut).

Pada akhir 2013 (kengerian tentang bagaimana waktu berlalu), saya berpikir untuk mengirimkan versi baru (versi) kepada pengguna. Tentu saja, pada waktu itu ada banyak solusi untuk tugas seperti itu, tetapi keinginan untuk membuat produk dan keinginan untuk “membangun sepeda” menang. Selain itu, saya ingin belajar C # lebih dalam - jadi saya memutuskan untuk membuat patcher sendiri. Ke depan, saya akan mengatakan bahwa proyek itu sukses, lebih dari selusin perusahaan telah menggunakannya dan menggunakannya dalam proyek mereka, beberapa diminta untuk membuat versi dengan mempertimbangkan persis keinginan mereka.

Solusi klasik melibatkan pembuatan paket delta (atau diffs) dari versi ke versi. Namun, pendekatan ini tidak nyaman bagi pemain penguji dan pengembang - dalam satu kasus, untuk mendapatkan versi terbaru dari gim ini, Anda harus melalui seluruh rantai pembaruan. Yaitu pemain perlu secara berurutan mengumpulkan sejumlah data tertentu yang dia (a) tidak akan pernah gunakan, dan pengembang untuk menyimpan di servernya (atau server) sekelompok data yang sudah ketinggalan zaman yang mungkin dibutuhkan beberapa pemain.

Dalam kasus lain - Anda perlu mengunduh tambalan untuk versi Anda ke yang terbaru, tetapi pengembang harus menjaga semua kebun binatang tambalan ini di rumah. Beberapa implementasi sistem patch memerlukan perangkat lunak tertentu dan beberapa logika di server - yang juga membuat sakit kepala tambahan untuk pengembang. Selain itu, seringkali pengembang game tidak ingin melakukan apa pun yang tidak terkait langsung dengan pengembangan game itu sendiri. Saya akan mengatakan lebih banyak lagi - kebanyakan bukan spesialis yang dapat mengkonfigurasi server untuk distribusi konten - ini bukan bidang aktivitas mereka.

Dengan semua ini dalam pikiran, saya ingin datang dengan solusi yang akan sesederhana mungkin bagi pengguna (yang ingin bermain lebih cepat dan tidak menari dengan tambalan versi yang berbeda), serta untuk pengembang yang perlu menulis permainan dan tidak mencari tahu apa dan mengapa tidak diperbarui oleh pengguna berikutnya.

Mengetahui bagaimana beberapa protokol sinkronisasi data bekerja - ketika data dianalisis pada klien dan hanya perubahan dari server yang ditransmisikan - Saya memutuskan untuk menggunakan pendekatan yang sama.
Selain itu, dalam praktiknya, dari versi ke versi sepanjang seluruh periode pengembangan, banyak file game berubah sedikit - teksturnya ada, modelnya sendiri, beberapa suara.

Akibatnya, tampaknya logis untuk mempertimbangkan setiap file dalam direktori gim sebagai satu set blok data. Ketika versi berikutnya dirilis, game build dianalisis, peta blok dibangun dan file game itu sendiri dikompres blok demi blok. Klien menganalisis blok yang ada dan hanya perbedaan yang diunduh.

Awalnya, patcher direncanakan sebagai modul di Unity3D, namun, satu detail yang tidak menyenangkan muncul yang membuat saya mempertimbangkan kembali hal ini. Faktanya adalah Unity3D adalah aplikasi (mesin) yang sepenuhnya independen terhadap kode Anda. Dan ketika mesin sedang berjalan, sejumlah besar file terbuka, yang menciptakan masalah ketika Anda ingin memperbaruinya.

Dalam sistem mirip Unix, menimpa file terbuka (kecuali jika dikunci secara khusus) tidak menimbulkan masalah, tetapi pada Windows, tanpa menari dengan rebana, “tipuan dengan telinga” seperti itu tidak berhasil. Itulah sebabnya saya menjadikan patcher sebagai aplikasi terpisah yang tidak memuat apa pun kecuali pustaka sistem. Secara de facto, patcher itu sendiri ternyata merupakan utilitas yang sepenuhnya independen dari mesin Unity3D, yang tidak mencegahnya, namun menambahkannya ke toko Unity3D.

Algoritma patcher


Jadi, pengembang merilis versi baru dengan frekuensi tertentu. Pemain ingin versi ini didapat. Tujuan pengembang adalah untuk menyediakan proses ini dengan biaya minimal dan sakit kepala minimal untuk pemain.

Dari pengembang


Saat menyiapkan tambalan, algoritme untuk tindakan tambalan terlihat seperti ini:

○ Buat pohon file game dengan atributnya dan checksum SHA512
○ Untuk setiap file:
► Pecahkan isinya menjadi beberapa blok.
► Simpan checksum SHA256.
► Kompres blok dan tambahkan ke peta blok file.
► Simpan alamat blokir dalam indeks.
○ Simpan pohon file dengan checksum mereka.
○ Simpan file data versi.

Pengembang harus mengunggah file yang diterima ke server.

Sisi pemain


Pada klien, patcher melakukan hal berikut:
○ Menyalin dirinya sendiri ke file dengan nama yang berbeda. Ini akan memperbarui file executable patcher jika perlu. Kemudian kontrol ditransfer ke salinan dan yang asli selesai.
○ Unduh file versi dan bandingkan dengan file versi lokal.
○ Jika perbandingannya tidak menunjukkan perbedaan - Anda dapat bermain, kami memiliki versi terbaru. Jika ada perbedaan, buka item berikutnya.
○ Unduh satu pohon file dengan checksum mereka.
○ Untuk setiap file di pohon dari server:
► Jika ada file, itu mempertimbangkan checksum (SHA512). Jika tidak, dianggap sebagai, tetapi kosong (mis. Terdiri dari nol solid) dan juga mempertimbangkan checksumnya.
► Jika jumlah file lokal tidak cocok dengan checksum file dari versi terbaru:
► Membuat peta blok lokal dan membandingkannya dengan peta blok dari server.
► Untuk setiap blok lokal yang berbeda dari yang jarak jauh, ia mengunduh blok terkompresi dari server dan menimpanya secara lokal.
○ Jika tidak ada kesalahan, perbarui file versi.

Saya membuat ukuran blok data kelipatan 1024 byte, setelah sejumlah tes, saya memutuskan bahwa lebih mudah dioperasikan dengan blok 64KB. Meskipun universalitas dalam kode tetap:

#region DQPatcher class public class DQPatcher { // some internal constants // 1 minute timeout by default private const int DEFAULT_NETWORK_TIMEOUT = 60000; // maximum number of compressed blocks, which we will download at once private const UInt16 MAX_COMPRESSED_BLOCKS = 1000; // default block size, you can use range from 4k to 64k, //depending on average size of your files in the project tree private const uint DEFAULT_BLOCK_SIZE = 64 * 1024; ... #region public constants and vars section // X * 1024 bytes by default for patch creation public static uint blockSize = DEFAULT_BLOCK_SIZE; ... #endregion .... 

Jika Anda membuat blok kecil, maka klien membutuhkan perubahan lebih sedikit ketika perubahan itu sendiri sedikit. Namun, masalah lain muncul - ukuran file indeks meningkat terbalik dengan penurunan ukuran blok - yaitu jika kita beroperasi dengan blok 8KB, maka file indeks akan 8 kali lebih besar daripada dengan blok 64KB.

Saya memilih SHA256 / 512 untuk file dan blok dari pertimbangan berikut: kecepatan berbeda sedikit dibandingkan dengan (usang) MD5 / SHA128, tetapi Anda masih perlu membaca blok dan file. Dan kemungkinan tabrakan dengan SHA256 / 512 secara signifikan lebih kecil dibandingkan dengan MD5 / SHA128. Untuk benar-benar membosankan - dalam hal ini, tetapi sangat kecil sehingga probabilitas ini dapat diabaikan.

Selain itu, klien memperhitungkan poin-poin berikut:
► Blok data dapat digeser dalam versi yang berbeda, mis. secara lokal, kami memiliki nomor blok 10, dan di server kami memiliki nomor blok 12, atau sebaliknya. Ini diperhitungkan agar tidak mengunduh data tambahan.
► Blok diminta bukan satu per satu, tetapi dalam kelompok - klien mencoba untuk menggabungkan rentang blok yang diperlukan dan meminta mereka dari server menggunakan header Range. Ini juga meminimalkan beban server:

 // get compressed remote blocks of data and return it to the caller // Note: we always operating with compressed data, so all offsets are in the _compressed_ data file!! // Throw an exception, if fetching compressed blocks failed public byte[] GetRemoteBlocks(string remoteName, UInt64 startByteRange, UInt64 endByteRange) { if (verboseOutput) Console.Error.WriteLine("Getting partial content for [" + remoteName + "]"); if (verboseOutput) Console.Error.WriteLine("Range is [" + startByteRange + "-" + endByteRange + "]"); int bufferSize = 1024; byte[] remoteData; byte[] buffer = new byte[bufferSize]; HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(remoteName); httpRequest.KeepAlive = true; httpRequest.AddRange((int)startByteRange, (int)endByteRange); httpRequest.Method = WebRequestMethods.Http.Get; httpRequest.ReadWriteTimeout = this.networkTimeout; try { // Get back the HTTP response for web server HttpWebResponse httpResponse = (HttpWebResponse)httpRequest.GetResponse(); if (verboseOutput) Console.Error.WriteLine("Got partial content length: " + httpResponse.ContentLength); remoteData = new byte[httpResponse.ContentLength]; Stream httpResponseStream = httpResponse.GetResponseStream(); if (!((httpResponse.StatusCode == HttpStatusCode.OK) || (httpResponse.StatusCode == HttpStatusCode.PartialContent))) // rise an exception, we expect partial content here { RemoteDataDownloadingException pe = new RemoteDataDownloadingException("While getting remote blocks:\n" + httpResponse.StatusDescription); throw pe; } int bytesRead = 0; int rOffset = 0; while ((bytesRead = httpResponseStream.Read(buffer, 0, bufferSize)) > 0) { // if(verboseOutput) Console.Error.WriteLine("Got ["+bytesRead+"] bytes of remote data block."); Array.Copy(buffer, 0, remoteData, rOffset, bytesRead); rOffset += bytesRead; } if (verboseOutput) Console.Error.WriteLine("Total got: [" + rOffset + "] bytes"); httpResponse.Close(); } catch (Exception ex) { if (verboseOutput) Console.Error.WriteLine(ex.ToString()); PatchException pe = new PatchException("Unable to fetch URI " + remoteName, ex); throw pe; } return remoteData; } 

Tentu saja, ternyata klien dapat diinterupsi kapan saja dan saat peluncuran berikutnya, secara de facto akan melanjutkan pekerjaannya, dan tidak akan mengunduh semuanya dari awal.

Di sini Anda dapat menonton video yang menggambarkan pekerjaan patcher pada proyek contoh Angry Bots:


Tentang bagaimana patch game universe diatur


Pada bulan September 2015, Alex Koshelkov menghubungi saya dan menawarkan untuk bergabung dengan proyek ini - mereka membutuhkan solusi yang akan memberikan 30 ribu (dengan ekor) pemain dengan pembaruan bulanan. Ukuran awal gim dalam arsip adalah 600 megabita. Sebelum menghubungi saya, ada upaya untuk membuat versi Anda sendiri menggunakan Electron, tetapi semuanya muncul terhadap masalah yang sama dari file yang terbuka (omong-omong, versi Electron saat ini dapat melakukan ini) dan beberapa lainnya. Juga, tidak ada pengembang yang mengerti bagaimana ini akan bekerja - mereka memberi saya beberapa desain sepeda, bagian server tidak ada sama sekali - mereka ingin melakukannya setelah semua tugas lainnya telah diselesaikan.

Selain itu, perlu untuk memecahkan masalah mencegah kebocoran kunci pemain - faktanya adalah bahwa kunci itu untuk platform Steam, meskipun game itu sendiri di Steam belum tersedia untuk umum. Mendistribusikan game diperlukan secara ketat oleh kunci - meskipun ada kemungkinan bahwa pemain dapat berbagi kunci permainan dengan teman-teman, ini bisa diabaikan, karena jika permainan muncul di Steam, kunci hanya dapat diaktifkan sekali.

Di versi normal patcher, pohon data untuk patch terlihat seperti ini:
 ./
 | - linux
 |  | - 1.0.0
 |  `- version.txt
 | - macosx
 |  | - 1.0.0
 |  `- version.txt
 `- windows
     | - 1.0.0
     `- version.txt


Saya perlu memastikan bahwa hanya mereka yang memiliki kunci yang benar yang memiliki akses.

Saya datang dengan solusi berikut - untuk setiap kunci kita mendapatkan hash-nya (SHA1), maka kita menggunakannya sebagai jalur untuk mengakses data tambalan di server. Di server, kami mentransfer data tambalan ke tingkat yang lebih tinggi dari docroot, dan menambahkan symlink ke direktori dengan data tambalan di dalam dokumen itu sendiri. Tautan simbolik memiliki nama yang sama dengan hash kunci, hanya dipecah menjadi beberapa tingkatan (untuk memfasilitasi pengoperasian sistem file), yaitu hash 0f99e50314d63c30271 ... ... ade71963e7ff akan ditampilkan sebagai
 ./0f/99/e5/0314d63c30271 ...........ade71963e7ff -----> / full / path / ke / patch-data /

Jadi, tidak perlu membagikan kunci itu sendiri kepada seseorang yang akan mendukung server pembaruan - cukup untuk mentransfer hash mereka, yang sama sekali tidak berguna bagi para pemain itu sendiri.

Untuk menambah kunci baru (atau menghapus yang lama) - cukup tambahkan / hapus tautan simbolik yang sesuai.

Dengan implementasi ini, verifikasi kunci itu sendiri jelas tidak dilakukan di mana pun, menerima 404 kesalahan pada klien menunjukkan bahwa kunci tersebut salah (atau telah dinonaktifkan).

Perlu dicatat bahwa akses kunci bukan perlindungan DRM lengkap - ini hanya pembatasan pada tahap pengujian alpha dan beta (tertutup). Dan pencarian mudah terputus dengan menggunakan server web itu sendiri (setidaknya di Nginx, yang saya gunakan).

Pada bulan peluncuran, hanya 2,5 TB lalu lintas dikirimkan pada hari pertama saja, dan pada hari-hari berikutnya, kira-kira jumlah yang sama didistribusikan rata-rata per bulan:

gambar

Karena itu, jika Anda berencana untuk mendistribusikan banyak konten - yang terbaik adalah menghitung terlebih dahulu berapa biayanya. Menurut pengamatan pribadi - lalu lintas termurah dari hosters Eropa, yang paling mahal (saya akan mengatakan "emas") dari Amazon dan Google.

Dalam praktiknya, penghematan lalu lintas per tahun rata-rata di The Universim sangat besar - bandingkan angka di atas. Tentu saja, jika seorang pengguna tidak memiliki permainan sama sekali atau jika itu sudah sangat usang, keajaiban tidak akan terjadi dan ia harus mengunduh banyak data dari server - jika dari awal, maka sedikit lebih banyak dari yang dibutuhkan dalam arsip dalam permainan. Namun, dengan pembaruan bulanan, semuanya menjadi sangat bagus. Dalam waktu kurang dari 6 bulan, mirror Amerika memberi sedikit lebih dari 10 TB traffic, tanpa menggunakan patcher nilai ini akan tumbuh secara signifikan.

Ini adalah bagaimana lalu lintas tahunan proyek terlihat seperti:

gambar

Beberapa kata tentang "rake" yang paling berkesan yang harus kami selesaikan dalam proses mengerjakan patcher khusus untuk game "The Universim":

● Masalah terbesar sedang menunggu saya dari antivirus. Ya, mereka tidak suka aplikasi yang mengunduh sesuatu dari Internet di sana, memodifikasi file (termasuk yang dapat dieksekusi), dan kemudian juga mencoba menjalankan unduhan. Beberapa antivirus tidak hanya memblokir akses ke file lokal - mereka juga memblokir panggilan ke server pembaruan sendiri, masuk langsung ke data yang diunduh klien. Solusinya adalah dengan menggunakan tanda tangan digital yang valid untuk patcher - ini secara dramatis mengurangi paranoia antivirus, dan menggunakan protokol HTTPS, bukan HTTP - dengan cepat menghilangkan beberapa kesalahan yang terkait dengan keingintahuan antivirus.

● Pembaruan kemajuan. Banyak pengguna (dan pelanggan) ingin melihat perkembangan pembaruan. Kita harus berimprovisasi, karena tidak selalu mungkin untuk menunjukkan kemajuan secara andal tanpa harus melakukan pekerjaan ekstra. Ya, dan waktu yang tepat dari akhir proses tambalan juga tidak dapat ditampilkan, karena tambalan itu sendiri tidak memiliki data tentang file mana yang perlu diperbarui terlebih dahulu.

● Sejumlah besar pengguna dari AS memiliki kecepatan koneksi ke server dari Eropa tidak terlalu tinggi. Migrasi server pembaruan ke AS memecahkan masalah ini. Untuk pengguna dari benua lain, kami meninggalkan server di Jerman. Omong-omong, lalu lintas di AS jauh lebih mahal daripada Eropa, dalam beberapa kasus - beberapa lusin kali.

● Apple tidak terlalu nyaman dengan metode pemasangan aplikasi ini. Kebijakan resmi - aplikasi harus diinstal hanya dari toko mereka. Tetapi masalahnya adalah, aplikasi pada tahap pengujian alfa dan beta tidak diperbolehkan di toko. Dan terlebih lagi, tidak ada yang perlu dibicarakan tentang menjual aplikasi mentah dari akses awal. Karena itu, Anda harus menulis instruksi tentang cara menari di atas bunga poppy. Opsi dengan AppAnnie (saat itu mereka masih independen) tidak dipertimbangkan karena batasan jumlah penguji.

● Jaringan tidak bisa diprediksi. Agar aplikasi tidak segera menyerah, saya harus memasukkan penghitung kesalahan. 9 pengecualian yang tertangkap memungkinkan Anda memberi tahu pengguna bahwa ia memiliki masalah dengan jaringan.

● Sistem operasi 32-bit memiliki batasan pada ukuran file yang ditampilkan dalam memori (Memory Mapped Files - MMF) untuk setiap utas eksekusi dan untuk proses secara keseluruhan. Versi pertama dari patcher menggunakan MMF untuk mempercepat pekerjaan, tetapi karena file sumber daya game bisa sangat besar, saya harus meninggalkan pendekatan ini dan menggunakan aliran file biasa. Kehilangan kinerja khusus, omong-omong, tidak diamati - kemungkinan besar karena pembacaan OS proaktif.

● Anda harus siap bagi pengguna untuk mengeluh. Tidak peduli seberapa bagus produk Anda, akan selalu ada orang yang tidak puas. Dan semakin banyak pengguna produk Anda (dalam kasus The Universim ada lebih dari 50 ribu saat ini) - semakin kuantitatif akan ada keluhan kepada Anda. Dalam istilah persentase, ini adalah jumlah yang sangat kecil, tetapi dalam istilah kuantitatif ...

Terlepas dari kenyataan bahwa proyek secara keseluruhan berhasil, ia memiliki beberapa kelemahan:

● Meskipun saya awalnya mengeluarkan semua logika utama secara terpisah, bagian GUI berbeda dalam implementasi untuk MAC dan Windows. Versi Linux tidak menimbulkan masalah - semua masalah terutama hanya ketika menggunakan monolitik yang tidak memerlukan Mono Runtime Environment - MRE. Tetapi karena Anda perlu memiliki lisensi tambahan untuk mendistribusikan file yang dapat dieksekusi tersebut, diputuskan untuk meninggalkan bangunan monolitik dan hanya memerlukan MRE. Versi Linux berbeda dari versi Windows hanya dalam mendukung atribut file yang spesifik untuk sistem * nix. Untuk proyek kedua saya, yang akan lebih dari sekedar patcher, saya berencana untuk menggunakan pendekatan modular dalam bentuk proses-kernel yang berjalan di latar belakang dan memungkinkan mengelola semuanya pada antarmuka lokal. Dan kontrol itu sendiri dapat dilakukan dari aplikasi berbasis Electron dan sejenisnya (atau hanya dari browser). Dengan hal kecil apa pun. Sebelum berbicara tentang ukuran distribusi aplikasi seperti itu - lihat ukuran game. Demo (!!!) versi beberapa menempati 5 atau lebih gigabyte di arsip (!!!).

● Struktur yang digunakan sekarang tidak menghemat ruang saat game dirilis untuk 3 platform - secara de facto, Anda perlu menyimpan 3 salinan data yang hampir identik, meskipun terkompresi.

● Versi patcher saat ini tidak men-cache pekerjaannya - setiap kali semua checksum dari semua file dihitung ulang. Adalah mungkin untuk mengurangi waktu secara signifikan jika patcher menyimpan hasil untuk file-file yang sudah ada di klien. Tetapi ada satu dilema - jika file rusak (atau hilang), tetapi entri cache untuk file ini disimpan, maka patcher akan melewatinya, yang akan menyebabkan masalah.

● Versi saat ini tidak dapat bekerja secara bersamaan dengan beberapa server (kecuali jika Anda membuat Round-robin menggunakan DNS) - Saya ingin beralih ke teknologi "torrent-like" sehingga Anda dapat menggunakan beberapa server pada saat yang sama. Tidak ada pertanyaan tentang menggunakan klien sebagai sumber data, karena hal ini menimbulkan banyak masalah hukum dan lebih mudah untuk menolak sejak awal.

● Jika Anda ingin membatasi akses ke pembaruan, maka logika ini harus diterapkan secara mandiri. Sebenarnya, ini hampir tidak bisa disebut kelemahan, karena setiap orang dapat memiliki keinginan mereka sendiri mengenai pembatasan. Pembatasan kunci paling sederhana - tanpa bagian server - dibuat cukup sederhana, seperti yang saya tunjukkan di atas.

● Patcher dibuat hanya untuk satu proyek pada satu waktu. Jika Anda ingin membuat sesuatu yang mirip dengan Steam, maka seluruh sistem pengiriman konten sudah diperlukan. Dan ini adalah proyek dari level yang sama sekali berbeda.

Saya berencana untuk menempatkan patcher sendiri di domain publik setelah "generasi kedua" diterapkan - sistem pengiriman konten game yang tidak hanya mencakup patcher yang dikembangkan, tetapi juga modul telemetri (karena pengembang perlu tahu apa yang dilakukan para pemain), Modul Cloud Save dan beberapa modul lainnya.

Jika Anda memiliki proyek nirlaba dan membutuhkan patcher, tuliskan saya detail tentang proyek Anda dan saya akan memberikan Anda salinannya secara gratis. Tidak akan ada tautan di sini, karena ini bukan hub "I PR".

Saya akan dengan senang hati menjawab pertanyaan Anda.

Source: https://habr.com/ru/post/id440870/


All Articles