Qual deve ser o tamanho do meu buffer de recv ao chamar recv na biblioteca de soquetes

129

Eu tenho algumas perguntas sobre a biblioteca de soquetes no C. Aqui está um trecho de código ao qual me referirei nas minhas perguntas.

char recv_buffer[3000];
recv(socket, recv_buffer, 3000, 0);
  1. Como decido qual o tamanho do recv_buffer? Estou usando 3000, mas é arbitrário.
  2. o que acontece se recv()receber um pacote maior que meu buffer?
  3. como posso saber se recebi a mensagem inteira sem chamar recv novamente e aguardar para sempre quando não houver nada a ser recebido?
  4. existe uma maneira de fazer com que um buffer não tenha uma quantidade fixa de espaço, para que eu possa continuar adicionando a ele sem medo de ficar sem espaço? talvez usando strcatpara concatenar a mais recente recv()resposta ao buffer?

Eu sei que há muitas perguntas em uma, mas eu aprecio muito as respostas.

adhanlon
fonte

Respostas:

230

As respostas a essas perguntas variam dependendo de você estar usando um soquete de fluxo ( SOCK_STREAM) ou um soquete de datagrama ( SOCK_DGRAM) - no TCP / IP, o primeiro corresponde ao TCP e o segundo ao UDP.

Como você sabe qual o tamanho da passagem do buffer recv()?

  • SOCK_STREAM: Realmente não importa muito. Se o seu protocolo é transacional / interativo, basta escolher um tamanho que possa conter a maior mensagem / comando individual que você esperaria razoavelmente (3000 provavelmente é bom). Se o seu protocolo estiver transferindo dados em massa, buffers maiores podem ser mais eficientes - uma boa regra geral é a mesma que o kernel recebe o tamanho do buffer do soquete (geralmente algo em torno de 256kB).

  • SOCK_DGRAM: Use um buffer grande o suficiente para armazenar o maior pacote que o protocolo em nível de aplicativo já envia. Se você estiver usando o UDP, em geral, seu protocolo no nível do aplicativo não deve enviar pacotes maiores que 1400 bytes, porque eles certamente precisarão ser fragmentados e remontados.

O que acontece se recvum pacote for maior que o buffer?

  • SOCK_STREAM: A questão realmente não faz sentido, porque os soquetes de fluxo não têm um conceito de pacotes - são apenas um fluxo contínuo de bytes. Se houver mais bytes disponíveis para leitura do que o seu buffer tem espaço, eles serão colocados na fila pelo sistema operacional e disponíveis para sua próxima chamada recv.

  • SOCK_DGRAM: Os bytes em excesso são descartados.

Como posso saber se recebi a mensagem inteira?

  • SOCK_STREAM: Você precisa criar uma maneira de determinar o final da mensagem em seu protocolo no nível do aplicativo. Geralmente, esse é um prefixo de comprimento (iniciando cada mensagem com o comprimento da mensagem) ou um delimitador de final de mensagem (que pode ser apenas uma nova linha em um protocolo baseado em texto, por exemplo). Uma terceira opção menos usada é exigir um tamanho fixo para cada mensagem. Combinações dessas opções também são possíveis - por exemplo, um cabeçalho de tamanho fixo que inclui um valor de comprimento.

  • SOCK_DGRAM: Uma única recvchamada sempre retorna um único datagrama.

Existe uma maneira de fazer com que um buffer não tenha uma quantidade fixa de espaço, para que eu possa continuar adicionando a ele sem medo de ficar sem espaço?

Não. No entanto, você pode tentar redimensionar o buffer usando realloc()(se ele foi originalmente alocado com malloc()ou calloc(), ou seja).

caf
fonte
1
Eu tenho um "/ r / n / r / n" no final de uma mensagem no protocolo que estou usando. E eu tenho um loop do while, por dentro estou chamando recv e coloco a mensagem no início do recv_buffer. e minha declaração while se parece com isso while ((! (strstr (recv_buffer, "\ r \ n \ r \ n"))); minha pergunta é: é possível que um recv obtenha "\ r \ n" e no próxima recv get "\ r \ n", para que a minha condição sem nunca se torna realidade?
adhanlon
3
Sim, ele é. Você pode resolver esse problema fazendo um loop se você não tiver uma mensagem completa e colocando os bytes a partir do próximo recvno buffer após a mensagem parcial. Você não deve usar strstr()o buffer bruto preenchido por recv()- não há garantia de que ele contenha um terminador nulo; portanto, pode causar strstr()uma falha.
caf
3
No caso do UDP, não há nada de errado em enviar pacotes UDP acima de 1400 bytes. A fragmentação é perfeitamente legal e uma parte fundamental do protocolo IP (mesmo no IPv6, ainda assim, sempre o remetente inicial deve executar a fragmentação). Para o UDP, você sempre salva se usar um buffer de 64 KB, pois nenhum pacote IP (v4 ou v6) pode ter mais de 64 KB de tamanho (nem mesmo quando fragmentado) e isso inclui os cabeçalhos IIRC, portanto os dados sempre serão abaixo de 64 KB, com certeza.
Mecki
1
@caf você precisa esvaziar o buffer em cada chamada para recv ()? Eu vi o código em loop e coletar os dados e em loop novamente, o que deve coletar mais dados. Mas se o buffer ficar cheio, você não precisará esvaziá-lo para evitar uma violação de memória devido à gravação, passar a quantidade de memória alocada para o buffer?
Alex_Nabu
1
@Alex_Nabu: Você não precisa esvaziá-lo enquanto houver espaço nele, e não precisa recv()escrever mais bytes do que o espaço restante.
caf
16

Para protocolos de streaming como o TCP, você pode definir seu buffer para qualquer tamanho. Dito isto, valores comuns que são potências de 2, como 4096 ou 8192, são recomendados.

Se houver mais dados do que seu buffer, ele simplesmente será salvo no kernel para sua próxima chamada recv.

Sim, você pode continuar aumentando seu buffer. Você pode fazer uma recv no meio do buffer, começando no deslocamento idx, você faria:

recv(socket, recv_buffer + idx, recv_buffer_size - idx, 0);
R Samuel Klatchko
fonte
6
O poder de dois pode ser mais eficiente de várias maneiras e é fortemente sugerido.
Yann Ramin
3
Ao elaborar @theatrus, uma eficiência notável é que o operador de módulo pode ser substituído por bits e por uma máscara (por exemplo, x% 1024 == x & 1023), e a divisão inteira pode ser substituída por uma operação shift shift (por exemplo, x / 1024 = = x / 2 ^ 10 == x >> 10)
vicatcu
15

Se você possui um SOCK_STREAMsoquete, recvapenas obtém "até os primeiros 3000 bytes" do fluxo. Não há uma orientação clara sobre o tamanho do buffer: a única vez que você sabe o tamanho de um fluxo é quando tudo está pronto ;-).

Se você possui um SOCK_DGRAMsoquete e o datagrama é maior que o buffer, recvpreenche o buffer com a primeira parte do datagrama, retorna -1 e define errno como EMSGSIZE. Infelizmente, se o protocolo for UDP, isso significa que o restante do datagrama está perdido - parte do motivo pelo qual o UDP é chamado de protocolo não confiável (eu sei que existem protocolos de datagrama confiáveis, mas eles não são muito populares) nomeie um na família TCP / IP, apesar de conhecer bem este último ;-).

Para aumentar um buffer dinamicamente, aloque-o inicialmente com malloce use reallocconforme necessário. Mas isso não irá ajudá-lo a recvpartir de uma fonte UDP, infelizmente.

Alex Martelli
fonte
7
Como o UDP sempre retorna no máximo um pacote UDP (mesmo que vários estejam no buffer de soquete) e nenhum pacote UDP pode ter mais de 64 KB (um pacote IP pode ter no máximo 64 KB, mesmo quando fragmentado), o uso de um buffer de 64 KB é absolutamente seguro e garante que você nunca perca nenhum dado durante uma recv em um soquete UDP.
Mecki
7

Para o SOCK_STREAMsoquete, o tamanho do buffer realmente não importa, porque você está apenas puxando alguns dos bytes em espera e pode recuperar mais em uma próxima chamada. Basta escolher o tamanho do buffer que você puder pagar.

Para o SOCK_DGRAMsoquete, você receberá a parte apropriada da mensagem em espera e o restante será descartado. Você pode obter o tamanho do datagrama em espera com o seguinte ioctl:

#include <sys/ioctl.h>
int size;
ioctl(sockfd, FIONREAD, &size);

Como alternativa, você pode usar MSG_PEEKe MSG_TRUNCsinalizadores da recv()chamada para obter o tamanho do datagrama em espera.

ssize_t size = recv(sockfd, buf, len, MSG_PEEK | MSG_TRUNC);

Você precisa MSG_PEEKespiar (não receber) a mensagem em espera - recv retorna o tamanho real, não truncado; e você MSG_TRUNCnão precisa sobrecarregar seu buffer atual.

Então você pode apenas malloc(size)o buffer e o recv()datagrama reais .

smokku
fonte
MSG_PEEK | MSG_TRUNC não faz sentido.
Marquês de Lorne
3
Você deseja que o MSG_PEEK espreite (não receba) a mensagem em espera, obtenha seu tamanho (recv retorna o tamanho real, não truncado) e precisa que o MSG_TRUNC não exceda seu buffer atual. Depois de obter o tamanho, você aloca o buffer correto e recebe (não espie, não trunque) a mensagem em espera.
smokku
@ Alex Martelli diz que 64 KB é o tamanho máximo de um pacote UDP; portanto, se malloc()um buffer de 64 KB MSG_TRUNCfor desnecessário?
mLstudent33
1
O protocolo IP suporta fragmentação, portanto o datagrama pode ser maior que um único pacote - será fragmentado e transmitido em vários pacotes. Também SOCK_DGRAMnão é apenas UDP.
smokku
1

Não há resposta absoluta para sua pergunta, porque a tecnologia sempre é obrigada a ser específica da implementação. Suponho que você esteja se comunicando no UDP porque o tamanho do buffer de entrada não traz problemas à comunicação TCP.

De acordo com a RFC 768 , o tamanho do pacote (inclusive cabeçalho) para UDP pode variar de 8 a 65.515 bytes. Portanto, o tamanho à prova de falhas para o buffer de entrada é de 65 507 bytes (~ 64 KB)

No entanto, nem todos os pacotes grandes podem ser roteados corretamente pelos dispositivos de rede; consulte a discussão existente para obter mais informações:

Qual é o tamanho ideal de um pacote UDP para o máximo rendimento?
Qual é o maior tamanho de pacote UDP seguro da Internet

YeenFei
fonte
-4

16kb é quase certo; se você estiver usando ethernet gigabit, cada pacote poderá ter 9 KB de tamanho.

Andrew McGregor
fonte
3
Os soquetes TCP são fluxos, o que significa que uma recv pode retornar dados acumulados de vários pacotes; portanto, o tamanho do pacote é totalmente irrelevante para o TCP. No caso de UDP, cada chamada de retorno retorna no máximo um único pacote UDP, aqui o tamanho do pacote é relevante, mas o tamanho correto do pacote é de cerca de 64 KB, pois um pacote UDP pode (e geralmente será) fragmentado, se necessário. No entanto, nenhum pacote IP pode ser acima de 64 KB, nem mesmo com a fragmentação, assim recv em uma lata socket UDP, no máximo retorno de 64 KB (eo que não é devolvido é descartado para o pacote atual!)
Mecki