Qu'est-ce qui est mieux? Beaucoup de petits paquets TCP, ou un long? [fermé]

16

J'envoie pas mal de données vers et depuis un serveur, pour un jeu que je fais.

J'envoie actuellement des données de localisation comme celle-ci:

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

De toute évidence, il envoie les valeurs respectives X, Y et Z.

Serait-il plus efficace d'envoyer des données comme celle-ci?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));
joehot200
la source
2
La perte de paquets est généralement inférieure à 5%, selon mon expérience limitée.
mucaho
5
SendToClient envoie-t-il réellement un paquet? Si oui, comment avez-vous fait pour cela?
user253751
1
@mucaho Je ne l'ai jamais mesuré ou quoi que ce soit, mais je suis surpris que TCP soit si rude sur les bords. J'aurais espéré quelque chose de plus comme 0,5% ou moins.
Panzercrisis
1
@Panzercrisis Je dois être d'accord avec vous. Je pense personnellement qu'une perte de 5% serait inacceptable. Si vous pensez à quelque chose comme moi qui envoie, disons, un nouveau vaisseau engendré dans le jeu, même une chance de 1% que ce paquet ne soit pas reçu serait désastreuse, car j'obtiendrais des vaisseaux invisibles.
joehot200
2
ne paniquez pas les gars, je parlais des 5% comme limite supérieure :) en réalité, c'est beaucoup mieux, comme l'ont noté d'autres commentaires.
mucaho

Réponses:

28

Un segment TCP a beaucoup de surcharge. Lorsque vous envoyez un message de 10 octets avec un paquet TCP, vous envoyez réellement:

  • 16 octets d'en-tête IPv4 (passera à 40 octets lorsque IPv6 deviendra commun)
  • 16 octets d'en-tête TCP
  • 10 octets de charge utile
  • surcharge supplémentaire pour les protocoles de liaison de données et de couche physique utilisés

résultant en 42 octets de trafic pour le transport de 10 octets de données. Vous n'utilisez donc que moins de 25% de votre bande passante disponible. Et cela ne tient pas encore compte de la surcharge que consomment les protocoles de bas niveau comme Ethernet ou PPPoE (mais ceux-ci sont difficiles à estimer car il existe de nombreuses alternatives).

En outre, de nombreux petits paquets mettent davantage à rude épreuve les routeurs, les pare-feu, les commutateurs et les autres équipements d'infrastructure réseau.Par conséquent, lorsque vous, votre fournisseur de services et vos utilisateurs n'investissez pas dans du matériel de haute qualité, cela pourrait se transformer en un autre goulot d'étranglement.

Pour cette raison, vous devriez essayer d'envoyer toutes les données dont vous disposez en une seule fois dans un segment TCP.

Concernant la gestion de la perte de paquets : lorsque vous utilisez TCP, vous n'avez pas à vous en soucier. Le protocole lui-même garantit que tous les paquets perdus sont renvoyés et que les paquets sont traités dans l'ordre, vous pouvez donc supposer que tous les paquets que vous envoyez arriveront de l'autre côté, et ils arriveront dans l'ordre dans lequel vous les envoyez. Le prix à payer est que, en cas de perte de paquets, votre lecteur subira un décalage considérable, car un paquet abandonné arrêtera tout le flux de données jusqu'à ce qu'il soit demandé et reçu à nouveau.

En cas de problème, vous pouvez toujours utiliser UDP. Mais alors vous devez trouver votre propre solution pour la perte et hors ordre des messages (au moins la garantie que les messages qui n'arrivent, arrivent complets et en bon état).

Philipp
la source
1
La surcharge de la façon dont TCP récupère la perte de paquets varie-t-elle en fonction de la taille des paquets?
Panzercrisis
@Panzercrisis Seulement dans la mesure où il existe un plus gros paquet qui doit être renvoyé.
Philipp
8
Je noterais que le système d'exploitation appliquera presque certainement l'algorithme Nagles en.wikipedia.org/wiki/Nagle%27s_algorithm aux données sortantes, cela signifie que peu importe si dans l'application vous effectuez des écritures séparées ou les combinez, il les combinera avant de les distribuer via TCP.
Vality
1
@Vality La plupart des API de socket que j'ai utilisées permettent d'activer ou de désactiver le nagle pour chaque socket. Pour la plupart des jeux, je recommanderais de le désactiver, car une faible latence est généralement plus importante que la conservation de la bande passante.
Philipp
4
L'algorithme de Nagle en est un, mais ce n'est pas la seule raison pour laquelle les données peuvent être tamponnées du côté de l'envoi. Il n'y a aucun moyen de forcer de manière fiable l'envoi de données. En outre, la mise en mémoire tampon / fragmentation peut se produire n'importe où après l'envoi des données, soit au niveau des NAT, des routeurs, des proxys, ou même du côté de la réception. TCP ne donne aucune garantie concernant la taille et le moment auquel vous recevez les données, mais seulement qu'elles arriveront dans l'ordre et de manière fiable. Si vous avez besoin de garanties de taille, utilisez UDP. Le fait que TCP semble plus facile à comprendre n'en fait pas le meilleur outil pour tous les problèmes!
Panda Pyjama
10

Un grand (dans des limites raisonnables) est préférable.

Comme vous l'avez dit, la perte de paquets est la principale raison. Les paquets sont généralement envoyés dans des trames de taille fixe, il est donc préférable de prendre une trame avec un gros message que 10 trames avec 10 petites.

Cependant, avec TCP standard, ce n'est pas vraiment un problème, sauf si vous le désactivez. (Il s'agit de l'algorithme de Nagle , et pour les jeux, vous devez le désactiver.) TCP attendra un délai fixe ou jusqu'à ce que le package soit "plein". Où "plein" est un nombre légèrement magique, déterminé en partie par la taille du cadre.

Elva
la source
J'ai entendu parler de l'algorithme de Nagle, mais est-ce vraiment une bonne idée de le désactiver? Je viens tout juste d'une réponse StackOverflow où quelqu'un a dit que c'était plus efficace (et pour des raisons évidentes, je veux l'efficacité).
joehot200
6
@ joehot200 La seule bonne réponse à cela est "ça dépend". Il est plus efficace pour envoyer de nombreuses données, oui, mais pas pour le streaming en temps réel dont les jeux ont tendance à avoir besoin.
Côté D du
4
@ joehot200: l'algorithme de Nagle interagit mal avec un algorithme à accusé de réception retardé que certaines implémentations TCP utilisent parfois. Certaines implémentations TCP retarderont l'envoi d'un ACK après avoir obtenu des données si elles s'attendent à ce que plus de données suivent peu de temps après (puisque la reconnaissance du dernier paquet reconnaîtrait implicitement le premier également). L'algorithme de Nagle dit qu'une unité ne devrait pas envoyer un paquet partiel si elle a envoyé des données mais n'a pas entendu d'accusé de réception. Parfois, les deux approches interagissent mal, chaque partie attendant que l'autre fasse quelque chose, jusqu'à ce que ...
supercat
2
... une "minuterie d'esprit" se déclenche et résout la situation (de l'ordre d'une seconde).
supercat
2
Malheureusement, la désactivation de l'algorithme de Nagle ne fera rien pour empêcher la mise en mémoire tampon de l'autre côté de l'hôte. La désactivation de l'algorithme de Nagle ne garantit pas que vous recevez un recv()appel pour chaque send()appel, ce que la plupart des gens recherchent. Utiliser un protocole qui garantit cela, comme le fait UDP. "Lorsque tout ce que vous avez est TCP, tout ressemble à un flux"
Panda Pyjama
6

Toutes les réponses précédentes sont incorrectes. En pratique, peu importe que vous émettiez un send()appel long ou plusieurs petitssend() appels.

Comme l'indique Phillip, un segment TCP a des frais généraux, mais en tant que programmeur d'application, vous n'avez aucun contrôle sur la façon dont les segments sont générés. En termes simples:

Un send()appel ne se traduit pas nécessairement par un segment TCP.

L'OS est totalement gratuit mettre en mémoire tampon toutes vos données et de les envoyer en un seul segment, ou de prendre le long et de le diviser en plusieurs petits segments.

Cela a plusieurs implications, mais la plus importante est la suivante:

Un send()appel ou un segment TCP ne se traduit pas nécessairement par un recv()appel réussi à l'autre extrémité

Le raisonnement derrière cela est que TCP est un protocole de flux . TCP traite vos données comme un long flux d'octets et n'a absolument aucun concept de "paquets". Avecsend() vous ajoutez des octets à ce flux, et avec recv()vous obtenez des octets de l'autre côté. TCP tamponnera et divisera vos données de manière agressive là où il le jugera bon pour vous assurer que vos données parviennent de l'autre côté aussi rapidement que possible.

Si vous souhaitez envoyer et recevoir des "paquets" avec TCP, vous devez implémenter des marqueurs de début de paquet, des marqueurs de longueur, etc. Que diriez-vous d'utiliser un protocole orienté message comme UDP à la place? UDP garantit qu'un send()appel se traduit par un datagramme envoyé et par un recv()appel!

Lorsque tout ce que vous avez est TCP, tout ressemble à un flux

Pyjama Panda
la source
1
Préfixer chaque message avec une longueur de message n'est pas si difficile.
ysdx
Vous avez un commutateur à inverser en ce qui concerne l'agrégation de paquets, que l'algorithme de Nagle soit en vigueur ou non. Il n'est pas rare qu'il soit désactivé dans le réseau de jeu pour assurer une livraison rapide des paquets sous-remplis.
Lars Viklund
Ceci est complètement spécifique au système d'exploitation ou même à la bibliothèque. Vous avez également beaucoup de contrôle - si vous le souhaitez. Il est vrai que vous n'avez pas un contrôle total, TCP est toujours autorisé à combiner deux messages, ou à en diviser un s'il ne correspond pas au MTU, mais vous pouvez toujours le suggérer dans la bonne direction. Définition de divers paramètres de configuration, envoi manuel de messages à 1 seconde d'intervalle ou mise en mémoire tampon des données et envoi en une seule fois
Dorus
@ysdx: non, pas du côté émetteur, mais oui du côté récepteur. Comme vous n'avez aucune garantie sur l'endroit exact où vous obtiendrez les données recv(), vous devez créer votre propre tampon pour compenser cela. Je le classerais dans la même difficulté que la mise en œuvre de la fiabilité sur UDP.
Panda Pyjama
@Pagnda Pyjama: Une implémentation naïve du côté récepteur est la suivante: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(en omettant la gestion des erreurs et les lectures partielles par souci de concision, mais cela ne change pas grand-chose). Faire cela selectn'ajoute pas beaucoup plus de complexité et si vous utilisez TCP, vous devrez probablement tamponner les messages partiels de toute façon. La mise en œuvre d'une fiabilité robuste sur UDP est beaucoup plus compliquée que cela.
ysdx
3

Beaucoup de petits paquets sont très bien. En fait, si vous êtes préoccupé par la surcharge TCP, insérez simplement un bufferstreamqui collecte jusqu'à 1500 caractères (ou quel que soit votre MTU TCP, il est préférable de le demander dynamiquement), et traitez le problème en un seul endroit. Cela vous épargne la surcharge de ~ 40 octets pour chaque paquet supplémentaire que vous auriez autrement créé.

Cela dit, il est toujours préférable d'envoyer moins de données, et la construction d'objets plus gros y contribue. Bien sûr, il est plus petit d'envoyer "UID:10|1|2|3que d'envoyer UID:10;x:1UID:10;y:2UID:10;z:3. En fait, à ce stade, vous ne devriez pas non plus réinventer la roue, utilisez une bibliothèque comme protobuf qui peut réduire des données comme celle-ci à une chaîne de 10 octets ou moins.

La seule chose que vous ne devez pas oublier est d'insérer un Flush commande sur votre flux aux emplacements appropriés, car dès que vous arrêtez d'ajouter des données à votre flux, il peut attendre infini avant d'envoyer quoi que ce soit. Vraiment problématique lorsque votre client attend ces données, et votre serveur n'enverra rien de nouveau jusqu'à ce que le client envoie la commande suivante.

La perte de colis est quelque chose que vous pouvez affecter ici, de manière marginale. Chaque octet que vous envoyez peut être potentiellement corrompu, et TCP demandera automatiquement une retransmission. Des packages plus petits signifient une chance moindre pour chaque package d'être corrompu, mais parce qu'ils s'additionnent sur la surcharge, vous envoyez encore plus d'octets, augmentant encore plus les chances d'un package perdu. Lorsqu'un package est perdu, TCP mettra en mémoire tampon toutes les données suivantes jusqu'à ce que le package manquant soit renvoyé et reçu. Cela entraîne un retard important (ping). Bien que la perte totale de bande passante en raison de la perte de package puisse être négligeable, le ping plus élevé ne serait pas souhaitable pour les jeux.

Conclusion: envoyez aussi peu de données que possible, envoyez de gros paquets et n'écrivez pas vos propres méthodes de bas niveau pour le faire, mais comptez sur des bibliothèques et des méthodes bien connues comme bufferstreamet protobuf pour gérer le gros du travail.

Dorus
la source
En fait, pour des choses simples comme celle-ci, il est facile de rouler le vôtre. Beaucoup plus facile que de parcourir une documentation de 50 pages pour utiliser la bibliothèque d'une autre personne, puis après cela, vous devez toujours faire face à leurs bugs et à leurs problèmes.
Pacerier
Certes, écrire la vôtre bufferstreamest trivial, c'est pourquoi je l'ai appelé une méthode. Vous souhaitez toujours le gérer en un seul endroit et ne pas intégrer votre logique de tampon à votre code de message. En ce qui concerne la sérialisation d'objets, je doute fortement que vous obteniez quelque chose de mieux que les milliers d'heures de travail que d'autres y mettent, même si vous essayez, je vous suggère fortement de comparer votre solution avec les implémentations connues.
Dorus
2

Bien qu'étant néophyte de la programmation réseau moi-même, je voudrais partager mon expérience limitée en ajoutant quelques points:

  • TCP implique un surcoût - vous devez mesurer les statistiques pertinentes
  • UDP est la solution de facto pour les scénarios de jeu en réseau, mais toutes les implémentations qui en dépendent ont un algorithme supplémentaire côté processeur pour tenir compte des paquets perdus ou envoyés dans le désordre

Concernant les mesures, les métriques à considérer sont:

Comme mentionné, si vous découvrez que vous n'êtes pas limité dans un sens et que vous pouvez utiliser UDP, allez-y. Il existe des implémentations basées sur UDP, vous n'avez donc pas à réinventer la roue ou à travailler contre des années d'expérience et une expérience éprouvée. Ces implémentations qui méritent d'être mentionnées sont:

Conclusion: étant donné qu'une implémentation UDP pourrait surpasser (par un facteur de 3x) une implémentation TCP, il est logique de la considérer, une fois que vous avez identifié votre scénario comme compatible UDP. Être averti! L'implémentation de la pile TCP complète au-dessus d'UDP est toujours une mauvaise idée.

teodron
la source
J'utilisais UDP. Je viens de passer à TCP. La perte de paquets UDP était tout simplement inacceptable pour les données cruciales dont le client avait besoin. Je peux envoyer des données de mouvement via UDP.
joehot200
Ainsi, les meilleures choses que vous pouvez faire sont: en effet, utilisez TCP uniquement pour des opérations cruciales OU utilisez une implémentation de protocole logiciel basée sur UDP (avec Enet étant simple et UDT étant bien testé). Mais d'abord, mesurez la perte et décidez si l'UDT vous apportera un avantage.
teodron