Wysyłam sporo danych do iz serwera, dla tworzonej przeze mnie gry.
Obecnie przesyłam takie dane lokalizacji:
sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));
Oczywiście wysyła odpowiednie wartości X, Y i Z.
Czy bardziej efektywne byłoby wysyłanie takich danych?
sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));
networking
joehot200
źródło
źródło
Odpowiedzi:
Segment TCP ma dość dużo narzutu. Gdy wysyłasz wiadomość 10-bajtową z jednym pakietem TCP, faktycznie wysyłasz:
co skutkuje 42 bajtami ruchu do transportu 10 bajtów danych. Wykorzystujesz więc mniej niż 25% dostępnej przepustowości. I to nie uwzględnia jeszcze narzutu, jaki zużywają protokoły niższego poziomu, takie jak Ethernet czy PPPoE (ale trudno je oszacować, ponieważ istnieje wiele alternatyw).
Ponadto wiele małych pakietów bardziej obciąża routery, zapory ogniowe, przełączniki i inny sprzęt infrastruktury sieciowej, więc jeśli Ty, Twój usługodawca i użytkownicy nie inwestujesz w sprzęt wysokiej jakości, może to stać się kolejnym wąskim gardłem.
Z tego powodu powinieneś spróbować wysłać wszystkie dostępne dane naraz w jednym segmencie TCP.
Odnośnie obsługi utraty pakietów : Kiedy używasz TCP, nie musisz się tym martwić. Sam protokół zapewnia, że wszelkie utracone pakiety są wysyłane ponownie, a pakiety są przetwarzane w kolejności, dzięki czemu można założyć, że wszystkie wysłane pakiety dotrą na drugą stronę i dotrą w kolejności, w której je wyślesz. Kosztem tego jest to, że gdy nastąpi utrata pakietu, twój odtwarzacz doświadczy znacznego opóźnienia, ponieważ jeden upuszczony pakiet zatrzyma cały strumień danych, dopóki nie zostanie ponownie zgłoszony i odebrany.
Gdy jest to problem, zawsze możesz użyć UDP. Ale wtedy trzeba znaleźć własne rozwiązanie dla zagubionych i out-of-order wiadomości (to przynajmniej gwarancje, że wiadomości, które mają przyjechać, przybyć kompletny i nieuszkodzony).
źródło
Jeden duży (w granicach rozsądku) jest lepszy.
Jak powiedziałeś, głównym powodem jest utrata pakietów. Pakiety są zazwyczaj wysyłane w ramkach o stałym rozmiarze, dlatego lepiej jest wziąć jedną ramkę z dużym komunikatem niż 10 ramek z 10 małymi.
Jednak w przypadku standardowego protokołu TCP tak naprawdę nie stanowi to problemu, chyba że go wyłączysz. (Nazywa się to algorytmem Nagle i w przypadku gier należy go wyłączyć.) TCP będzie czekał na ustalony limit czasu lub do momentu zapełnienia pakietu. Gdzie „pełne” byłoby jakąś nieco magiczną liczbą, określaną częściowo przez rozmiar ramki.
źródło
recv()
połączenie dla każdegosend()
połączenia, a tego właśnie szuka większość ludzi. Używanie protokołu, który to gwarantuje, podobnie jak UDP. „Gdy wszystko, co masz, to TCP, wszystko wygląda jak strumień”Wszystkie poprzednie odpowiedzi są niepoprawne. W praktyce nie ma znaczenia, czy wykonasz jedno długie
send()
połączenie, czy kilka małychsend()
połączeń.Jak stwierdza Phillip, segment TCP ma pewne narzuty, ale jako programista aplikacji nie masz kontroli nad sposobem generowania segmentów. W prostych słowach:
System operacyjny może całkowicie buforować wszystkie dane i wysyłać je w jednym segmencie lub wziąć długi i podzielić go na kilka małych segmentów.
Ma to kilka implikacji, ale najważniejsze to:
Powodem tego jest to, że TCP jest protokołem strumieniowym . TCP traktuje twoje dane jako długi strumień bajtów i absolutnie nie ma pojęcia „pakietów”. Wraz z
send()
dodaniem bajtów do tego strumienia i za pomocąrecv()
dostajesz bajty z drugiej strony. TCP będzie agresywnie buforować i dzielić twoje dane tam, gdzie uzna to za stosowne, aby upewnić się, że dane dostaną się na drugą stronę tak szybko, jak to możliwe.Jeśli chcesz wysyłać i odbierać „pakiety” za pomocą TCP, musisz zaimplementować znaczniki początku pakietu, znaczniki długości i tak dalej. A może zamiast tego użyć protokołu zorientowanego na wiadomości, takiego jak UDP? UDP gwarantuje, że jedno
send()
połączenie jest tłumaczone na jeden wysłany datagram i na jednorecv()
połączenie!źródło
recv()
, musisz wykonać własne buforowanie, aby to zrekompensować. Oceniłbym to na tej samej trudności, co wdrożenie niezawodności w stosunku do UDP.while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }
(pomijanie obsługi błędów i częściowych odczytów dla zwięzłości, ale niewiele to zmienia). Wykonanie tegoselect
nie zwiększa złożoności, a jeśli używasz protokołu TCP, prawdopodobnie i tak musisz buforować częściowe wiadomości. Wdrożenie solidnej niezawodności w porównaniu z UDP jest znacznie bardziej skomplikowane.Wiele małych paczek jest w porządku. W rzeczywistości, jeśli martwisz się o obciążenie TCP, po prostu wstaw
bufferstream
zbierający do 1500 znaków (lub cokolwiek to jest Twój MTU, najlepiej żądać go dynamicznie) i rozwiąż problem w jednym miejscu. Pozwala to zaoszczędzić około 40 bajtów na każdym dodatkowym pakiecie, który w innym przypadku zostałby utworzony.To powiedziawszy, nadal lepiej jest wysyłać mniej danych, a budowanie większych obiektów pomaga w tym. Oczywiście wysyłanie jest mniejsze
"UID:10|1|2|3
niż wysyłanieUID:10;x:1UID:10;y:2UID:10;z:3
. W rzeczywistości również w tym momencie nie powinieneś wymyślać koła na nowo, użyj biblioteki takiej jak protobuf, która może zmniejszyć takie dane do ciągu 10 bajtów lub mniej.Jedyną rzeczą, o której nie należy zapominać, jest wstawienie
Flush
poleceń do strumienia w odpowiednich lokalizacjach, ponieważ gdy tylko przestaniesz dodawać dane do strumienia, może czekać nieskończenie długo, zanim cokolwiek wyśle. Naprawdę problematyczne, gdy klient czeka na te dane, a serwer nie wysyła niczego nowego, dopóki klient nie wyśle następnej komendy.Utrata paczki może mieć tutaj niewielki wpływ. Każdy wysłany bajt może być potencjalnie uszkodzony, a TCP automatycznie zażąda retransmisji. Mniejsze paczki oznaczają mniejszą szansę na uszkodzenie każdego pakietu, ale ponieważ sumują się one narzut, wysyłasz jeszcze więcej bajtów, jeszcze bardziej zwiększając szanse na zgubienie paczki. Gdy pakiet zostanie utracony, TCP buforuje wszystkie kolejne dane, dopóki brakujący pakiet nie zostanie ponownie wysłany i odebrany. Powoduje to duże opóźnienie (ping). Podczas gdy całkowita utrata przepustowości z powodu utraty pakietu może być nieznaczna, wyższy ping byłby niepożądany w grach.
Konkluzja: Wysyłaj tak mało danych, jak to możliwe, wysyłaj duże paczki i nie pisz własnych metod niskiego poziomu, aby to zrobić, ale polegaj na dobrze znanych bibliotekach i metodach takich jak
bufferstream
i protobuf, aby poradzić sobie z ciężkim podnoszeniem.źródło
bufferstream
jest banalne, dlatego nazwałem to metodą. Nadal chcesz obsłużyć go w jednym miejscu i nie integrować logiki bufora z kodem wiadomości. Jeśli chodzi o Serializację obiektów, bardzo wątpię, aby uzyskać coś lepszego niż tysiące roboczogodzin włożonych w to, nawet jeśli spróbujesz, zdecydowanie sugeruję, byś porównał swoje rozwiązanie ze znanymi implementacjami.Chociaż sam jestem neofita programowania sieciowego, chciałbym podzielić się moim ograniczonym doświadczeniem, dodając kilka punktów:
Jeśli chodzi o pomiary, należy wziąć pod uwagę następujące wskaźniki:
Jak wspomniano, jeśli okaże się, że nie jesteś ograniczony w pewnym sensie i możesz korzystać z UDP, idź na to. Istnieją pewne implementacje oparte na UDP, więc nie musisz wymyślać koła ani pracować w oparciu o lata doświadczenia i sprawdzone doświadczenie. Warto wspomnieć o takich wdrożeniach:
Wniosek: skoro implementacja UDP może przewyższać (trzykrotnie) wartość TCP, warto rozważyć to, kiedy już określisz swój scenariusz jako przyjazny dla UDP. Być ostrzeżonym! Wdrożenie pełnego stosu TCP na UDP jest zawsze złym pomysłem.
źródło