Czy istnieje wzorzec do pisania serwera turowego komunikującego się z klientami za pośrednictwem gniazd?

14

Pracuję na ogólnym serwerze gier, który zarządza grami dla dowolnej liczby klientów w sieci z gniazdami TCP grających w grę. Mam „projekt” zhakowany razem z taśmą klejącą, która działa, ale wydaje się zarówno delikatna, jak i nieelastyczna. Czy istnieje dobrze ustalony wzorzec, w jaki sposób pisać komunikację klient / serwer, który jest solidny i elastyczny? (Jeśli nie, jak ulepszysz to, co mam poniżej?)

Z grubsza mam to:

  • Podczas konfigurowania gry serwer ma jeden wątek dla każdego gniazda odtwarzacza obsługującego synchroniczne żądania od klienta i odpowiedzi z serwera.
  • Jednak gdy gra się rozpoczyna, wszystkie wątki oprócz jednego snu i ten wątek przechodzi przez wszystkich graczy jeden po drugim, komunikując się o swoim ruchu (w odwrotnym żądaniu-odpowiedzi).

Oto schemat tego, co aktualnie mam; kliknij, by zobaczyć większą / czytelniejszą wersję, lub 66kB PDF .

Schemat sekwencji przepływu

Problemy:

  • Wymaga od graczy odpowiedzi po kolei dokładnie z właściwą wiadomością. (Przypuszczam, że mógłbym pozwolić każdemu graczowi na przypadkowe bzdury i przejść dalej tylko wtedy, gdy dadzą mi prawidłowy ruch).
  • Nie pozwala graczom rozmawiać z serwerem, chyba że jest to ich kolej. (Mogę poprosić serwer o wysyłanie aktualizacji o innych graczach, ale nie może przetwarzać żądania asynchronicznego).

Wymagania końcowe:

  • Wydajność nie jest najważniejsza. Będzie to głównie wykorzystywane do gier nie w czasie rzeczywistym, a przede wszystkim do stawiania sobie AI przeciwko sobie, a nie drżącym ludziom.

  • Rozgrywka zawsze będzie oparta na turach (nawet w bardzo wysokiej rozdzielczości). Każdy gracz jest zawsze przetwarzany jeden ruch, zanim wszyscy inni gracze otrzymają kolej.

Implementacja serwera dzieje się w Rubim, jeśli to robi różnicę.

Phrogz
źródło
Jeśli używasz gniazda TCP na klienta, czy to ograniczenie do (65535-1024) klientów?
o0 ”.
1
Dlaczego to jest problem, Lohoris? Większość ludzi będzie miała
problem
@Kylotan Rzeczywiście. Jeśli mam więcej niż 10 równoczesnych SI, będę zaskoczony. :)
Phrogz,
Z ciekawości, jakiego narzędzia użyłeś do stworzenia tego diagramu?
Matthias
@matthias Niestety, to zbyt wiele niestandardowych prac w programie Adobe Illustrator.
Phrogz

Odpowiedzi:

10

Nie jestem pewien, co dokładnie chcesz osiągnąć. Ale jest jeden wzorzec, który jest stale używany na serwerach gier i może ci pomóc. Użyj kolejek wiadomości.

Mówiąc dokładniej: kiedy klienci wysyłają wiadomości do serwera, nie przetwarzaj ich natychmiast. Zamiast tego przeanalizuj je i umieść w kolejce dla tego konkretnego klienta. Następnie w jakiejś głównej pętli (może nawet w innym wątku) przejrzyj kolejno wszystkich klientów, pobieraj wiadomości z kolejki i przetwarzaj je. Gdy przetwarzanie wskazuje, że tura tego klienta jest zakończona, przejdź do następnego.

W ten sposób klienci nie muszą ściśle pracować krok po kroku; tylko wystarczająco szybko, aby mieć coś w kolejce do czasu przetworzenia klienta (możesz oczywiście albo poczekać na klienta, albo pominąć jego kolej, jeśli się opóźni). Możesz dodać obsługę żądań asynchronicznych, dodając kolejkę „asynchroniczną”: gdy klient wysyła specjalne żądanie, jest ono dodawane do tej specjalnej kolejki; kolejka ta jest sprawdzana i przetwarzana częściej niż klientów.

Nieważne
źródło
1

Wątki sprzętowe nie skalują się wystarczająco dobrze, aby „jeden na gracza” był rozsądnym pomysłem dla 3-cyfrowej liczby graczy, a logika wiedząca, kiedy je obudzić, będzie się komplikować. Lepszym pomysłem jest znalezienie asynchronicznego pakietu we / wy dla Ruby, który pozwoli ci wysyłać i odbierać dane bez konieczności zatrzymywania całego wątku programu podczas operacji odczytu lub zapisu. Rozwiązuje to również problem oczekiwania na odpowiedź graczy, ponieważ nie będzie żadnych wątków zawieszonych na operacji odczytu. Zamiast tego Twój serwer może po prostu sprawdzić, czy upłynął limit czasu, a następnie odpowiednio powiadomić drugiego gracza.

Zasadniczo „asynchroniczne operacje we / wy” to „wzorzec”, którego szukasz, chociaż tak naprawdę nie jest to wzorzec, a raczej podejście. Zamiast jawnie wywoływać polecenie „czytaj” na gnieździe i wstrzymywać program do momentu nadejścia danych, skonfigurujesz system tak, aby wywoływał procedurę obsługi „onRead”, gdy tylko dane są gotowe, i kontynuujesz przetwarzanie do tego czasu.

każde gniazdo ma zwrot

Każdy gracz ma swoją kolej, a każdy gracz ma gniazdo, które wysyła dane, co jest nieco inne. Pewnego dnia możesz nie chcieć jednego gniazda na odtwarzacz. W ogóle nie możesz używać gniazd. Zachowaj oddzielne obszary odpowiedzialności. Przepraszam, jeśli brzmi to jak nieistotny szczegół, ale kiedy połączysz koncepcje w swoim projekcie, które powinny być inne, utrudni to znalezienie i omówienie lepszych podejść.

Kylotan
źródło
1

Z pewnością jest na to więcej niż jeden sposób, ale osobiście całkowicie pominąłem osobne wątki i użyję pętli zdarzeń. Sposób, w jaki to zrobisz, będzie w pewnym stopniu zależeć od używanej biblioteki we / wy, ale w zasadzie twoja główna pętla serwera będzie wyglądać następująco:

  1. Skonfiguruj pulę połączeń i gniazdo nasłuchiwania dla nowych połączeń.
  2. Poczekaj, aż coś się wydarzy.
  3. Jeśli coś jest nowym połączeniem, dodaj je do puli.
  4. Jeśli coś jest żądaniem klienta, sprawdź, czy możesz to natychmiast obsłużyć. Jeśli tak, zrób to; jeśli nie, umieść go w kolejce i (opcjonalnie) wyślij potwierdzenie do klienta.
  5. Sprawdź także, czy w kolejce jest coś, co możesz teraz obsłużyć; jeśli tak, zrób to.
  6. Wróć do kroku 2.

Załóżmy na przykład, że masz n klientów zaangażowanych w grę. Kiedy następnie n-1 z nich wyśle ​​swoje ruchy, po prostu sprawdzasz, czy ruch wygląda na prawidłowy, i odsyłasz wiadomość z informacją, że ruch został otrzymany, ale nadal czekasz na ruch innych graczy. Po przeniesieniu wszystkich n graczy przetwarzasz wszystkie zapisane ruchy i wysyłasz wyniki do wszystkich graczy.

Możesz również sprecyzować ten schemat, aby zawierał limity czasu - większość bibliotek we / wy powinna mieć mechanizm oczekiwania na pojawienie się nowych danych lub upływ czasu.

Oczywiście można również zaimplementować coś takiego z osobnymi wątkami dla każdego połączenia, przekazując te wątki wszelkim żądaniom, których nie mogą obsłużyć bezpośrednio do centralnego wątku (jednego na grę lub jednego na serwer), który działa w pętli jak pokazano powyżej, z tym wyjątkiem, że rozmawia z wątkami procedury obsługi połączeń, a nie bezpośrednio z klientami. To, czy uznasz to za prostsze czy bardziej skomplikowane niż podejście z jednym wątkiem, zależy od ciebie.

Ilmari Karonen
źródło