Cos'è meglio? Molti piccoli pacchetti TCP o uno lungo? [chiuso]


16

Sto inviando un bel po 'di dati da e verso un server, per un gioco che sto realizzando.

Attualmente invio dati di posizione come questo:

sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));

Ovviamente sta inviando i rispettivi valori X, Y e Z.

Sarebbe più efficiente inviare dati come questo?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));

2
La perdita di pacchetti di solito è inferiore al 5%, nella mia esperienza limitata.
mucaho,

5
SendToClient invia effettivamente un pacchetto? Se è così, come hai fatto a farlo?
user253751

1
@mucaho Non l'ho mai misurato da solo o niente, ma sono sorpreso che TCP sia così ruvido attorno ai bordi. Avrei sperato in qualcosa di più simile allo 0,5% o meno.
Panzercrisis,

1
@Panzercrisis Devo essere d'accordo con te. Personalmente ritengo che una perdita del 5% sarebbe inaccettabile. Se pensi a qualcosa come me che sta inviando, diciamo, una nuova nave generata nel gioco, anche una probabilità dell'1% di non ricevere quel pacchetto sarebbe disastrosa, perché otterrei navi invisibili.
joehot200,

2
non impazzire ragazzi, intendevo il 5% come limite superiore :) in realtà è molto meglio, come notato da altri commenti.
mucaho,

Risposte:


28

Un segmento TCP ha un sacco di sovraccarico. Quando si invia un messaggio di 10 byte con un pacchetto TCP, si invia effettivamente:

  • 16 byte di intestazione IPv4 (aumenterà a 40 byte quando IPv6 diventa comune)
  • 16 byte di intestazione TCP
  • 10 byte di payload
  • sovraccarico aggiuntivo per i protocolli di collegamento dati e livello fisico utilizzati

con conseguente 42 byte di traffico per il trasporto di 10 byte di dati. Quindi utilizzi solo meno del 25% della larghezza di banda disponibile. E ciò non tiene ancora conto del sovraccarico che consumano i protocolli di livello inferiore come Ethernet o PPPoE (ma questi sono difficili da stimare perché ci sono così tante alternative).

Inoltre, molti piccoli pacchetti mettono a dura prova router, firewall, switch e altre apparecchiature di infrastruttura di rete, quindi quando tu, il tuo fornitore di servizi e i vostri utenti non investite in hardware di alta qualità, ciò potrebbe trasformarsi in un altro collo di bottiglia.

Per questo motivo, dovresti provare a inviare tutti i dati che hai a disposizione contemporaneamente in un segmento TCP.

Per quanto riguarda la gestione della perdita di pacchetti : quando si utilizza TCP non è necessario preoccuparsene. Il protocollo stesso assicura che tutti i pacchetti persi vengano rinviati e i pacchetti vengano elaborati in ordine, quindi puoi presumere che tutti i pacchetti che invii arriveranno dall'altra parte e arriveranno nell'ordine in cui li invii. Il prezzo per questo è che quando c'è una perdita di pacchetti, il tuo giocatore sperimenterà un ritardo considerevole, perché un pacchetto abbandonato interromperà l'intero flusso di dati fino a quando non verrà richiesto e ricevuto nuovamente.

Quando questo è un problema, puoi sempre usare UDP. Ma allora avete bisogno di trovare la propria soluzione per e out-of-order messaggi persi (almeno garantisce che i messaggi che non arrivano, arrivano completa e non danneggiata).


1
Il sovraccarico derivante dal modo in cui TCP recupera la perdita di pacchetti varia in base alle dimensioni dei pacchetti?
Panzercrisis,

@Panzercrisis Solo nella misura in cui esiste un pacchetto più grande che deve essere rinviato.
Philipp

8
Vorrei notare che il sistema operativo applicherà quasi sicuramente Naggor Algorithm en.wikipedia.org/wiki/Nagle%27s_algorithm ai dati in uscita, ciò significa che non importa se nell'app si scrivono o li combinano separati, li combinano prima di trasferirli effettivamente via TCP.
Vality,

1
@Valità La maggior parte delle API di socket che ho usato consentono di attivare o disattivare il nagle per ciascun socket. Per la maggior parte dei giochi, consiglierei di disattivarlo, poiché una bassa latenza è generalmente più importante della conservazione della larghezza di banda.
Philipp

4
L'algoritmo di Nagle è uno, ma non l'unico motivo per cui i dati possono essere bufferizzati sul lato di invio. Non è possibile forzare in modo affidabile l'invio di dati. Inoltre, il buffering / frammentazione può verificarsi ovunque dopo l'invio dei dati, su NAT, router, proxy o persino sul lato ricevente. TCP non fornisce alcuna garanzia in merito alle dimensioni e ai tempi in cui si ricevono i dati, ma solo che arriveranno in ordine e in modo affidabile. Se hai bisogno di garanzie sulle dimensioni, usa UDP. Il fatto che TCP sembra più facile da capire non lo rende lo strumento migliore per tutti i problemi!
Panda Pajama,

10

Uno grande (entro limiti ragionevoli) è meglio.

Come hai detto, la perdita di pacchetti è il motivo principale. I pacchetti vengono generalmente inviati in frame di dimensioni fisse, quindi è meglio occupare un frame con un messaggio grande rispetto a 10 frame con 10 piccoli.

Tuttavia, con TCP standard, questo non è davvero un problema, a meno che non lo si disabiliti. (Si chiama algoritmo di Nagle e per i giochi è necessario disabilitarlo.) TCP attenderà un timeout fisso o fino a quando il pacchetto non sarà "pieno". Dove "pieno" è un numero leggermente magico, determinato in parte dalla dimensione della cornice.


Ho sentito parlare dell'algoritmo di Nagle, ma è davvero una buona idea disabilitarlo? Sono appena arrivato da una risposta StackOverflow in cui qualcuno ha detto che è più efficiente (e per ovvi motivi voglio efficienza).
joehot200

6
@ joehot200 L'unica risposta corretta è "dipende". È più efficiente per l'invio di molti dati, sì, ma non per lo streaming in tempo reale di cui i giochi tendono ad avere bisogno.
Lato D

4
@ joehot200: l'algoritmo di Nagle interagisce male con un algoritmo di riconoscimento ritardato che a volte utilizzano alcune implementazioni TCP. Alcune implementazioni TCP ritarderanno l'invio di un ACK dopo aver ottenuto alcuni dati se si aspettano che seguano altri dati subito dopo (poiché riconoscere il pacchetto successivo riconoscerebbe implicitamente anche il precedente). L'algoritmo di Nagle afferma che un'unità non dovrebbe inviare un pacchetto parziale se ha inviato alcuni dati ma non ha ricevuto un riconoscimento. A volte i due approcci interagiscono male, con ciascuna delle parti in attesa che l'altra faccia qualcosa, fino a quando ...
supercat

2
... entra in funzione un "timer di sanità mentale" e risolve la situazione (nell'ordine di un secondo).
supercat

2
Sfortunatamente, disabilitare l'algoritmo di Nagle non farà nulla per impedire il buffering sull'altro lato host. Disabilitare l'algoritmo di Nagle non garantisce di ricevere una recv()chiamata per ogni send()chiamata, che è ciò che la maggior parte delle persone sta cercando. Utilizzando un protocollo che lo garantisce, come fa UDP. "Quando tutto ciò che hai è TCP, tutto sembra un flusso"
Panda Pajama,

6

Tutte le risposte precedenti sono errate. In pratica, non importa se ne rilasci uno lungosend() chiamata o diverse send()chiamate piccole .

Come afferma Phillip, un segmento TCP ha un certo sovraccarico, ma come programmatore dell'applicazione, non hai alcun controllo su come vengono generati i segmenti. In parole povere:

Una send()chiamata non si traduce necessariamente in un segmento TCP.

Il sistema operativo è completamente gratuito per bufferizzare tutti i tuoi dati e inviarli in un segmento, oppure prendere quello lungo e suddividerlo in diversi piccoli segmenti.

Ciò ha diverse implicazioni, ma la più importante è che:

Una send()chiamata o un segmento TCP non si traduce necessariamente in una recv()chiamata riuscita sull'altra estremità

Il ragionamento alla base di ciò è che TCP è un protocollo stream . TCP tratta i tuoi dati come un lungo flusso di byte e non ha assolutamente alcun concetto di "pacchetti". Con send()te aggiungi byte a quel flusso e con recv()te togli byte dall'altra parte. Il TCP tamponerà e dividerà in modo aggressivo i tuoi dati ovunque ritengano opportuno per assicurarti che i tuoi dati arrivino dall'altra parte il più velocemente possibile.

Se si desidera inviare e ricevere "pacchetti" con TCP, è necessario implementare marcatori di inizio pacchetto, marcatori di lunghezza e così via. Che ne dici di usare un protocollo orientato ai messaggi come UDP invece? UDP ne garantisce unosend() chiamata si traduce in un datagramma inviato e in una recv()chiamata!

Quando tutto ciò che hai è TCP, tutto sembra un flusso


1
Prefissare ogni messaggio con una lunghezza del messaggio non è così complicato.
ysdx,

Hai un interruttore da girare quando si tratta di aggregazione di pacchetti, indipendentemente dal fatto che l'algoritmo di Nagle sia attivo o meno. Non è raro che sia disattivato nella rete di gioco per garantire la pronta consegna di pacchetti insufficienti.
Lars Viklund,

Questo è completamente OS o persino specifico della libreria. Inoltre, hai un sacco di controllo, se vuoi. È vero che non hai il controllo totale, a TCP è sempre permesso combinare due messaggi o dividerne uno se non si adatta all'MTU, ma puoi comunque suggerirlo nella giusta direzione. Impostazione di varie impostazioni di configurazione, invio manuale di messaggi a 1 secondo di distanza o buffering dei dati e invio in una sola volta.
Dorus,

@ysdx: no, non sul lato mittente, ma sì sul lato ricevente. Dal momento che non hai garanzie su dove esattamente otterrai i dati recv(), devi compensare il tuo buffering. Lo classificherei nella stessa difficoltà dell'implementazione dell'affidabilità su UDP.
Panda Pajama,

@Pagnda Pajama: un'implementazione ingenua del lato ricevente è: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(omettendo la gestione degli errori e letture parziali per brevità, ma non cambia molto). In questo modo selectnon si aggiunge molta più complessità e se si utilizza TCP probabilmente è necessario bufferizzare i messaggi parziali comunque. L'implementazione dell'affidabilità affidabile su UDP è molto più complicata di così.
ysdx,

3

Molti piccoli pacchetti vanno bene. In effetti, se sei preoccupato per l'overhead TCP, inserisci abufferstream che raccoglie fino a 1500 caratteri (o qualunque sia il tuo MTU TCP, meglio richiederlo dinamicamente), e affronta il problema in un unico posto. In questo modo risparmi il sovraccarico di ~ 40 byte per ogni pacchetto aggiuntivo che altrimenti avresti creato.

Detto questo, è ancora meglio inviare meno dati e creare oggetti più grandi aiuta lì. Ovviamente è più piccolo inviare "UID:10|1|2|3che inviare UID:10;x:1UID:10;y:2UID:10;z:3. In effetti, anche a questo punto non dovresti reinventare la ruota, usa una libreria come protobuf che può ridurre tali dati a una stringa di 10 byte o meno.

L'unica cosa che non dovresti dimenticare è inserire un Flushcomando sul tuo stream in posizioni pertinenti, perché non appena smetti di aggiungere dati al tuo stream, potrebbe aspettare infinito prima di inviare qualcosa. Davvero problematico quando il tuo client è in attesa di quei dati e il tuo server non invierà nulla di nuovo fino a quando il client non invierà il comando successivo.

La perdita del pacchetto è qualcosa che puoi influenzare qui, marginalmente. Ogni byte inviato può essere potenzialmente danneggiato e TCP richiederà automaticamente una ritrasmissione. Pacchetti più piccoli significano una minore possibilità di corruzione per ogni singolo pacchetto, ma poiché si sommano sull'overhead, si inviano ancora più byte, aumentando ulteriormente le probabilità di un pacchetto perso. Quando un pacchetto viene perso, TCP memorizzerà in buffer tutti i dati successivi fino a quando il pacchetto mancante viene rinviato e ricevuto. Ciò comporta un grande ritardo (ping). Mentre la perdita totale della larghezza di banda a causa della perdita del pacchetto potrebbe essere trascurabile, il ping più elevato sarebbe indesiderabile per i giochi.

Conclusione: inviare il minor numero possibile di dati, inviare pacchetti di grandi dimensioni e non scrivere i propri metodi di basso livello per farlo, ma fare affidamento su librerie e metodi noti come bufferstreame protobuf per gestire il sollevamento di carichi pesanti.


In realtà per cose semplici come questa è facile da realizzare. Molto più facile che passare attraverso una documentazione di 50 pagine per usare la libreria di un'altra persona, e poi dopo devi ancora affrontare i loro bug e problemi.
Pacerier,

È vero, scrivere il tuo bufferstreamè banale, ecco perché l'ho chiamato un metodo. Vuoi ancora gestirlo in un unico posto e non integrare la logica del buffer con il codice del messaggio. Per quanto riguarda la serializzazione degli oggetti, dubito fortemente che tu ottenga qualcosa di meglio delle migliaia di ore lavorative che gli altri inseriscono, anche se ci provi, ti consiglio vivamente di confrontare la tua soluzione con implementazioni note.
Dorus,

2

Pur essendo un neofita per programmare in rete me stesso, vorrei condividere la mia esperienza limitata aggiungendo alcuni punti:

  • TCP implica un sovraccarico: è necessario misurare le statistiche pertinenti
  • UDP è la soluzione di fatto per gli scenari di gioco in rete, ma tutte le implementazioni che si basano su di esso hanno un algoritmo aggiuntivo sul lato CPU per tenere conto dei pacchetti persi o inviati fuori servizio

Per quanto riguarda le misurazioni, le metriche da considerare sono:

Come accennato, se scopri che non sei limitato in un certo senso e potresti usare UDP, prova questo. Ci sono alcune implementazioni basate su UDP là fuori, quindi non devi reinventare la ruota o lavorare contro anni di esperienza e comprovata esperienza. Tali implementazioni degne di nota sono:

Conclusione: poiché un'implementazione UDP potrebbe superare (di un fattore 3x) una TCP, ha senso prenderla in considerazione, una volta identificato lo scenario come compatibile con UDP. Stai attento! L'implementazione dell'intero stack TCP su UDP è sempre una cattiva idea.


Stavo usando UDP. Sono appena passato a TCP. La perdita di pacchetti di UDP era semplicemente inaccettabile per i dati cruciali di cui il cliente aveva bisogno. Posso inviare dati di movimento tramite UDP.
joehot200,

Quindi, le cose migliori che puoi fare sono: in effetti, usa TCP solo per operazioni cruciali O usa un'implementazione del protocollo software basato su UDP (con Enet semplice e UDT ben testato). Ma prima, misura la perdita e decidi se UDT ti porterebbe un vantaggio.
Teodron,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.