Co jest lepsze? Dużo małych pakietów TCP, czy jeden długi? [Zamknięte]

16

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)));
joehot200
źródło
2
Z mojego ograniczonego doświadczenia wynika, że ​​utrata pakietów wynosi zwykle poniżej 5%.
mucaho,
5
Czy sendToClient faktycznie wysyła pakiet? Jeśli tak, jak to zrobiłeś?
user253751
1
@mucaho Nigdy tego nie mierzyłem ani nic, ale jestem zaskoczony, że TCP jest tak szorstki na brzegach. Chciałbym mieć coś w rodzaju 0,5% lub mniej.
Panzercrisis,
1
@Panzercrisis Muszę się z tobą zgodzić. Osobiście uważam, że 5% strata byłaby nie do przyjęcia. Jeśli pomyślisz o czymś takim, jak wysłanie przeze mnie, powiedzmy, nowego statku odrodzonego w grze, nawet 1% szansy na brak otrzymania tego pakietu byłoby katastrofalne, ponieważ dostałbym niewidzialne statki.
joehot200
2
nie wariujcie facetów, miałem na myśli 5% jako górną granicę :) w rzeczywistości jest o wiele lepiej, jak zauważają inne komentarze.
mucaho,

Odpowiedzi:

28

Segment TCP ma dość dużo narzutu. Gdy wysyłasz wiadomość 10-bajtową z jednym pakietem TCP, faktycznie wysyłasz:

  • 16 bajtów nagłówka IPv4 (zwiększy się do 40 bajtów, gdy IPv6 stanie się powszechny)
  • 16 bajtów nagłówka TCP
  • 10 bajtów ładunku
  • dodatkowy narzut dla używanych protokołów łącza danych i warstwy fizycznej

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).

Philipp
źródło
1
Czy narzut związany ze sposobem, w jaki protokół TCP odzyskuje utratę pakietów, jest różny w zależności od wielkości pakietów?
Panzercrisis,
@Panzercrisis Tylko w takim stopniu, w jakim istnieje większy pakiet, który należy wysłać ponownie.
Philipp
8
Chciałbym zauważyć, że system operacyjny prawie na pewno zastosuje algorytm Nagles en.wikipedia.org/wiki/Nagle%27s_alnodm do danych wychodzących, co oznacza, że ​​nie ma znaczenia, czy w aplikacji wykonujesz osobne zapisy, czy je łączysz, połączy je zanim faktycznie przekażą je za pośrednictwem TCP.
Vality
1
@Vality Większość interfejsów API gniazd, z których korzystałem, pozwala aktywować lub dezaktywować nagle dla każdego gniazda. W przypadku większości gier zalecałbym jego dezaktywację, ponieważ małe opóźnienie jest zwykle ważniejsze niż zachowanie przepustowości.
Philipp
4
Algorytm Nagle jest jeden, ale nie jedyny powód, dla którego dane mogą być buforowane po stronie wysyłającej. Nie ma sposobu, aby wiarygodnie wymusić wysyłanie danych. Również buforowanie / fragmentacja może wystąpić w dowolnym miejscu po wysłaniu danych, albo w NAT, routerach, serwerach proxy, a nawet po stronie odbierającej. TCP nie daje żadnych gwarancji co do wielkości i czasu, w którym otrzymujesz dane, tylko, że dotrą one w porządku i niezawodnie. Jeśli potrzebujesz gwarancji rozmiaru, użyj UDP. Fakt, że TCP wydaje się łatwiejszy do zrozumienia, nie czyni go najlepszym narzędziem do rozwiązywania wszystkich problemów!
Panda Pajama,
10

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.

Elva
źródło
Słyszałem o algorytmie Nagle, ale czy naprawdę warto go wyłączyć? Właśnie przyszedłem z odpowiedzi StackOverflow, gdzie ktoś powiedział, że jest bardziej wydajny (i z oczywistych powodów chcę wydajności).
joehot200
6
@ joehot200 Jedyną prawidłową odpowiedzią jest „to zależy”. Jest bardziej efektywny w wysyłaniu dużej ilości danych, tak, ale nie w przypadku przesyłania strumieniowego w czasie rzeczywistym, którego gry potrzebują.
D
4
@ joehot200: Algorytm Nagle słabo współdziała z algorytmem opóźnionego potwierdzenia, którego czasami używają niektóre implementacje TCP. Niektóre implementacje TCP opóźnią wysłanie ACK po otrzymaniu danych, jeśli spodziewają się, że wkrótce pojawi się więcej danych (ponieważ potwierdzenie późniejszego pakietu domyślnie potwierdzi także wcześniejszy). Algorytm Nagle mówi, że jednostka nie powinna wysyłać częściowego pakietu, jeśli wysłała jakieś dane, ale nie słyszała potwierdzenia. Czasami oba podejścia źle się ze sobą
łączą
2
... uruchamia się „zegar rozsądku” i rozwiązuje sytuację (w kolejności sekundowej).
supercat,
2
Niestety wyłączenie algorytmu Nagle nie zrobi nic, aby zapobiec buforowaniu po drugiej stronie hosta. Wyłączenie algorytmu Nagle nie gwarantuje, że otrzymasz jedno recv()połączenie dla każdego send()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ń”
Panda Pajama
6

Wszystkie poprzednie odpowiedzi są niepoprawne. W praktyce nie ma znaczenia, czy wykonasz jedno długie send()połączenie, czy kilka małych send()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:

Jedno send()połączenie niekoniecznie przekłada się na jeden segment TCP.

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:

Jedno send()połączenie lub jeden segment TCP niekoniecznie przekłada się na jedno udane recv()połączenie na drugim końcu

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 jedno recv()połączenie!

Gdy wszystko, co masz, to TCP, wszystko wygląda jak strumień

Panda Piżama
źródło
1
Prefiksowanie każdej wiadomości długością wiadomości nie jest takie trudne.
ysdx
Masz jeden przełącznik do przełączania, jeśli chodzi o agregację pakietów, niezależnie od tego, czy działa algorytm Nagle. Często zdarza się, że jest wyłączony z sieci, aby zapewnić szybką dostawę niedopełnionych pakietów.
Lars Viklund,
Jest to całkowicie specyficzne dla systemu operacyjnego, a nawet biblioteki. Ty też masz dużo kontroli - jeśli chcesz. To prawda, że ​​nie masz całkowitej kontroli, TCP zawsze może łączyć dwie wiadomości lub dzielić jeden, jeśli nie pasuje do MTU, ale nadal możesz podpowiedzieć we właściwym kierunku. Konfigurowanie różnych ustawień konfiguracji, ręczne wysyłanie wiadomości w odstępie 1 sekundy lub buforowanie danych i wysyłanie ich jednym strzałem.
Dorus,
@ysdx: nie, nie po stronie wysyłającej, ale tak po stronie odbierającej. Ponieważ nie masz gwarancji, gdzie dokładnie będziesz otrzymywać dane 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.
Panda Pajama,
@Pagnda Pajama: Naiwną implementacją strony odbierającej jest: 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 tego selectnie 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.
ysdx
3

Wiele małych paczek jest w porządku. W rzeczywistości, jeśli martwisz się o obciążenie TCP, po prostu wstawbufferstream 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|3niż wysyłanie UID: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 bufferstreami protobuf, aby poradzić sobie z ciężkim podnoszeniem.

Dorus
źródło
W rzeczywistości w przypadku prostych rzeczy takich jak ta jest łatwa do zrolowania. O wiele łatwiej niż przejrzeć 50-stronicową dokumentację, aby korzystać z biblioteki innej osoby, a potem nadal musisz poradzić sobie z jej błędami i problemami.
Pacerier
To prawda, że ​​pisanie własnego bufferstreamjest 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.
Dorus,
2

Chociaż sam jestem neofita programowania sieciowego, chciałbym podzielić się moim ograniczonym doświadczeniem, dodając kilka punktów:

  • TCP oznacza narzut - musisz zmierzyć odpowiednie statystyki
  • UDP jest de facto rozwiązaniem dla scenariuszy gier sieciowych, ale wszystkie implementacje, które na nim polegają, mają dodatkowy algorytm po stronie procesora, aby uwzględnić utratę pakietów lub wysłanie ich poza kolejnością

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.

teodron
źródło
Korzystałem z UDP. Właśnie przełączyłem się na TCP. Utrata pakietu UDP była po prostu nie do przyjęcia dla kluczowych danych potrzebnych klientowi. Mogę wysyłać dane o ruchu za pośrednictwem UDP.
joehot200
Tak więc najlepsze rzeczy, które możesz zrobić to: rzeczywiście, używaj TCP tylko do kluczowych operacji LUB używaj implementacji protokołu opartego na UDP (Enet jest prosty, a UDT dobrze przetestowany). Ale najpierw zmierz stratę i zdecyduj, czy UDT przyniesie ci przewagę.
teodron