O que é melhor? Muitos pacotes TCP pequenos, ou um longo? [fechadas]

16

Estou enviando bastante dados de e para um servidor, para um jogo que estou criando.

Atualmente, envio dados de localização como este:

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

Obviamente, ele está enviando os respectivos valores X, Y e Z.

Seria mais eficiente enviar dados como este?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));
joehot200
fonte
2
A perda de pacotes geralmente é inferior a 5%, na minha experiência limitada.
Mucaho 19/03/2015
5
O sendToClient realmente envia um pacote? Se sim, como você fez isso?
user253751
1
@mucaho Eu nunca medi eu mesmo ou algo assim, mas estou surpreso que o TCP seja tão duro assim. Eu esperava algo mais como 0,5% ou menos.
Panzercrisis
1
@ Panzercrisis Devo concordar com você. Pessoalmente, sinto que uma perda de 5% seria inaceitável. Se você pensar em algo como eu enviando, digamos, uma nova nave gerada no jogo, mesmo uma chance de 1% desse pacote não ser recebido seria desastrosa, porque eu receberia naves invisíveis.
joehot200
2
não surte caras, eu quis dizer os 5% como um limite superior :) na realidade, é muito melhor, como observado por outros comentários.
Mucaho 20/03/2015

Respostas:

28

Um segmento TCP possui muita sobrecarga. Quando você envia uma mensagem de 10 bytes com um pacote TCP, você realmente envia:

  • 16 bytes de cabeçalho IPv4 (aumentará para 40 bytes quando o IPv6 se tornar comum)
  • 16 bytes do cabeçalho TCP
  • 10 bytes de carga útil
  • sobrecarga adicional para os protocolos de link de dados e camada física usados

resultando em 42 bytes de tráfego para transportar 10 bytes de dados. Portanto, você utiliza apenas menos de 25% de sua largura de banda disponível. E isso ainda não explica a sobrecarga que os protocolos de nível inferior, como Ethernet ou PPPoE, consomem (mas é difícil estimar porque há muitas alternativas).

Além disso, muitos pacotes pequenos sobrecarregam roteadores, firewalls, comutadores e outros equipamentos de infraestrutura de rede; portanto, quando você, seu provedor de serviços e seus usuários não investem em hardware de alta qualidade, isso pode se transformar em outro gargalo.

Por esse motivo, você deve tentar enviar todos os dados disponíveis ao mesmo tempo em um segmento TCP.

Sobre o tratamento da perda de pacotes : Quando você usa o TCP, não precisa se preocupar com isso. O protocolo em si garante que todos os pacotes perdidos sejam reenviados e os pacotes sejam processados ​​em ordem, para que você possa assumir que todos os pacotes enviados chegarão ao outro lado e eles chegarão na ordem em que você os enviar. O preço para isso é que, quando houver perda de pacotes, seu player sofrerá um atraso considerável, porque um pacote descartado interromperá todo o fluxo de dados até que seja novamente solicitado e recebido.

Quando isso ocorre, você sempre pode usar o UDP. Mas então você precisa encontrar sua própria solução para e mensagens perdidas fora de ordem (que, pelo menos, garantias de que as mensagens que não chegam, chegam completa e sem danos).

Philipp
fonte
1
A sobrecarga da maneira como o TCP recupera a perda de pacotes varia de acordo com o tamanho dos pacotes?
Panzercrisis 19/03/2015
@ Panzercrisis Somente na medida em que exista um pacote maior que precise ser reenviado.
Philipp
8
Eu observaria que o sistema operacional quase certamente aplicará o Nagles Algorithm en.wikipedia.org/wiki/Nagle%27s_algorithm aos dados enviados, o que significa que não importa se, no aplicativo, você separa as gravações ou as combina, elas serão combinadas antes de realmente distribuí-los via TCP.
Vality 19/03/2015
1
@Vality A maioria das APIs de soquete que usei permitem ativar ou desativar nagle para cada soquete. Para a maioria dos jogos, eu recomendaria desativá-lo, porque a baixa latência é geralmente mais importante do que conservar a largura de banda.
Philipp
4
O algoritmo de Nagle é um, mas não é o único motivo pelo qual os dados podem ser armazenados em buffer no lado de envio. Não há como forçar o envio confiável de dados. Além disso, o buffer / fragmentação pode ocorrer em qualquer lugar após o envio dos dados, em NATs, roteadores, proxies ou até no lado de recebimento. O TCP não oferece nenhuma garantia em relação ao tamanho e ao momento em que você recebe dados, apenas para que eles cheguem em ordem e de forma confiável. Se você precisar de garantias de tamanho, use UDP. O fato de o TCP parecer mais fácil de entender não o torna a melhor ferramenta para todos os problemas!
Panda Pyjama
10

Um grande (dentro da razão) é melhor.

Como você disse, a perda de pacotes é o principal motivo. Os pacotes geralmente são enviados em quadros de tamanho fixo; portanto, é melhor ocupar um quadro com uma mensagem grande do que 10 quadros com 10 pequenos.

No entanto, com o TCP padrão, isso não é realmente um problema, a menos que você o desative. (Ele é chamado algoritmo de Nagle e, para jogos, você deve desativá-lo.) O TCP aguardará um tempo limite fixo ou até que o pacote esteja "cheio". Onde "cheio" é um número ligeiramente mágico, determinado em parte pelo tamanho do quadro.

Elva
fonte
Já ouvi falar do algoritmo de Nagle, mas é realmente uma boa ideia desabilitá-lo? Acabei de chegar de uma resposta do StackOverflow em que alguém disse que é mais eficiente (e por razões óbvias, quero eficiência).
joehot200
6
@ joehot200 A única resposta correta para isso é "depende". É mais eficiente para enviar muitos dados, sim, mas não para streaming em tempo real de que os jogos tendem a precisar.
D-side
4
@ joehot200: O algoritmo de Nagle interage mal com um algoritmo de reconhecimento de atraso que algumas implementações de TCP às vezes usam. Algumas implementações de TCP atrasam o envio de um ACK depois que eles obtêm alguns dados, se esperarem que mais dados sejam seguidos logo depois (já que o reconhecimento do pacote posterior também implicitamente reconheceria o anterior). O algoritmo de Nagle diz que uma unidade não deve enviar um pacote parcial se enviou alguns dados, mas não ouviu um reconhecimento. Às vezes, as duas abordagens interagem mal, com cada uma das partes esperando a outra fazer alguma coisa, até ... #
23335
2
... um "temporizador de sanidade" entra em ação e resolve a situação (da ordem de um segundo).
Supercat
2
Infelizmente, desabilitar o algoritmo do Nagle não fará nada para impedir o buffer do outro lado do host. Desativar o algoritmo do Nagle não garante que você receba uma recv()chamada para cada send()chamada, que é o que a maioria das pessoas procura. Usando um protocolo que garante isso, como o UDP faz. "Quando tudo que você tem é TCP, tudo parece um stream"
Panda Pajama
6

Todas as respostas anteriores estão incorretas. Na prática, não importa se você faz uma send()chamada longa ou várias send()chamadas pequenas .

Como Phillip afirma, um segmento TCP possui alguma sobrecarga, mas como programador de aplicativos, você não tem controle sobre como os segmentos são gerados. Em termos simples:

Uma send()chamada não se traduz necessariamente em um segmento TCP.

O sistema operacional é totalmente gratuito para armazenar em buffer todos os seus dados e enviá-los em um segmento ou pegar o longo e dividi-lo em vários pequenos segmentos.

Isso tem várias implicações, mas a mais importante é que:

Uma send()chamada ou um segmento TCP não se traduz necessariamente em uma recv()chamada bem-sucedida do outro lado

O raciocínio por trás disso é que o TCP é um protocolo de fluxo . O TCP trata seus dados como um longo fluxo de bytes e não tem absolutamente nenhum conceito de "pacotes". Com send()você adiciona bytes a esse fluxo e recv()obtém bytes do outro lado. O TCP fará buffer agressivo e dividirá seus dados sempre que achar necessário, para garantir que eles cheguem ao outro lado o mais rápido possível.

Se você deseja enviar e receber "pacotes" com TCP, é necessário implementar marcadores de início de pacote, marcadores de comprimento e assim por diante. Que tal usar um protocolo orientado a mensagens como o UDP? O UDP garante que uma send()chamada se converta em um datagrama enviado e em uma recv()chamada!

Quando tudo que você tem é TCP, tudo se parece com um fluxo

Panda Pajama
fonte
1
Prefixar cada mensagem com um comprimento de mensagem não é tão complicado.
ysdx
Você tem uma opção para ativar quando se trata de agregação de pacotes, independentemente de o Algoritmo de Nagle estar em vigor ou não. Não é incomum que ele esteja desativado nas redes de jogos para garantir a entrega rápida de pacotes insuficientes.
Lars Viklund
Isso é completamente específico do sistema operacional ou mesmo da biblioteca. Além disso, você tem muito controle - se quiser. É verdade que você não tem controle total, o TCP sempre pode combinar duas mensagens ou dividir uma se não couber no MTU, mas você ainda pode sugerir na direção certa. Definir várias definições de configuração, enviar mensagens manualmente com um segundo de intervalo ou armazenar dados em buffer e enviá-los de uma só vez.
Dorus
@ sydx: não, não no lado de envio, mas sim no lado de recebimento. Como você não tem garantias de onde exatamente obterá dados recv(), é necessário criar seu próprio buffer para compensar isso. Eu o classificaria na mesma dificuldade em implementar a confiabilidade sobre o UDP.
Panda Pyjama
Pajama @ Pagnda: Uma implementação ingênua do lado receptor é: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(omitir o tratamento de erros e leituras parciais por questões de brevidade, mas isso não muda muito). Fazer isso selectnão adiciona muito mais complexidade e, se você estiver usando o TCP, provavelmente precisará armazenar em buffer as mensagens parciais. A implementação de confiabilidade robusta sobre UDP é muito mais complicada do que isso.
ysdx
3

Muitos pacotes pequenos estão bem. De fato, se você estiver preocupado com a sobrecarga do TCP, basta inserir um bufferstreamque colete até 1500 caracteres (ou seja qual for sua MTUs TCP, melhor solicitá-la dinamicamente) e lide com o problema em um só lugar. Isso poupa a sobrecarga de ~ 40 bytes para cada pacote extra que você teria criado.

Dito isto, ainda é melhor enviar menos dados e a criação de objetos maiores ajuda lá. É claro que é menor enviar "UID:10|1|2|3do que enviar UID:10;x:1UID:10;y:2UID:10;z:3. De fato, também neste ponto você não deve reinventar a roda, use uma biblioteca como protobuf que pode diminuir dados como esses para uma sequência de 10 bytes ou menos.

A única coisa que você não deve esquecer é inserir um Flushcomando no seu fluxo em locais relevantes, porque assim que você parar de adicionar dados ao seu fluxo, ele poderá esperar infinito antes de enviar qualquer coisa. Realmente problemático quando seu cliente está aguardando esses dados e seu servidor não envia nada de novo até que o cliente envie o próximo comando.

Perda de pacote é algo que você pode afetar aqui, marginalmente. Cada byte que você enviar pode ser potencialmente corrompido e o TCP solicitará automaticamente uma retransmissão. Pacotes menores significam uma chance menor de que cada pacote seja corrompido, mas, como aumentam a sobrecarga, você envia ainda mais bytes, aumentando ainda mais as chances de um pacote perdido. Quando um pacote é perdido, o TCP armazena em buffer todos os dados subsequentes até que o pacote ausente seja reenviado e recebido. Isso resulta em um grande atraso (ping). Embora a perda total de largura de banda devido à perda de pacotes possa ser insignificante, o ping mais alto seria indesejável para jogos.

Resultado: envie o mínimo de dados possível, envie pacotes grandes e não escreva seus próprios métodos de baixo nível para fazê-lo, mas conte com bibliotecas e métodos conhecidos como bufferstreame protobuf para lidar com o trabalho pesado.

Dorus
fonte
Na verdade, coisas simples como essa são fáceis de criar. Muito mais fácil do que passar por uma documentação de 50 páginas para usar a biblioteca de outra pessoa, e depois disso você ainda terá que lidar com os bugs e dicas deles.
Pacerier 20/03/2015
É verdade que escrever o seu próprio bufferstreamé trivial, por isso chamei de método. Você ainda deseja manipulá-lo em um só lugar e não integrar sua lógica de buffer ao seu código de mensagem. Quanto à serialização de objetos, duvido muito que você obtenha algo melhor do que as milhares de horas de trabalho que outras pessoas colocam lá, mesmo se você tentar, sugiro fortemente que você compare sua solução com implementações conhecidas.
Dorus
2

Embora eu seja um neófito da programação em rede, gostaria de compartilhar minha experiência limitada adicionando alguns pontos:

  • O TCP implica uma sobrecarga - você deve medir as estatísticas relevantes
  • O UDP é a solução de fato para cenários de jogos em rede, mas todas as implementações que dependem dele têm um algoritmo adicional no lado da CPU para contabilizar a perda ou o envio de pacotes fora de ordem

Com relação às medições, as métricas que devem ser consideradas são:

Como mencionado, se você descobrir que não está limitado em um sentido e pode usar o UDP, faça isso. Existem algumas implementações baseadas em UDP, por isso você não precisa reinventar a roda ou trabalhar contra anos de experiência e experiência comprovada. Tais implementações que vale a pena mencionar são:

Conclusão: como uma implementação UDP pode ter um desempenho superior (por um fator de 3x) ao TCP, faz sentido considerá-lo, uma vez que você identificou seu cenário como sendo UDP. Esteja avisado! Implementar a pilha TCP completa sobre o UDP é sempre uma má ideia.

teodron
fonte
Eu estava usando UDP. Acabei de mudar para o TCP. A perda de pacotes do UDP era simplesmente inaceitável para dados cruciais que o cliente precisava. Posso enviar dados de movimento via UDP.
joehot200
Portanto, as melhores coisas que você pode fazer são: de fato, use o TCP apenas para operações cruciais OU use uma implementação de protocolo de software baseado em UDP (com Enet sendo simples e UDT sendo bem testado). Mas primeiro, meça a perda e decida se a UDT trará uma vantagem para você.
Teodron