TCP e UDP, uma técnica interessante (e perigosa)…

Há uns anos escrevi uns artigos aqui sobre o princípio da construção de um http server em C. Eis dois detalhes que deixei de mencionar: TCP é um protocolo de streaming que não oferece “fronteiras” entre mensagens. Isso quer dizer que vocẽ pode enviar 10 pacotes de 300 bytes de um lado e obtê-los na mesma ordem (com tamanhos diferentes), ou obter um único pacotão de 3000 bytes, de uma só vez. Não há garantias de como você receberá esses dados, a única garantia é que, se não houverem erros ou se a conexão não cair, vocẽ os receberá na ordem que foram enviados. Esse comportamento acontece porque sua placa de rede mantém um buffer (e o driver de rede também).

Outro problema é que, se você tentar ler mais dados do que foram enviados, a system call recv(), se o descritor de arquivos (socket) não estivar marcado como “non blocking“, ficará esperando novos dados “eternamente”. Então, só existem duas maneiras de sabermos quantos dados devem ser lidos:

  1. Se soubermos de antemão o tamanho da mensagem;
  2. Se a mensagem contiver “marcadores”.

O segundo caso é o usado pelo protocolo HTTP quando uma requisição é feita. Toda linha de uma requisição termina com “\r\n” e a última linha é vazia (tem apenas a marca de fim de linha)… Podem existir mais linhas, no caso de um método POST, mas ai o tamanho do conteúdo é informado na requisição via atributo “ContentLength”, no header.

Um jeito interessante de lidar com esse tipo de coisa é usando o modo streamming de arquivos, na linguagem C, ou seja, usando a estrutura opaca FILE. Suponha que fd seja o descritor de um socket. Podemos fazer algo assim:

FILE *fsock;

if ( ! ( fsock = fdopen ( fd, "r+" ) ) )
{ ... trata erro aqui ... }

E daqui para frente poderemos usar funções como fgetc, fgets, fputc fputs, fread e fwrite ou até mesmo fscanf e fprintf para lidar com o descritor. Note, por exemplo, que fgets pára de ler o stream quando encontra um ‘\n’, o que pode facilitar o desenvolvimento de um HTTP server…

Existem duas vantagens e uma desvantagem em usar a estrutura de streams de C. A primeira vantagem é que você deixa pra libc ler os caracteres. A desvantagem é que perderemos o controle da semântica de recv… Quando recv retorna 0, ele indica que a conexão “caiu”, quando retorna -1, houve um erro. No caso do retorno de 0 as funções de streaming podem ficar “esperando” pelos dados, simplesmente achando que não existem dados para serem lidos! Em teoria, não temos problemas com o retorno de -1, que será repassado, mas provavelmente errno não será ajustado de forma coerente. Assim, se as funções de streaming facilitam a manipulação de leitura/escrita, ela dificulta em outros detalhes (que não são intratáveis!).

A outra vantagem, a despeito da desvantagem da semântica, é que o gerenciamento do buffer do stream, feito pela libc, permite-nos usar a função ungetc para colocarmos de volta os bytes que, por ventura, tivermos lido, acidentalmente, além de algum “marcador”.

É importante notar, também, que, já que estamos trabalhando com double buffering aqui (o buffer do stream e o buffer da fila do driver de rede), temos que usar a função fflush para descarregarmos o stream quando estivermos escrevendo no socket via FILE.

fprintf ( fsocket, 
          "%d %s HTTP/1.1\r\n", 
          error_code, 
          error_str[error_code] );
fflush ( fsocket );
fclose ( fsocket );

O fclose chamará a system call close e fechará o socket normalmente, mas precisamos mesmo usar o fflush antes disso, senão a string poderá ficar “presa” no buffer do stream. Uma maneira de evitar isso, se o nosso marcador for ‘\n’, e marcar fsocket como line buffered, logo depois de criá-lo com fdopen:

setlinebuf ( fsock ); // disponível apenas na GNU libc

Ou controlando o tamanho do buffer de streaming, de maneira mais padronizada:

static char buffer[2048]; // buffer de 2 KiB...
setvbuf ( fsock, buffer, _IOLBF, sizeof buffer );

Mesmo assim, fflush pode ser necessário se estivermos enviando dados que não contenham ‘\n’ no fim da string.

Onde é que o UDP entra nisso tudo? Não entra! Esse método não deve ser usado com UDP, especialmente porque ele é feito para o envio de um pacote único, caso contrário, não há garantidas da ordem da recepção… Ou seja, UDP não é streaming.