Architektura serwera gry multiplayer

1

Witam, nie dawno rozpocząłem tworzenie projektu gry mmorpg wraz z niewielkim czteroosobowym zespołem. W zespole zajmuje się tworzeniem serwera gry. Client gry powstaje w unity, gdzie cały networking będzie tworzony przy użyciu C#. Serwer postanowiłem napisać w C++ przy użyciu biblioteki boost.asio. Pomimo, że napisałem już jakiś podstawowy kod, wciąż mam problem w przemyśleniu dobrej architektury serwera. Głównym problemem jest to jak ma przebiegać komunikacja server-client. Rozpisałem kilka możliwych schematów takiej komunikacji, jednak jako iż wcześniej miałem mało do czynienia z networkingiem, nie wiem który schemat będzie dobry do wymagań projektu.

Z początków moich prac nad serwerem miałem w głowie tylko jeden schemat. Na obrazku z komunikacją jest to schemat 3. Tzn. mamy klienta, który łączy się z jednym serwerem. Ten serwer robi wszystko począwszy od autoryzacji użytkowników, aktualizowania stanu gry, przy tym wysyłając odpowiednie zapytania do serwera bazy danych. Jednak wraz z biegiem czasu pomyślałem, że dobrym wyjściem byłoby rozdzielić ten jeden serwer na kilka podrzędnych, które działały by na różnych wątkach, lub w ogóle jako osobne procesy.

Dwa z nich to serwer gry oraz serwer autoryzacyjny. W celu rejestracji/logowania przez użytkownika, wysyłałby on odpowiedni pakiet na serwer autoryzacyjny, który zwracał by token uwierzytelnienia. W ten sposób klient mógłby się już łączyć z serwerem gry.

Aby uniknąć potrzeby bezpośredniego łączenia z serwerem bazy danych przez te dwa serwery, pomyślałem jeszcze czy by nie zrobić trzeciego serwera - takiego connectora do serwera bazy danych. Ten serwer byłby odpowiedzialny za łączność z bazą danych przez konstruowanie odpowiednich zapytań. Serwer ten komunikowałby się wyłącznie z serwerem autoryzacyjnym oraz serwerem gry, tzn. bezpośredni dostęp do tego serwera przez klienta nie byłby możliwy. A więc w przypadku tego serwera następowałaby komunikacja server-server.

I z tym serwerem mam duży dylemat, bo myślę, że taka komunikacja mogła by trochę wprowadzać bałagan w projekcie.

Jeśli chodzi o moje pojęcie tych serwerów, to mam na myśli serwer udp bądź serwer tcp przy użyciu biblioteki asio. W moim zamyśle serwer autoryzacyjny powinien być zrealizowany z użyciem protokołu tcp, a z serwerem gry następuję mały kłopot, gdyż wolałbym wykorzytać tych dwóch protokołów. Protokołu udp użyłbym w celu przesyłania danych, których utrata nie będzie miała dużego znaczenia, np. pakiet danych poruszającego się gracza. Jednak ta myśl znowu trochę utrudni sprawę, więc raczej zdecydowałbym się na robienie wszystkiego na protokołach tcp.

Czy mój tok rozumowania działania takiej architektury jest sensowny? Moim celem byłoby stworzenie serwera, który obsługiwałby ponad 1000 graczy jednocześnie, jednak nie wiem czy taka architektura nie skomplikuje zbytnio tego procesu. Wiadomo, że dużo zależy od tego jak to zaprogramuje, jednak nie chciałbym się w połowie tworzenia zorientować, że to co zrobiłem, przemyślałem było złe i przebudowywać wszystko inaczej.

Serwer ten piszę w środowisku Visual Studio, gdzie całe rozwiązanie podzieliłem na podrzędne projekty. Każdy z serwerów ma swój odpowiedni projekt. Mam także stworzony projekt "NetworkCore", który dostarcza potrzebne funkcje do innych projektów. W NetworkCore chcę skupić się na elementach, które będą kluczowę do działania tych podrzędnych projektów. Póki co zaimplementowałem w nim klasy serwera UDP oraz TCP, klasę pakietu, klasę do serializacji pakietów, klasę, która umożliwia połączenie. A w przyszłości będzie tego o wiele więcej.

Poniżej przedstawiam schematy, które zrobiłem do tej pory. Możliwe jest, że coś w nich napisałem nie do końca jasno albo narysowałem mało poprawnie, ale myślę, że będzie widać o co mi chodzi. Będe wdzięczny za jakieś wskazówki, który z tych typów komunikacji oznaczonych odpowiednio na obrazku, będzie dobrym rozwiązaniem. Mam świadomość, że każda z tych komunikacji może byłaby odpowiednia, ale to dużo zależy od innych czynników, a więc głównie chciałbym się dowiedzieć jakie są wady i zalety rozwiązania z podziałem komunikacji na kilka serwerów. Słyszałem coś ostatnio o architekturze mikroserwisów. Czy taka architektura byłaby w tym przypadku rozsądna i jak bardzo bym mógł jeszcze to rozbudować?

Dziękuje :)

image

Moje trzy pomysły:

image

0

Wpisanie interfejsów w architekturę to typowy przykład mieszania poziomów abstrakcji. Poza tym, ten interfejs chyba tam nawet nie jest potrzebny.

2
Zigor36 napisał(a):

Serwer postanowiłem napisać w C++ przy użyciu biblioteki boost.asio

Nie szedł bym w C++: ciężko utrzymywalny kod i słabo dokumentacja (zwłaszcza dla asio). Polecam go jak chcesz coś prostego z niskim latency albo rust jak chcesz maksymalnej wydajności. Ewentualnie jak klient jest w Unity to może po prostu C#, żeby było spójnie? Wybacz, ale to jak opisujesz problem mówi mi, że średnio ogarniasz i chciałbym ci odradzić rzucanie się z motyką na słońce (tj. na ASIO)

byłoby rozdzielić ten jeden serwer na kilka podrzędnych, które działały by na różnych wątkach, lub w ogóle jako osobne procesy.

Brzmi to bardzo dziwnie, twój serwer raczej i tak będzie wielowątkowy (bo pewnie będziesz używał proactora w ASIO). Co oznacza według ciebie podział na różne wątki/procesy i jakie problemy to powoduje/naprawia?

Dwa z nich to serwer gry oraz serwer autoryzacyjny. W celu rejestracji/logowania przez użytkownika, wysyłałby on odpowiedni pakiet na serwer autoryzacyjny, który zwracał by token uwierzytelnienia. W ten sposób klient mógłby się już łączyć z serwerem gry.

Aby uniknąć potrzeby bezpośredniego łączenia z serwerem bazy danych przez te dwa serwery, pomyślałem jeszcze czy by nie zrobić trzeciego serwera - takiego connectora do serwera bazy danych. Ten serwer byłby odpowiedzialny za łączność z bazą danych przez konstruowanie odpowiednich zapytań. Serwer ten komunikowałby się wyłącznie z serwerem autoryzacyjnym oraz serwerem gry, tzn. bezpośredni dostęp do tego serwera przez klienta nie byłby możliwy. A więc w przypadku tego serwera następowałaby komunikacja server-server.

Niech serwer gry woła bazę. Takie serwery przelotki (które nie mają logiki tylko przepychają) to zazwyczaj anty pattern. Zwłaszcza w twoim wypadku, gdzie masz serwer gry i latency jest kluczowe

Jeśli chodzi o moje pojęcie tych serwerów, to mam na myśli serwer udp bądź serwer tcp przy użyciu biblioteki asio. W moim zamyśle serwer autoryzacyjny powinien być zrealizowany z użyciem protokołu tcp, a z serwerem gry następuję mały kłopot, gdyż wolałbym wykorzytać tych dwóch protokołów. Protokołu udp użyłbym w celu przesyłania danych, których utrata nie będzie miała dużego znaczenia, np. pakiet danych poruszającego się gracza. Jednak ta myśl znowu trochę utrudni sprawę, więc raczej zdecydowałbym się na robienie wszystkiego na protokołach tcp.

Wszystko zależy co to za rodzaj gry. Najprościej użyć TCP, pisanie własnej nadbudówki nad UDP zależy od tego jakiego rodzaju jest to gra

Czy mój tok rozumowania działania takiej architektury jest sensowny? Moim celem byłoby stworzenie serwera, który obsługiwałby ponad 1000 graczy jednocześnie, jednak nie wiem czy taka architektura nie skomplikuje zbytnio tego procesu. Wiadomo, że dużo zależy od tego jak to zaprogramuje, jednak nie chciałbym się w połowie tworzenia zorientować, że to co zrobiłem, przemyślałem było złe i przebudowywać wszystko inaczej.

Jak nie wiesz jaka architektura jest najlepsza to zrób najprostszą. Jeden serwer, jedna baza. Jak uznasz, że coś jest nie tak to zawsze możesz wyciągnąć część serwera do innej aplikacji

1

W treści wyniki, że piszesz takie coś pierwszy raz. Napisz najprościej jak się da i tak będziesz to przepisywał kilka/kilkanaście razy jako, że nie masz doświadczenia. Zdobywając je zobaczysz co jest git, co jest nie tak, w kolejnej iteracji przepiszesz. Użyj C# nie macie przecież tam ogromnego ruchu, aby robić aż tak ogromne optymalizacje(kwestia też na ile szybsze będzie c++ niż c#. Gra WW3 ma backend serwerowy napisany w node.js i daje radę(dobra nie dało to przy pierwszej alphie, ale później napisali od nowa)

0

Ja bym protokół łączący wygenerował, tj użył coś jak Apache Thrift albo gRPC (albo niżej, tylko serilazacja Protocol Buffers)
Dlaczego ? Bo lubię

Zarazem to częściowo odpowiada na (słuszną) uwagę, ze C++ jest bardzo żmudnym językiem.

Disclaimer: gier ani z jednej strony, ani z drugiej nie znam. Wygenerowane protokoły by były konsekwentnie TCP a nie datagramowe.

Kolejne, co z orientacji na topową wydajność bym dał w wersji alfa (za @Michalk001 )

0
slsy napisał(a):
Zigor36 napisał(a):

byłoby rozdzielić ten jeden serwer na kilka podrzędnych, które działały by na różnych wątkach, lub w ogóle jako osobne procesy.

Brzmi to bardzo dziwnie, twój serwer raczej i tak będzie wielowątkowy (bo pewnie będziesz używał proactora w ASIO). Co oznacza według ciebie podział na różne wątki/procesy i jakie problemy to powoduje/naprawia?

Teraz bardziej się zastanawiam właśnie nad podziałem na procesy i myślę, że gdy odpalę te oba serwery na osobnych procesach, to będą one mogły pracować niezależnie od siebie, co może byłoby bardziej wydajne. Rozmawiałem też na ten temat z bratem, który też jest w zespole i ma doświadczenie pracy w projekcie, gdzie korzystali właśnie z takich mikroserwisów i w momencie jak jeden serwis się zatrzymał z jakiegoś powodu, to drugi mógł dalej działać. I na moim przykładzie gdyby serwer gry z jakiegoś powodu się zawiesił, to dalej byłaby możliwa rejestracja/logowanie użytkowników, np. na stronie internetowej gry (bo do gry by już użytkownik nie wszedł).

@Edit Chyba że to samo dałoby się osiągnąć przy zastosowaniu kilku wątków tak jak piszesz?

Niech serwer gry woła bazę. Takie serwery przelotki (które nie mają logiki tylko przepychają) to zazwyczaj anty pattern. Zwłaszcza w twoim wypadku, gdzie masz serwer gry i latency jest kluczowe

Tak też zrobię. Trochę za bardzo z tym poleciałem i myślę, że najpierw to spróbuje napisać bez tego, a gdy na prawdę będzie potrzeba, to najwyżej przemyśle jeszcze raz takie rozwiązanie.

Jeśli chodzi o moje pojęcie tych serwerów, to mam na myśli serwer udp bądź serwer tcp przy użyciu biblioteki asio. W moim zamyśle serwer autoryzacyjny powinien być zrealizowany z użyciem protokołu tcp, a z serwerem gry następuję mały kłopot, gdyż wolałbym wykorzytać tych dwóch protokołów. Protokołu udp użyłbym w celu przesyłania danych, których utrata nie będzie miała dużego znaczenia, np. pakiet danych poruszającego się gracza. Jednak ta myśl znowu trochę utrudni sprawę, więc raczej zdecydowałbym się na robienie wszystkiego na protokołach tcp.

Wszystko zależy co to za rodzaj gry. Najprościej użyć TCP, pisanie własnej nadbudówki nad UDP zależy od tego jakiego rodzaju jest to gra

Gra w zamiarze ma być takim hack n slashem, porównałbym ją do takiej gierki jak Drakensang Online. Wizja naszego świata gry byłaby podobna do popularnych gier mmorpg, takich jak Guild Wars 2, WoW, Metin2, Tibia. A więc serwer musiałby realizować wiele połączeń i interakcje wielu graczy między sobą. Wiele się tak na prawdę nad tym jeszcze nie zastanawialiśmy, więc ciężko mi dokładnie powiedzieć jakie wprowadzimy rodzaje pakietów, jak dużo będzie ich wysyłanych oraz jak wprowadzić w tym celu wielowątkowość. Wiem, że na 100% zrobimy jakiś podział świata gry na instancje. W Guild Wars 2 jak oglądałem to jest to zrealizowane tak, że są instancje jakichś mapek w grze a ilość tych instancji zależy od ilości serwerów, a jest ich tam dosyć sporo. Każda instancja ma dodatkowo limit graczy, który na nich może przebywać. Nie przewiduje, że nasz serwer będzie będzie tak rozbudowany, żeby odpalać serwery w różnych częściach europy/świata, przynajmniej na początku. Jednak wprowadzenie takiego limitu graczy i wielu instancji jednej mapy mógłby zapewnić lepszą wydajność. I ja to rozumiem tak, że każda instancja mogłaby być analizowana na osobnym wątku serwera, ale nie wiem czy to dobre podejście, bo w końcu nie wiadomo ile w sumie będzie takich instancji oraz ich ilość może wzrosnąć wraz z upływem czasu.

Z tymi pakietami udp to szczerze nie wiem, bo przeczytałem, że jest dużo zalet ich używania w poszczególnych przypadkach, ale jeśli rzeczywiście może to utrudnić to wprowadzę je dopiero gdy byłoby to konieczne, więc pewnie na etapie testowania gry, gdy będzie to już jakoś podstawowo działać.

Michalk001 napisał(a):

W treści wyniki, że piszesz takie coś pierwszy raz. Napisz najprościej jak się da i tak będziesz to przepisywał kilka/kilkanaście razy jako, że nie masz doświadczenia. Zdobywając je zobaczysz co jest git, co jest nie tak, w kolejnej iteracji przepiszesz. Użyj C# nie macie przecież tam ogromnego ruchu, aby robić aż tak ogromne optymalizacje(kwestia też na ile szybsze będzie c++ niż c#. Gra WW3 ma backend serwerowy napisany w node.js i daje radę(dobra nie dało to przy pierwszej alphie, ale później napisali od nowa)

Właśnie jeśli chodzi o wybór języka, to właśnie wolałbym w ramach potrzeby lepszej wydajności, czy też większego ruchu przepisywać wszystko od nowa w tym samym języku, mając już jakieś doświadczenie niż wybrać język mniej wydajny i przepisywać od nowa w nowym języku z nowymi wyzwaniami (dodatkowo w trudniejszym języku).

AnyKtokolwiek napisał(a):

Ja bym protokół łączący wygenerował, tj użył coś jak Apache Thrift albo gRPC (albo niżej, tylko serilazacja Protocol Buffers)
Dlaczego ? Bo lubię

Zarazem to częściowo odpowiada na (słuszną) uwagę, ze C++ jest bardzo żmudnym językiem.

Disclaimer: gier ani z jednej strony, ani z drugiej nie znam. Wygenerowane protokoły by były konsekwentnie TCP a nie datagramowe.

Kolejne, co z orientacji na topową wydajność bym dał w wersji alfa (za @Michalk001 )

Takie rozwiązanie jeśli dobrze rozumiem, to mogłoby też uprościć kod odnośnie network'ingu po stronie C# i C++? Czyli np. napisałbym kod dla wielu struktur danych takich np. pakietów i z taki kod mógłby zostać wygenerowany po obu stronach. Bo mam ten projekt NetworkCore, na którym tak właściwie będą się opierać wszystkie inne projekty i jeśli bym użył takiego protokołu zaoszczędziłbym sobię trochę pracy z pisaniem od postaw takiego systemu po stronie C# i C++?

Swoją drogą wiele się nad tym zastanawiałem czy nie pisać wszystkiego w tym samym języku, tzn clienta i serwera. Widziałem też tutorial na yt, gdzie gość uczył boost.asio właśnie na podstawie pisanie frameworka sieciowego, z którego korzystał client w C++ i serwer w C++. I właśnie się zastanawiałem czy są jakieś rozwiązania aby zrobić taki framework, tyle że mając clienta w C# i serwer w C++.

0
Zigor36 napisał(a):

Takie rozwiązanie jeśli dobrze rozumiem, to mogłoby też uprościć kod odnośnie network'ingu po stronie C# i C++? Czyli np. napisałbym kod dla wielu struktur danych takich np. pakietów i z taki kod mógłby zostać wygenerowany po obu stronach. Bo mam ten projekt NetworkCore, na którym tak właściwie będą się opierać wszystkie inne projekty i jeśli bym użył takiego protokołu zaoszczędziłbym sobię trochę pracy z pisaniem od postaw takiego systemu po stronie C# i C++?

Swoją drogą wiele się nad tym zastanawiałem czy nie pisać wszystkiego w tym samym języku, tzn clienta i serwera. Widziałem też tutorial na yt, gdzie gość uczył boost.asio właśnie na podstawie pisanie frameworka sieciowego, z którego korzystał client w C++ i serwer w C++. I właśnie się zastanawiałem czy są jakieś rozwiązania aby zrobić taki framework, tyle że mając clienta w C# i serwer w C++.

Taaaa. Tutoriale na YT, gdzie protokół sieciowy to dwie czynności Hello / Ping albo wyświetlenie czasu.

Jest pewna pula rozwiązań międzyjęzykowych do sprzęgania remote. Ja wymieniłem dwie.

Albo opieraja sie na definicji (struktur / metod) w swoim prostym meta-jęzuku, z którego sa generowane stuby w konkretnych językach.
Albo są "code first", gdzie np robisz adnotowany serwer w C#, a stuby w innych językach się "jakoś" wygenerują.

0

A dlaczego nie skorzystasz z rozwiązań, które oferuje Unity? Przecież pisanie własnego serwera to mnóstwo niepotrzebnej roboty takiej jak np. replikacja, lag compensation, synchronizacja symulacji fizyki etc., a to wszystko i jeszcze więcej masz już dostępne z poziomu silnika. Masz tam też dostępne różne strategie dotyczące topologii połączeń. Zajrzyj tu: https://docs-multiplayer.unity3d.com/netcode/current/about/
Dodam jeszcze, że używanie TCP do rozgrywki to bardzo kiepski pomysł. Z racji, że jest to transport połączeniowy, z gwarancją przesyłu pakietów i gwarancją kolejności, jest do dynamicznych gier zbyt mało wydajny.
Jako dobry punkt startowy polecam ten artykuł: https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/
Później oczywiście warto zapoznać się z kolejnymi tematami z tej strony.

2

CPP + ASIO to overkill. Serwer gry MMORPG, szczególnie raczkującej, nie za wiele robi więc nie potrzebujecie nie wiadomo jakiej wydajności. Użyjcie czegokolwiek co pozwoli Wam ruszyć dalej z developmentem, najlepiej czegoś co Wam się łatwo zintegruje z silnikiem.

Connecting unity clients with dedicated server Part 1

BTW. Trudność boost::asio polega na tym, że jest dość mocno oparty/inspirowany epoll'em, co nie do końca jest oczywiste, na linuksie bebechy ASIO to w praktyce jest epoll, który dodatkowo jest przykryty boostową szabloną abstrakcją, która cieknie. Cieknie, to znaczy, że czasem żeby działało trzeba zrobić rzeczy, które z pozoru nie mają większego sensu, dopóki nie zrozumie się wspomnianego epolla.

1 użytkowników online, w tym zalogowanych: 0, gości: 1