Pixockets: bagaimana kami menulis perpustakaan jaringan kami sendiri untuk server game



Halo! Connected Stanislav Yablonsky, Pengembang Server Utama Pixonic.

Ketika saya pertama kali datang ke Pixonic, server permainan kami adalah aplikasi yang didasarkan pada Photon Realtime SDK : kerangka kerja multifungsi namun sangat berat. Tampaknya solusi ini adalah menyederhanakan pekerjaan dengan server. Jadi - sampai titik tertentu.

Photon Realtime mengikat kami pada dirinya sendiri dengan harus menggunakannya untuk bertukar data antara pemain dan server - dan juga mengikatnya ke Windows, karena itu hanya dapat bekerja di atasnya. Ini memberlakukan batasan pada kami berdua dari sudut pandang runtime (runtime): tidak mungkin untuk mengubah banyak pengaturan penting dari mesin virtual .NET, dan sistem operasi. Kami terbiasa bekerja dengan server Linux, bukan Windows. Selain itu, harganya lebih murah.

Juga, penggunaan kinerja hit Photon baik di server dan di klien, dan ketika profil, beban yang layak pada pengumpul sampah dan sejumlah besar tinju / unboxing terbentuk.

Singkatnya, solusi dengan Photon Realtime jauh dari optimal bagi kami, dan untuk waktu yang lama diperlukan untuk melakukan sesuatu dengan itu - tetapi selalu ada tugas yang lebih mendesak, dan tangan tidak mencapai solusi masalah dengan server.

Karena menarik bagi saya tidak hanya untuk menyelesaikan masalah, tetapi juga untuk lebih memahami jaringan, saya memutuskan untuk mengambil inisiatif di tangan saya sendiri dan mencoba menulis perpustakaan sendiri. Tetapi, Anda mengerti, di rumah - di rumah, di tempat kerja - di tempat kerja, akibatnya, waktu untuk mengembangkan perpustakaan hanya dalam transportasi. Namun, ini tidak menghentikan gagasan untuk membuahkan hasil.

Apa yang terjadi - baca terus.

Ideologi perpustakaan


Karena kami mengembangkan game online, sangat penting bagi kami untuk bekerja tanpa jeda, sehingga biaya rendah telah menjadi persyaratan utama bagi perpustakaan. Bagi kami, ini adalah, pertama-tama, beban rendah pada pengumpul sampah. Untuk mencapainya, saya mencoba menghindari alokasi, dan dalam kasus di mana sulit untuk mencapai atau tidak berhasil sama sekali, kami membuat kumpulan (untuk buffer byte, status koneksi, header, dll.).

Untuk kesederhanaan dan kenyamanan dukungan dan perakitan, kami mulai menggunakan hanya C # dan soket sistem. Selain itu, penting untuk menyesuaikan dengan anggaran waktu per frame, karena data dari server seharusnya tiba tepat waktu. Oleh karena itu, saya mencoba untuk mengurangi waktu eksekusi, bahkan dengan biaya beberapa ketidak-optimalan: yaitu, di beberapa tempat layak untuk mengganti algoritma yang cepat dan sebagian lebih kompleks dan struktur data dengan yang lebih sederhana dan lebih dapat diprediksi. Misalnya, kami tidak menggunakan antrian bebas kunci, karena mereka membuat beban pada pengumpul sampah.

Biasanya untuk penembak multipemain, data kami dikirim melalui UDP. Masih di atasnya ditambahkan fragmentasi dan perakitan paket untuk mengirim data dengan ukuran lebih besar dari ukuran bingkai, serta pengiriman yang andal karena meneruskan dan membangun koneksi.

Bingkai UDP di perpustakaan kami secara default adalah 1200 byte. Paket dengan ukuran ini harus ditransmisikan dalam jaringan modern dengan risiko fragmentasi yang cukup rendah, karena MTU di sebagian besar jaringan modern lebih tinggi dari nilai ini. Pada saat yang sama, biasanya jumlah ini cukup untuk menyesuaikan dengan perubahan yang perlu dikirim ke pemain setelah centang berikutnya (pembaruan status) dalam permainan.

Arsitektur


Di perpustakaan kami, kami menggunakan soket dua lapis:

  • Lapisan pertama bertanggung jawab untuk bekerja dengan panggilan sistem dan menyediakan API yang lebih nyaman untuk tingkat selanjutnya;
  • Lapisan kedua bekerja langsung dengan sesi, fragmentasi / perakitan paket, penerusannya, dll.



Kelas untuk bekerja dengan koneksi, pada gilirannya, juga dibagi menjadi dua tingkatan:

  • Level bawah (SockBase) bertanggung jawab untuk mengirim dan menerima data melalui UDP. Ini adalah pembungkus tipis di atas objek sistem soket.
  • Level Atas (SmartSock) menyediakan fungsionalitas tambahan di atas UDP. Memotong dan menempelkan paket, meneruskan data yang belum mencapai, penolakan duplikat - semua ini adalah bidang tanggung jawabnya.

Level yang lebih rendah dibagi menjadi dua kelas: BareSock dan ThreadSock.

  • BareSock bekerja di utas yang sama tempat panggilan berasal, mengirim dan menerima data dalam mode non-pemblokiran.
  • ThreadSock menempatkan paket dalam antrian dan karenanya membuat utas terpisah untuk mengirim dan menerima data. Saat mengaksesnya, hanya ada satu operasi: menambah atau menghapus data dari antrian.

BareSock sering digunakan untuk bekerja dengan klien, ThreadSock - dengan server.

Fitur pekerjaan


Saya juga menulis dua jenis soket tingkat rendah:

  • Yang pertama adalah single-threaded sinkron. Di dalamnya, kita mendapatkan overhead minimum untuk memori dan prosesor, tetapi pada saat yang sama panggilan sistem terjadi secara langsung ketika mengakses soket. Ini meminimalkan overhead secara umum (tidak perlu menggunakan antrian dan buffer tambahan), tetapi panggilan itu sendiri mungkin memakan waktu lebih lama daripada mengambil item dari antrian.
  • Yang kedua adalah asinkron dengan utas terpisah untuk membaca dan menulis. Dalam hal ini, kami mendapatkan overhead tambahan untuk antrian, sinkronisasi, dan waktu pengiriman / penerimaan (dalam beberapa milidetik), karena pada saat akses ke soket, utas baca atau tulis dijeda.

Kami juga mencoba menggunakan SocketAsyncEventArgs - mungkin API jaringan paling canggih di .NET yang saya ketahui. Tapi ternyata itu mungkin tidak berfungsi untuk UDP: TCP stack berfungsi dengan baik, tetapi UDP memberikan kesalahan tentang mendapatkan bingkai yang terpotong aneh dan bahkan menabrak. NET - seolah-olah memori di bagian asli mesin virtual rusak. Saya tidak menemukan contoh pengoperasian skema semacam itu.

Fitur penting lainnya dari perpustakaan kami adalah mengurangi kehilangan data. Kami mendapat kesan bahwa untuk menghilangkan duplikat, banyak perpustakaan membuang paket data lama, seperti yang kemudian kita lihat dari pengalaman kami sendiri. Tentu saja, implementasi seperti itu jauh lebih sederhana, karena dalam kasusnya satu penghitung dengan jumlah frame terakhir tiba sudah cukup, tetapi itu tidak cocok untuk kita. Oleh karena itu, Pixockets menggunakan buffer melingkar dari jumlah frame terakhir untuk memfilter duplikat: angka yang baru tiba ditimpa alih-alih yang lama, dan duplikat dicari di antara frame yang terakhir diterima.



Jadi, jika suatu paket dikirim sebelum frame saat ini, tetapi datang setelahnya, ia masih akan mencapai tujuan. Ini dapat sangat membantu, misalnya, dalam kasus interpolasi posisi. Dalam hal ini, kita akan memiliki cerita yang lebih lengkap.

Struktur paket data


Data di perpustakaan ditransmisikan sebagai berikut:



Di awal paket adalah header:

  • Dimulai dengan ukuran paket, yang pada gilirannya terbatas pada 64 kilobyte.
  • Ukurannya diikuti oleh byte dengan bendera. Interpretasi dari sisa judul tergantung pada ketersediaannya.
  • Berikutnya adalah pengidentifikasi untuk sesi atau koneksi.

Dengan bendera yang sesuai, maka kita dapatkan:

  • Jika bendera dengan nomor paket pada gilirannya diatur, nomor paket ditransmisikan setelah pengidentifikasi sesi.
  • Mengikutinya - juga dalam hal set bendera - jumlah paket yang dikonfirmasi dan jumlahnya.

Di akhir tajuk terdapat informasi tentang fragmen:

  • pengidentifikasi urutan fragmen, yang diperlukan untuk membedakan fragmen pesan yang berbeda;
  • nomor urut fragmen;
  • total jumlah fragmen dalam pesan.

Informasi tentang fragmen juga memerlukan pengaturan bendera yang sesuai.

Perpustakaan ditulis. Apa berikutnya?


Untuk mendapatkan informasi koneksi sinkron yang lebih akurat, kami kemudian mengatur koneksi eksplisit. Ini membantu kami untuk memahami dengan jelas situasi ketika satu pihak berpikir bahwa koneksi dibuat dan tidak terputus, dan yang lain - bahwa itu terputus.

Dalam versi pertama Pixockets, ini bukan: klien tidak perlu memanggil metode Connect (host, port) - itu baru mulai mengirim data ke alamat dan port yang dikenal. Kemudian server memanggil metode Listen (port) dan mulai menerima data dari alamat tertentu. Data sesi diinisialisasi setelah menerima / mengirim paket.

Sekarang, untuk membuat koneksi, "jabat tangan" telah menjadi penting - pertukaran paket yang dibentuk secara khusus - dan klien harus memanggil Connect.

Selain itu, salah satu rekan saya bercabang di perpustakaan, lebih memperhatikan keamanan jaringan, dan juga menambahkan beberapa fitur, seperti kemampuan untuk terhubung kembali langsung di dalam soket: misalnya, ketika beralih antara Wi-Fi dan 4G, koneksi sekarang dipulihkan secara otomatis. Tapi kita akan membicarakannya nanti.

Pengujian


Tentu saja, kami menulis unit test untuk perpustakaan: mereka memeriksa semua cara utama untuk membangun koneksi, mengirim dan menerima data, fragmentasi dan perakitan paket, berbagai anomali dalam mengirim dan menerima data - seperti duplikasi, kehilangan, ketidakcocokan dalam urutan pengiriman dan penerimaan. Untuk pemeriksaan kinerja awal, saya menulis aplikasi pengujian khusus untuk pengujian integrasi: klien ping, server ping dan aplikasi yang menyinkronkan posisi, warna, dan jumlah lingkaran berwarna pada layar melalui jaringan.

Setelah aplikasi pengujian membuktikan fungsionalitas perpustakaan kami, kami mulai membandingkannya dengan perpustakaan lain: dengan Photon Realtime lama kami dan dengan perpustakaan UDP LiteNetLib 0.7.

Kami menguji versi sederhana dari server permainan yang hanya mengumpulkan input dari pemain dan mengirimkan kembali hasil "terpaku". Kami mengambil 500 pemain di kamar 6 orang, kecepatan refresh adalah 30 kali per detik.



Beban pada pengumpul sampah dan konsumsi prosesor ternyata lebih rendah dalam kasus Pixockets, serta persentase paket yang hilang - tampaknya karena kenyataan bahwa, tidak seperti versi UDP lainnya, kami tidak mengabaikan paket yang terlambat.

Setelah kami menerima konfirmasi tentang keunggulan solusi kami dalam tes sintetis, langkah selanjutnya adalah menjalankan perpustakaan pada proyek nyata.

Pada saat itu, dalam proyek yang kami pilih, klien dan server game disinkronkan melalui Server Photon. Saya menambahkan dukungan Pixockets ke klien dan server, sehingga memungkinkan untuk mengontrol pilihan protokol dari server perjodohan - yang mana klien mengirim permintaan untuk memasuki permainan.

Untuk beberapa periode, klien bermain secara bersamaan pada kedua protokol, dan pada saat itu kami mengumpulkan statistik tentang bagaimana mereka melakukannya. Pada akhir pengumpulan statistik, ternyata hasilnya tidak berbeda dengan tes sintetik: beban pada pengumpul sampah dan prosesor mengalami penurunan, paket juga hilang. Pada saat yang sama, ping menjadi sedikit lebih rendah. Oleh karena itu, versi berikutnya dari permainan telah dirilis sepenuhnya pada Pixockets tanpa menggunakan Photon Realtime SDK.



Rencana masa depan


Sekarang kami ingin mengimplementasikan fitur-fitur berikut di perpustakaan:

  • Koneksi yang disederhanakan: sekarang tidak berfungsi secara optimal, dan setelah memanggil Sambungkan pada klien, Anda perlu memanggil Baca sampai status koneksi berubah;
  • Shutdown eksplisit: saat ini, shutdown di sisi lain hanya terjadi oleh timer;
  • Ping bawaan untuk menjaga konektivitas;
  • Penentuan otomatis ukuran frame optimal (sekarang hanya konstanta yang digunakan).

Anda dapat melihat dan berpartisipasi dalam pengembangan Pixockets lebih lanjut di alamat repositori.

All Articles