Exemplo simples do uso de GtkBuilder

Ok… existe um jeito de construir uma janela e atribuir os sinais aos widgets sem escrever muito código usando o Glade. O exemplo abaixo é bem simples: Apenas uma janelinha com um label e um button:

Essa já é a janela da aplicação rodando!

Para fazer isso vamos ao Glade criar uma GtkWindow e, dentro dela, colocar uma GtkBox com orientação vertical (o padrão) e duas divisões:

Note que mudei o nome do objeto da janela para window, ao invés do default window1. Modifiquei também a propriedade title na aba “General” para “Hello”. O motivo de colocarmos a GtkBox acima é que queremos colocar uma janela filha embaixo da outra. Daí, colocamos elas na GtkBox e temos algo assim:

Depois de modificadas as características visuais de cada objeto vou às abas “Signals” tanto da window quanto de button1 e colocarei o nome da função gtk_main_quit nos eventos destroy e clicked:

Tudo o que temos que fazer agora é salvar o projeto no formato GtkBuilder e fazer um programinha para carregá-lo:

#include <gtk/gtk.h>

int main(int argc, char *argv[])
{
  GtkBuilder *builder;
  GtkWidget *window;

  gtk_init(&argc, &argv);

  builder = gtk_builder_new();

  // "App.glade" é o arquivo que salvamos antes!
  // Existem outros lugares de onde podemos obter o XML!
  gtk_builder_add_from_file(builder, "App.glade", NULL);

  // Conecta os eventos assinalados no XML com as 
  // respectivas funções.
  gtk_builder_connect_signals(builder, NULL);

  // Pega o objeto window e mostra-o.
  window = GTK_WIDGET(gtk_builder_get_object(builder,
                      "window"));
  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

Voilà! Basta compilar e executar:

$ gcc -O3 -s -o hello hello.c \
`pkg-config --cflags --libs gtk+-3.0`

Repare que isso ai exige o GTK+3.x.

UPDATE:

Duas pequenas modificações sem muita importância… É conveniente que o evento “clicked” do botão requisite que a janela seja fechada. Isso pode ser feito modificando o evento para chamar gtk_window_close com o parâmetro window (aparecerá uma lista com os objetos contidos na janela ao clicar em “User Data”).

A outra modificação é quanto às própria janela window, o label label1 e o botão button1. Em window, habilite os tamanhos default e ajuste-os em 320×200 (por exemplo). No label1, na aba “Packing”, habilite “Expand”. E em button1, mude o “Button Content” para “Stock Button” e selecione “gtk-close” na combobox. A diferença na janela final será essa:

No caso do botão, se seu sistema estiver em português, aparecerá “Fechar” e se os botões com ícones estiverem habilitados, aparecerá um x vermelho, ao lado… :)

Anúncios

Programação Gráfica em C: Gnome Tool Kit (GTK+)

Se você gostou da pequena série de textos sobre programação gráfica usando a Win32 API (aqui, aqui e aqui), saiba que apenas arranhei o assunto, mas toda a base está lá… Até mesmo para ambientes diferentes como X11, no Linux e Unixes em geral. É claro que a coisa toda é um pouco mais simples e diferente nesses outros ambientes.

No caso do Linux (e Unixes) o ambiente gráfico não é integrado ao sistema operacional. Trata-se de um software “servidor” ou daemon. A aplicação literalmente envia comandos para o daemon pedindo para desenhar uma janela, movê-la, mudar de tamanho, pintar uma área e o daemon manda mensagens de volta… Em essência, é o que o Windows também faz. Mas, no X11, as janelas são desenhadas por um desktop environment manager, que é um módulo que roda sobre o daemon X11. Existem vários: Gnome, Unity, Wayland, Cinnamon, MATE, KDE, XFCE, WindowMaker, Motif etc.

Os dois desktop managers mais famosos e mais aceitos são Gnome e KDE e, por isso, as duas bibliotecas mais usadas para desenvolvimento “em janelas” para Linux são o GTK+ e o Qt. O primeiro é uma conjunto de bibliotecas para serem usadas em C. A outra, é uma class library, um conjunto de bibliotecas para serem usadas em C++. Neste aspecto, o GTK+ é mais parecido com a Win32 API do que o Qt. Eis um “hello, world” com o GTK+:

#include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc, char *argv[])
{
  GtkWidget *mainWindow;
  GtkWidget *label;

  gtk_init(&argc, &argv);

  // Cria a janela, ajusta o tamanho, o título e
  // conecta o evento "destroy" à função gtk_main_quit.
  mainWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  gtk_window_set_default_size(GTK_WINDOW(mainWindow),
                              320, 200);
  gtk_window_set_title(GTK_WINDOW(mainWindow), 
                       "Hello App");
  gtk_signal_connect(GTK_OBJECT(mainWindow),
                     "destroy",
                     GTK_SIGNAL_FUNC(gtk_main_quit),
                     NULL);

  // Cria o label e ajusta o texto.
  label = gtk_label_new(NULL);
  gtk_label_set_text(GTK_LABEL(label), "Hello, world!");

  // Adiciona o label à janela.
  // Mostra a janela e seus filhos.
  gtk_container_add(GTK_CONTAINER(mainWindow), label);
  gtk_widget_show_all(mainWindow);

  // Loop principal.
  gtk_main();

  return EXIT_SUCCESS;
}

Para compilar é sempre bom usar um utilitário que nos dará a lista de diretórios onde acharmos os headers e as libraries:

$ pkg-config --cflags gtk+-2.0
-pthread -I/usr/include/gtk-2.0 -I/usr/lib/x86_64-linux-gnu/gtk-2.0/include \
-I/usr/include/gio-unix-2.0/ -I/usr/include/cairo -I/usr/include/pango-1.0 \
-I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 \
-I/usr/include/libpng12 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng12 \
-I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 \
-I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include \
-I/usr/include/freetype2

$ pkg-config --libs gtk+-2.0
-lgtk-x11-2.0 -lgdk-x11-2.0 -lpangocairo-1.0 -latk-1.0 -lcairo \
-lgdk_pixbuf-2.0 -lgio-2.0 -lpangoft2-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 \
-lfontconfig -lfreetype

Impressionante, não? GTK+ exige um porrilhão (como sempre, essa é uma especificação de grandeza! hehe) de diretórios contendo arquivos header e um punhado de bibliotecas! Para compilarmos o nosso programinha podemos misturar as opções --cflags e --libs em uma linha só, assim:

$ gcc -O3 -s -o hello hello.c `pkg-config --cflags --libs gtk+-2.0`

Estou usando as opções -O3 e -s para otimizações e eliminar símbolos não necessários no arquivo binário final. O resultado da execução de hello é esse:

Agora, mova a janela, mude o tamanho, minimize, maximize… tá tudo lá num pequeno executável de 10 KiB!

O que pode surpreender é o aparente uso de Orientação a Objetos em um código em C. Os macros GTK_WIDGET, GTK_WINDOW e GTK_LABEL são apenas type castings para que a função não reclame que estamos passando o tipo errado e, ao mesmo tempo, todo objeto visível é um widget (GtkWidget).

Para tornar as coisas mais rápidas, a identificação de cada objeto é, de fato, um ponteiro que aponta para ele próprio. Nada de “handles”, como no Win32. E, embora pudéssemos criar nosso próprio loop de mensagens, GTK+ fornece a função gtk_main() que faz isso por nós. Ele permite registrar callbacks para hooks no loop, como exemplo, quando o loop estiver esperando por mensagens, pode executar uma função idle

Ahhh… quanto ao pkg-config, use a opção --list-all para ver todas as bibliotecas que ele consegue obter as listas de headers e libs

Por que gtk_init() toma ponteiros para argc e argv?

A função gtk_init() pode receber opções da linha de comando diretamente para ele… No caso do X11, por exemplo, a opção --display indica o daemon onde as janelas serão criadas (pode estar em outra máquina, por exemplo!)… A função retira da lista de parâmetros aqueles que são destinados apenas ao GTK+!

Um aviso sobre o sistema de coordenadas usado no GTK+:

Se você já mexeu com Applets em JAVA, deve se lembrar da antiga biblioteca AWT (Abstract Widget Toolkit). Ele foi “copiado” de bibliotecas como XLib e GTK+… Os containers do GTK+ (GtkWindow, por exemplo) usam o sistema de coordenadas “geográfico” por default, onde, ao inserir um único widget-filho ele é colocado no “centro”, ocupando a área útil toda… mas, podemos colocá-lo ao norte, ao sul, ao leste ou oeste, assim como era feito no AWT.

Isso não significa que não podemos usar um sistema de coordenadas cartesiano, mas, para isso, precisaríamos incluir o container GtkFixed na nossa janela e esse container é o que permite posicionamento cartesiano… A maioria das aplicações feitas com GTK+ não fazem isso…

Existe um utilitário para desenhar janelas?

Sim… existe o Glade:

Screenshot do Glade

Ele gera um arquivo XML contendo a descrição de sua janela e o GTK+ possui funções que interpretam esse XML e montam a janela completa (GtkBuilder). É a perfeita implementação do MVC. O View é apenas o arquivo XML contendo a parte visual e o código que lida com ele é o seu Model e Controller. Num outro artigo explico como usá-lo…

Gtk+ é portável!

Um detalhe final é que GTK+ é portável. Existe o GTK+ para Linux, OS/X, FreeBSD, mas também tem para Windows! Isso quer dizer que você pode criar programas gráficos para todas as plataformas com o mesmo conjunto de bibliotecas. No caso do Windows, terá que usar o MinGW para compilar com o GCC e carregar junto um caminhão de DLLs que colocará em C:\Windows\System32 (aqui é mostrado como)… Note que é perfeitamente possível compilar sua aplicação com GTK+ usando o Visual Studio.

Note que, no Windows, não é necessário usar aquela construção biruta de WinMain(). O bom e velho main funciona!

GTK+ 2.0 e GTK+ 3.0

Existem duas implementações da biblioteca porque existem, hoje, dois “Gnomes” (versão 2.x e 3.x) diferentes… O ‘G’ do GTK vem de Gnome e é para desenvolver aplicações para ele que a biblioteca existe. Clique nos links para obter documentação de referência a ambas as versões: GTK+ 2.0 e GTK+ 3.0. Note, também, que a GLib é parte integrante e, portanto, uma pancada de algoritmos está à sua disposição (listas, árvores etc)…

Criando screenshots com múltiplos frames

Já viu uma daquelas imagens contendo o nome do arquivo, características de codecs, resolução e qualidade, e alguns quadros do vídeo? Tipo esse aqui:

imperial-starport-frakfurt
Já viu isso?

Geralmente esse tipo de imagem é construída com o auxílio de algum programinha especialista nisso… Só que dá para fazer algo semelhante usando apenas o ffmpeg, o ImageMagick (com os utilitários convert e montage) e um cadinho de auxílio do GNU bc (calculadora de múltipla precisão do projeto GNU) e do sed.

O primeiro passo é obter as informações do arquivo. Nada mais fácil:

$ ffprobe -hide_banner video.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    creation_time   : 2014-07-05T18:16:29.000000Z
    encoder         : Lavf53.32.100
  Duration: 00:01:13.84, start: 0.000000, bitrate: 1450 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 1317 kb/s, 25 fps, 25 tbr, 25 tbn, 50 tbc (default)
    Metadata:
      creation_time   : 2014-07-05T18:16:29.000000Z
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: ac3 (ac-3 / 0x332D6361), 44100 Hz, stereo, fltp, 128 kb/s (default)
    Metadata:
      creation_time   : 2014-07-05T18:16:29.000000Z
      handler_name    : SoundHandler
    Side data:
      audio service type: main

O único trabalho é separar apenas os dados pertinentes e, neste, ponto, o sed vem em nosso auxílio:

$ ffprobe -hide_banner video.mp4 2>&1 |\
  sed -n '/Video:/s/^.\+Video: \(\w\+\).\+, \(.\+\) \[.\+$/Video: \1 (\2)/p'
$ ffprobe -hide_banner video.mp4 2>&1 |\
  sed -n '/Audio:/s@^.\+Audio: \(\w\+\) [^,]\+\(.\+ kb/s\).\+$@Audio: \1\2@p'

As regular expressions, acima, apenas separam e montam a string que vão aparecer logo abaixo do nome do arquivo, na imagem. Vale reparar que apenas duas linhas têm as substrings “Video:” e “Audio:” (em vermelho). Note que essas regular expressions funcionam muito bem para containers MP4. O ffmpeg não segue um padrão muito bom para mostrar as informações de diversos tipos de vídeos e codecs diferentes, você terá que adaptá-las…

Podemos obter a duração do vídeo, em segundos, para calcular de quantos em quantos frames capturaremos uma imagem assim:

$ ffprobe -v error \
  -select_streams v:0 \
  -show_entries stream=duration \
  -of default=nokey=1:noprint_wrappers=1 \
  video.mp4

Para uma visão completa de todas as variáveis que podem ser usadas com o ffprobe, use a linha de comando abaixo:

$ ffprobe -v error \
  -show_format -show_streams \
  video.mp4
[STREAM]
index=0
codec_name=h264
codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
profile=Main
...
[/STREAM]
[STREAM]
index=1
codec_name=ac3
codec_long_name=ATSC A/52A (AC-3)
...
[/STREAM]
[FORMAT]
filename=video.mp4
nb_streams=2
...
[/FORMAT]

Note que existem, pelo menos, duas listas distintas: A do formato do container e a dos streams. Para isolar um único item usa-se a opção -show_entries em conjunto com a formatação de saída (opção -of ou -print_format) e a seleção do stream desejado via -select_streams. A primeira isola o item desejado e a segunda livra-se da formatação adicional indesejada.

Essa técnica pode ser interessante para resolver o problema citado anteriormente, ao tentar obter infos sobre os streams contidos no vídeo…

Neste ponto podemos usar uma variedade de técnicas para obter as imagens do vídeo. Sabendo que queremos 30 imagens, colhidas em toda a extensão do vídeo, poderíamos obter um quadro a cada intervalo regular de $latex duration/30″ segundos. Teríamos que transformar os incrementos desses valores, começando de 0 (zero), no formato hh:mm:ss.ccc, de volta, e obter um frame através das opções -ss e -frames:v, no ffmpeg. A coisa ficaria, em pseudo código, mais ou menos assim:

# Isso é um pseudo-código (não é BASH!).
DURATION_INC=DURATION/30
N=0
while [N < 30]; do
  TIME=DURATION_INC*N
  TIMESTR=time2str(TIME)
  ffmpeg -ss $TIMESTR -i video.mp4 -frames:v 1 frame${N}.png
  $((N++))
done

Felizmente existe um atalho para isso. Não é propriamente exato, é meio que uma gambiarra, mas quebra o galho (novamente, desculpe-me pelas longas linhas):

THUMBS=29 # Nº de frames - 1.
THUMBSIZE="128:72" # Tamanho (w,h) do thumbnail.
DURATION="`ffprobe -v error -select_streams v:0 -show_entries stream=duration -of default=nokey=1:noprint_wrappers=1 video.mp4 2>&1`"
CUSTOM_FPS="`echo "scale=4; $THUMBS / $DURATION" | bc`"

# Obtém as imagens e monta os screenshots
ffmpeg -i video.mp4 \
  -vf "fps=$CUSTOM_FPS,scale=$THUMBSIZE" \
  frames%02d.png

O filtro “fps”, com um valor fracionário, simplesmente pulará os frames intermediários, obtendo apenas os frames que queremos. Note que usei uma precisão maior que 3 dígitos apenas para garantir que a divisão da duração seja um cadinho mais correta… Ainda, como obteremos apenas 30 imagens, os valores precedidos pelo nome “frames” têm apenas 2 dígitos.

Já temos os frames e já temos as informações sobre o arquivo. Para montar a montagem dos screenshots basta montar duas imagens. A primeira com as informações e a segunda com os frames, e depois juntá-las. Para montar a imagem com as informações:

$ convert -background lightgray \
  "label:File: $1\\n$VIDEOINFO\\n$AUDIOINFO" \
  title.png

E, para montar a grade com as imagens:

$ THUMBS_PER_LINE=6 \
  montage -border 1 \
    -bordercolor black \
    -background lightgray \
    frames*.png \
    -geometry +2+2 \
    -tile $THUMBS_PER_LINE \
    montage.png

O utilitário montage, que acompanha o ImageMagick faz a mágica. Ele agrupa as imagens “frames*.png”, colocando-as de 6 em 6 em cada linha (opção -tile) e separando-as por 2 pixels (opção -geometry). Coloquei uma borda de 1 pixel preto em cada thumbnail (opções -border e -bordercolor) e a mesma cor de fundo que usei no título (opção -background).

Agora, só precisamos juntar as duas imagens. Mas há um problema…

Por algum motivo, a primeira imagem é tratada como monocromática pelo ImageMagick (não me perguntem porquê!), daí temos que usar outra gambiarra para fazer a coisa funcionar: Precisamos “colorir” o canvas (X11 Canvas, abaixo – No Windows, provavelmente, terá que usar “canvas:red”) de vermelho e depois juntá-las:

$ convert -background lightgray \
  xc:red title.png montage.png \
  -append +repage \
  screenshots.png

A gambiarra está tanto no uso do canvas avermelhado quando no atributo +repage, que retira o canvas inserido na imagem…

O scripr completo fica assim:

#!/bin/bash

# Apenas verifica se a linha de comando está correta!
if [ $# -ne 1 ]; then
  echo "Usage: thumbs <video>"
  exit 1
fi

THUMBS=29          # nº de frames - 1.

THUMBSIZE="128:72" # Poderíamos calcular isso com base no
                   # aspect ratio do vídeo (exercício!).

THUMBS_PER_LINE=6

# Pega infos do vídeo...
# FIXME: Tenho que refinar isso para vídeos .webm, avi, .mov e outros...
#        Funciona bem com MP4.
#        Provavelmente usando -show_entries resolveria...
VIDEOINFO="`ffprobe -hide_banner "$1" 2>&1 | \
  sed -n '/Video:/s/^.\+Video: \(\w\+\).\+, \(.\+\) \[.\+$/Video: \1 (\2)/p'`"
AUDIOINFO="`ffprobe -hide_banner "$1" 2>&1 | \
  sed -n '/Audio:/s@^.\+Audio: \(\w\+\) [^,]\+\(.\+ kb/s\).\+$@Audio: \1\2@p'`"

# Monta a imagem da barra de título.
convert -background lightgray \
  "label:`echo -e "File: $1\\n$VIDEOINFO\\n$AUDIOINFO\\n"`" \
  title.png

# Calcula a taxa de frames por segundo para obter apenas THUMBS quadros do vídeo.
DURATION="`ffprobe -v error \
             -select_streams v:0 \
             -show_entries stream=duration \
             -of default=nokey=1:noprint_wrappers=1 \
             "$1" 2>&1`"
CUSTOM_FPS="`echo "scale=4; $THUMBS / $DURATION" | bc`"

# Obtém as imagens dos frames já no tamanho dos thumbnails.
ffmpeg -i "$1" \
  -vf "fps=$CUSTOM_FPS,scale=$THUMBSIZE" \
  frames%04d.png

# Monta os screenshots.
montage -border 1 \
  -bordercolor black \
  -background lightgray \
  frames*.png \
  -geometry +2+2 \
  -tile $THUMBS_PER_LINE \
  montage.png

# Não preciso mais dos frames.
rm frames*.png

# Gambiarra: Quando misturamos uma imagem em "grayscale" com uma colorida, o
# resultado será uma imagem em "grayscale". Isso é um "bug" do ImageMagick e
# pode ser resolvido "colorindo" a imagem em "grayscale" (envermelhando-a, por exemplo).
convert -background lightgray \
  xc:red \
  title.png montage.png \
  -append \
  +repage \
  "${1%.*}.png"

# Não preciso mais dos arquivos temporários.
rm title.png montage.png

ATENÇÃO:

Repare que na figura dos screenshots que usei lá em cima. Muitos quadros parecem estar repetidos. Isso acontece porque não houveram mudanças significativas nos quadros capturados. Para evitar isso, poderíamos usar um filtro diferente para capturar os quadros. O filtro select serve para selecionar (o que mais?) quadros de acordo com um critério. A linha de comando abaixo selecionará apenas os quadros que tiveram mudanças acima de 40% do conteúdo.

ffmpeg -i video.mp4 \
  -vf "select=gt(scene\,0.4)" \
  -vsync vfr \
  frame%04d.png

Só que esse filtro, ao descartar os quadros intermediários, repetirá os quadros selecionados tantos quantos forem os frames descartados. E não é isso o que queremos… Para descartar quadros duplicados temos que mudar o esquema de sincronismo de vídeo do ffmpeg através da opção -vsync. Usando o modo vfr os quadros duplicados são descartados de verdade.

Esse método não é muito bom para garantir uma quantidade mínima de quadros. Você pode acabar com menos quadros do que quer ou acabar com muitos quadros para selecionar manualmente, mas ele garante imagens “diferentes”.

ATUALIZAÇÃO:

Dando uma olhada nos filtros do ffmpeg descobri o framestep, que faz exatamente o que queremos. Considere que queremos 30 quadros de um vídeo e temos como saber a quantidade de quadros totais do mesmo. Basta usar o filtro com 1/30 da quantidade total de quadros para obtermos os quadros desejados:

ffmpeg -i video.mp4 \
  -vf framestep=`echo "scale=3; $(ffprobe -v error \
    -select_streams v:0 -show_entries stream=nb_frames \
    -of default=nk=1:nw=1 video.mp4) / 30.0" | bc` \
  frames%02d.png

Na opção -of troquei os atributos do formato default para nk (de nokey) e nw (de noprint_wrappers). Repare também que, com a linha acima, não precisamos calcular a duração e aquele “frames por segundo” fracionário… Só que, dependendo do valor calculado para o filtro, podemos acabar com um quadro a mais ou a menos da amostra. Daí, o a conversão, usando o montage teria que ficar:

montage -border 1 \
  -bordercolor black \
  -background lightgray \
  frames{01..30}.png \
  -geometry +2+2 \
  -tile $THUMBS_PER_LINE \
  montage.png

Note o globbing que usei no nome dos arquivos dos frames!

Talvez você tenha maior precisão obtendo a contagem de frames de uma forma diferente. A entrada nb_read_frames retorna a contagem dos frames lidos do stream (nb_read_frames, que pode ser ligeiramente diferente do que está escrito no header do stream, ou seja, nb_frames), mas precisamos mandar o ffprobe contar os frames, do início ao fim do arquivo:

ffprobe -v error \
  -select_streams v:0 \
  -count_frames
  -show_entries stream=nb_read_frames \
  -of default=nk=1:nw=1 video.mp4

Mas, mesmo assim, vale a pena limitar os nomes dos arquivos como citei acima…

Uma dica para testes é a de que essas opções costumam valer também para o ffplay. Então você pode ver o resultado antes de modificar o script.

Sockets: Criando um httpd simples (Parte 6)

Pretendo que essa seja a última parte porque não vou implementar o httpd completamente. O objetivo aqui é apenas mostrar como fazê-lo.

Uma vez que a requisição tenha sido lida, juntamente com o header completo do protocolo HTTP, temos no buffer object da requisição as linhas, separadas por “\r\n” contendo o método (GET, PUT, POST, …), o path e os atributos. A questão agora é separar as linhas com a função strtok_r() — lempre-se que temos uma conexão por thread e strtok() não é reentrante. A primeira linha é especial: Ela contém o método e o path (bem como a versão “HTTP/1.1”). O formato exato deve ser verificado, de acordo como mostrei na primeira parte. Daí, separamos isso e codificamos os campos da requisição de acordo… E, ainda usando strtok_r(), obteremos as demais linhas, em loop…

Cada linha que segue a primeira contém informações no formato “Attributo: valor”. De novo, outra chamada a strtok_r() é feita, mas com o ponteiro de controle diferente, separando o primeiro item do segundo via comparação com a string “: “. De posse disso, chamamos tratadores especializados para cada um atributo — através de comparações de strings feitas com strcmp() ou, melhor ainda, strcasecmp(). Esses tratadores vão pegar a string contida depois do separador “: “ e convertê-la nos formatos exigidos pelo atributo. Por exemplo, o atributo “Content-Length” aceita apenas um valor numérico e precisaremos convertê-lo para um valor do tipo “unsigned long long” (ou “unsigned int” se você só vai permitir o tratamento até 4 GiB ou menos de payloads) e verificarmos se ele é um valor numérico, decimal, válido…

Atributos como “Cookies” aceitam uma grande string contendo “chave=atributo” separadas por “&” e têm que ser decodificadas de acordo (espaços são “+” e caracteres especiais são codificados em hexadecimal “%NN”)… Outros atributos podem aceitar formatos diferentes: “Connection”, por exemplo, aceita strings contendo “keep-alive” e “close”“Content-Type” a ceita uma string com o tipo de conteúdo seguindo a especificação MIME, mantida pela IANA (“text/plain”, “text/html”, “application/json”…). Cada atributo tem seu jeitinho…

Depois que todas as linhas forem decodificadas pela função process_request() e os respectivos valores armazenados na requisição, contida na sessão, foram copiados e validados, podemos chamar a função session_handler(), ambas brevemente descritas no exemplo da parte anterior dessa série de artigos. Ela vai pegar o path da requisição e compará-lo contra uma tabela que realizará saltos para funções específicas… O path “/”, por exemplo, poderá montar uma resposta padrão para a aplicação (a “homepage”). O path “/sendfile” poderá atualizar e ler o payload da requisição (lembre-se que, se tivermos algum payload, a leitura da requisição obterá parte dele!), gravando o stream de dados num arquivo… O path “/getfile” poderá fazer o contrário, ler blocos de 8 KiB de um arquivo e enviá-los na resposta… e por ai vai… É claro que podemos fazer algum filtro: O path “/something”, por exemplo, poderá chamar um script em bash e retornar para o requisitante, no payload da resposta, o que o script colocou em stdout… O importante é que seus handlers não usem funções não reentrantes, já que eles serão executados num ambiente multi threaded.

O path também precisa de um tratamento especial. Ele pode conter uma URL completa, inclusive contendo query strings, como em http://meusite.com.br/recurso?x=1&b=fred&#8221;. A função de processamento da requisição precisa saber separar o path da URI (a parte antes do path) e o path das query strings. Essas últimas podem entrar como se fossem atributos do path, no objeto da requisição… Para diferenciar: URI é um identificador. Ele contém apenas a identificação do site. Uma URL é um URI que contém a localização de um recurso (e atributos adicionais)… Nesse contexto, URI é o nome do site e o protocolo (http://meusite.com.br&#8221;, onde http://&#8221; é o protocolo e, obviamente, “meusite.com.br” o site).

Por que diabos uma URL pode aparecer na primeira linha da requisição? É que uma aplicação pode requisitar um recurso a um web server qualquer através de outro (um proxy). Dai, o atributo Host e a URL não apontarão para a mesma URI e o web server repassará a requisição para outro web server… Nosso httpd não é um web server completo e eu não implementaria essa re-requisição, mas é importante tratar o path estirpando a URI…

Quanto as respostas, usamos a função send() da mesma forma que usamos a função recv()… Ou seja, se mandarmos enviar n bytes, ela pode enviar menos do que isso e devemos enviar os bytes que faltam, em loop… A função devolverá -1 em caso de erro e errno setado com o motivo do erro. Um erro EINTR significa, como em recv(), que a syscall foi interrompida. Um erro ENOTCONN significa que a conexão não existe mais. Um erro EAGAIN ou EWOULDBLOCK, para sockets não bloqueáveis, significa que o buffer de saída está cheio e devemos tentar enviar os dados de novo… Consulte a documentação da função nas manpages.

Se errno for ENOTCONN, por exemplo, devemos sair do loop de session_manager() e permitir que a conexão seja fechada do nosso lado, destruindo a pilha da thread e retirando a referência da lista de sessões… Provavelmente fazemos o mesmo se o erro ECONNRESET for recebido. A mesma coisa se recebermos EPIPE (“broken pipe” normalmente é um erro de conexão quebrada!)… Diferente de recv(), se send() retorna zero, não significa que a conexão caiu…

A montagem e envio de uma resposta é sempre mais simples do que a leitura de uma requisição. Aqui nós é que temos o controle total… Se quisermos enviar um payload teremos certeza de ajustar o atributo “Content-Length” da resposta para o tamanho exato dela. Na leitura não temos essa certeza… O cliente pode nos enviar um stream de dados dizendo que ele tem 1 KiB, mas ele tem apenas 512 bytes! Por isso o esquema de timeout usando poll(). Usar poll() para enviar um stream de bytes pode ser interessante para determinar se o sistema operacional aceita o envio de dados de antemão, mas podemos confiar, até certo ponto, que ele conseguirá. O importante é tratar os erros reportados por send().

Em resumo: Um httpd é, basicamente, um processador de requisições e um fornecedor de respostas. O usuário pede algo via método “GET” ou envia algo usando os métodos “PUT” ou “POST”, a aplicação a interpreta, obtendo ou não um payload que pode existir ou não, monta uma resposta e a envia… Os demais métodos, para esse httpd simples são inócuos (HEAD, DELETE, CONNECT, …), mas é interessante oferecer uma resposta padrão para eles…

Usei essa pseudo-implementação de um httpd porque ela é complexa o suficiente para que eu pudesse escrever sobre como um protocolo do nível de aplicação pode ser tratado usando sockets. Sockets, por sua vez, são a mesma coisa que usar um file descriptor obtido via open(), só que não lidamos com dispositivos de blocos… Na realidade os “blocos” são os pacotes que chegam na sua interface de rede, que podem ter até 576 bytes de tamanho cada. Os drivers sabem como juntar pacotes recebidos corretamente e permitir a leitura de mais que 576 bytes de uma só vez, mas há um limite: Placas de rede costumam usar um buffer circular pequenininho (alguns KiB) e é por isso que recv() e send() podem tamanhos menores do que os mandamos ler/enviar… Além disso temos que ficar espertos para as condições de erro: Um arquivo está acessível, normalmente, o tempo todo, não é o caso de uma conexão de rede… Entender os códgos de erro e valores de retorno de funções que lidam com sockets é extremamente importante para o desenvolvimento de rotinas que atendam ao protocolo desejado.

Sockets: Criando um httpd simples (Parte 5)

Em teoria, desenvolver um http daemon é simples: Para cada conexão cria-se uma thread que a aceitará, lerá a requisição, a processará, executará alguma ação com base nos dados da requisição e enviará uma resposta. Mas, temos que considerar os problemas nesse meio tempo! Já que estaremos lidando com um stream de dados que podem nem mesmo chegar a ser transmitido completamente ou a conexão pode cair no meio da coisa toda…

Neste meu exemplo de implementação (que estou fazendo devagarzinho para tentar explicar o funcionamento!), criarei uma thread para cada conexão que chamarei de sessão, seguindo a nomenclatura conhecida pelos desenvolvedores “web”… A “sessão” faz o que descrevi no primeiro parágrafo e precisaremos manter uma lista de sessões ativas (threads) para que, em casos especiais, possamos percorrê-la e nos livrarmos daquelas que não atendam mais certos requisitos… O tempo de inatividade é um deles.

A primeira ideia era implementar um garbage collector. Uma thread isolada que ficaria varrendo a lista, de tempos em tempos, à procura de estados específicos de uma sessão e a última vez que este estado foi iniciado… Por exemplo: Se a sessão estiver em estado IDLE por mais que 30 minutos, a matamos… Outros estados podem ser implementados, como WAITING_DATA e/ou RECEIVING_REQUEST, PROCESSING e TRANSMITTING_RESPONSE. Para simplificar escolhi esse estado “ocupado” sendo apenas BUSY. O terceiro estado óbvio é CLOSED, se o cliente ou o servidor fecharem a conexão (fácil de detectar com recv retornando 0, neste caso).

Ao invés de usar uma thread isolada para implementar o garbage collector usarei a própria thread que “ouve” as conexões. Vale lembrar que a função poll pode receber um parâmetro de timeout… No caso do socket listener, se esperarmos mais que, por exemplo, 30 segundos, por uma conexão, trataremos o timeout de poll e executaremos o coletor de lixo, lidando com a lista de sessões, verificando seus estados e respectivos timeouts. Se houver um timeout numa sessão basta cancelarmos a thread com pthread_cancel() que, ela própria, conterá o mecanismo para auto-destruição.

Neste ponto, vale a pena mostrar como os estados são mantidos. Inicialmente pensei em alocar todas as estruturas dinamicamente, no heap. No entanto, cada thread tem uma pilha relativamente grande, de cerca de 16 KiB, no mínimo (usarei a constante PTHREAD_STACK_MIN em limits.h), e uma “guarda” default de 4 KiB… Como assim “guarda”? Eis como a pilha, inclusive da thread principal, funciona: A medida que RSP vai sendo decrementado e dados vão sendo gravados, quando o endereço em RSP cruza a fronteira da última página alocada para a pilha entramos na página de “guarda” (não é à toa que ela tenha, por default, 4 KiB!). Essa página, provavelmente, é alocada com o atributo PROT_WRITE desligado (ou seja, é “read-only”), o que gera uma falta de página… No caso da thread principal, o sistema operacional lida com a falta aumentando a quantidade de páginas para a pilha até o limite especificado em ulimit -s (de cerca de 8 MiB, na minha estação de trabalho). Se passar desse limite o Linux te dará um erro de “Stack Overflow” e abortará o processo… Esses 8 MiB incluem a página de guarda!

Já numa thread secundária, o tamanho da página é fixo, definido pelos atributos da thread via função pthread_attr_setstacksize() que, por default, pode chegar até o mesmo limite especificado em ulimit -s. Não é isso que eu quero: Definirei que cada thread secundária poderá ter uma pilha de tamanho máximo de 16 KiB, o que é mais que suficiente para conter tanto variáveis locais da thread quanto os endereços de retorno das chamadas das funções… Acontece que, se esse limite for extrapolado, entramos na página de guarda e a thread “crasha”… Podemos retirar a página de guarda via pthread_attr_setguardsize(), mas é prudente deixá-la por lá! Isso só fará que cada thread tenha uma pilha de 20 KiB.

Porque o limite da pilha é importante? Quanto menor a pilha, mais threads podemos disparar sem nos importamos muito com o consumo de memória e, ainda, manter as estruturas de sessões locais às threads (veja a função session_manager(), abaixo). O que colocaremos na lista gerenciada pelo garbage collector, que chamarei de sessions_list, são apenas os ponteiros para essas estruturas locais. Podemos fazer isso porque todas as threads compartilham o mesmo address space do processo…

Até o momento, a estrutura de uma sessão usada no projeto sofreu uma simplificação:

typedef struct session_s {
  /* Para controle de thread. */
  pthread_t thread_id;
  pthread_mutex_t mutex;
  
  /* Para controle de conexão. */
  int sock_fd; /* obtido via accept() */
  int close_session; /* Por default é 0 (falso). */
  struct sockaddr_in sin; /* Endereço/porta do cliente. */
  socklen_t sin_sz; /* No futuro podemos querer usar
                       IPv6... quardo o tamanho da 
                       estrutura aqui. */

  /* Para controle de estado da sessão. */
  session_state_t state;
  time_t state_time;  /* timestamp da última
                         atualização de estado. */

  /* A requisição */
  request_t req;

  /* Não precisa de ponteiro para resposta.
     Ela será montada e enviada por último. */
} session_t;

A sessão é mantida pela thread com a simples declaração da estrutura… A função abaixo está incompleta (faltam os sincronismos de thread com o garbage collector):

/* Essa é a thread que tratará da sessão. */
void *session_manager(void *data)
{
  int listener_fd = *(int *)data;
  session_t s; /* A sessão é local à thread! */

  /* Inclusive, seta a sessão para SESSION_IDLE. */
  session_init(&s);

  /* Aceita a conexão. */
accept_again:
  if ((s.sock_fd = accept(listener_fd,
                          (struct sockaddr *)&s.sin, 
                          &s.sin_sz)) == -1)
  {
    if (errno == EINTR) goto accept_again;
    else return (void *)-1;
  }

  /* Muda o modo do socket para O_NONBLOCK. */
  set_socket_non_blocking_mode(&sock_fd);

  /* Insere a sessão na lista do garbage collector. */
  dlist_insert(&sessions_list, &s);

  /* Registra procedimento de limpeza da sessão.
     Ele, inclusive, retira a sessão da lista do
     "garbage collector". */
  pthread_cleanup_push(session_cleanup, &s);

  /* Só sai se a thread for cancelada ou
     se o processamento foi completado e
     http keep alive não for requisitado. */
  while (1)
  {
    request_t *req = &session.req;

    request_init(req);

    /* get request. */
    if ((recv_req_buffer(s.sock_fd, 
                         &req->buf, 
                         &req->pld)) == -1)
    {
      /* O usuário desconectou? Sai da sessão. */
      if (errno == ECONNABORTED)
        break;

      /* A requisição é muito grande? Loga isso
         e sai da sessão. */
      if (errno == E2BIG)
      {
        error("Request from %s:%hu too long.", 
              inet_ntoa(s.sin.s_addr), s.sin.s_port);
        break;
      }

      if (errno == ETIME && 
          buffobj_isempty(req->buf))
        continue;
    }

    /* processa a requisição... */
    process_request(req);

    /* chama a função que irá lidar com a requisição,
       montar a resposta e enviá-la. */
    handle_request(req);

    /* Acabamos por aqui! Livra-se da requisição. */
    request_destroy(req);

    /* Se o atributo "Connection" do header HTTP é
       "close", então sairemos e fecharemos a sessão. */
    if (s.close_session)
      break;

    /* Fez tudo o que devia, coloca sessão em idle. */
    set_session_state(&s, SESSION_IDLE);
  }

  pthread_cleanup_pop(1);

  return (void *)0;
}

A rotina session_cleanup faz a mágica de liberar toda a memória usada pela sessão da thread… Ela será chamada se a thread for cancelada ou terminada normalmente.

Ainda existem detalhes a serem considerados… Mas já que quase todas as funções possuem pontos de cancelamento, o garbage collector, ao detectar a condição suficiente para livrar-se de uma sessão, só tem que chamar pthread_cancel() com o id da thread em questão (e é por isso que ele consta da estrutura da sessão!). O coletor, sem os sincronismos necessários (que serão implementados nas rotinas de manipulação da lista) e sem o bloco de temporização (a coleta não deve ser um loop infinito sem colocarmos a thread para dormir e a acordarmos de tempos em tempos!), fica mais ou menos assim:

void garbage_collector(void)
{
  dlist_node_t *node = sessions_list.head.next;
  dlist_node_t *next = node->next;
  time_t tm;

  time(&tm);
  for (; node != next; node = next)
  {
    next = node->next;
    
    session_t *s = (session_t *)node->data;
    if (session_get_state(s) == SESSION_IDLE)
      if (((tm - s->last_state_time)*60) > SESSION_TIMEOUT)
        pthread_cancel(s->thread_id);
  }
}

Sockets: Criando um httpd simples (Parte 4)

A ideia por trás do processamento de uma requisição é obter o header, processá-lo e, dentre outras coisas, verificar se existe um attributo “Content-Length” para obter o tamanho do payload, se houver. Neste httpd simples vamos pegar cada um dos atributos e convertê-los em estruturas mais simples, antes de manipulá-los. Por exemplo, o atributo “User-Agent” será simplesmente copiado para uma string, substituindo o terminador de linha “\r\n” por um simples ‘\0’. Mas, o “Content-Type”, se houver, será convertido para uma constante inteira (uma enumeração), bem como o “Content-Length” (size_t).

Além da requisição o servidor terá que compor uma resposta que tem o seu código de retorno, uma mensagem pequena para a linha contendo o código e atributos. Ambas entidades (requisição e resposta) fazem parte de um container chamado de “sessão”. A sessão do usuário nada mais é que um conjunto de estados mantidos enquanto a conexão estiver ativa… Requisições e respostas são transitórias, mas o tempo de inatividade da conexão e outros dados estatísticos não são.

Eis uma prévia da estrutura de uma sessão:

typedef
struct session_s {
  /* Guardamos o id da thread e
     um mutex aqui para facilitar 
     o acesso. */
  thread_id tid;
  pthread_mutex_t mutex;

  /* Este é o descritor do socket que será retornado
     pela syscall accept(). */
  int conn_fd;

  session_state_e state;

  /* Última vez que o estado da sessão foi 
     modificado. */
  time_t last_state_time;

  request_t req;
  response_t resp;
} session_t;

A estrutura pode crescer, se precisamos. Os membros req e resp conterão os dados da requisição e da resposta. Os membro state é uma enumerações simples:

typedef
enum session_state_e {
  SESSION_IDLE,     /* A sessão está vazia e
                       esperando por uma requisição. */
  SESSION_CLOSED,   /* A conexão foi encerrada. */
  SESSION_BUSY      /* A sessão está sendo processada. */
} session_state_e;

Sempre que o estado da sessão for mudado o membro last_state_time será ajustado com o timestamp (data/hora) atual. Isso serve para calcularmos o timeout da sessão e derrubá-la se o estdo for diferente de SESSION_CLOSED. No loop da thread principal podemos chamar uma rotina assim, para limpar as sessões expiradas:

/* Destrói o nó de uma lista de
   encadeamento duplo que contém uma sessão. */
void session_destroy(dlist_node_t *node)
{
  session_t *s = (session_t *)node->data;

  /* Cancela a thread da sessão se a rotina
     não for chamada de dentro da própria thread
     da sessão! */
  if (pthread_self() != s->tid)
    pthread_cancel(s->tid);

  /* A rotina de cleanup da thread tomará conta
     de livrar-se da sessão. */
}

void close_all_expired_sessions(dlist_t *sessions_list)
{
  dlist_node_t *p;
  time_t t;

  time(&t);
  for (p = sessions_list->head->next; 
       p != p->next; 
       p = p->next)
  {
    session_t *s = (session_t *)p->data;

    pthread_mutex_lock(&s->mutex);
    session_state_e state = s->mutex;
    pthread_mutex_unlock(&s->mutex);

    /* Qualquer outro estado que não SESSION_CLOSED
       só destruirá a sessão se o timeout ocorrer. */
    if (state == SESSION_CLOSED)
      session_destroy(p);
    else
    {
      t -= sp->last_idle_time;

      /* SESSION_TIMEOUT é em minutos! */
      if (60*t >= SESSION_TIMEOUT)
        session_destroy(p);
    }
  }
}

E, quando formos mudar o estado da sessão para SESSION_IDLE teremos que obter o tempo em que isso ocorreu. A rotina abaixo garante que isso ocorrerá:

void change_session_state(session_t *s, 
                          session_state_e state)
{
  pthread_mutex_lock(&s->mutex);
  if (state != s->state)
    time(&s->last_state_time);
  pthread_mutex_unlock(&s->mutex);
}

Repare que as instâncias de sessões são mantidas numa lista de encadeamento duplo para:

  1. Permitir percorrer a lista para nos livramos de sessões expiradas, como mostrado acima e;
  2. Permitir apagar qualquer item da lista sem termos que percorrê-la toda a partir do início e;
  3. Permitir fechar e apagar todos os itens da lista, se precisarmos.

O item 3 é bem similar a rotina anterior:

void close_all_sessions(dlist_t *sessions_list)
{
  dlist_node_t *p;

  for (p = sessions_list->head->next;
       p != p->next; p = p->next)
    session_destroy(p);

  /* Não podemos mais confiar no estado de sessions_list */
}

Você notou o uso de mutexes para sincronizar as leituras e atualizações do membro state da estrutura da sessão? Isso é importante, já que a thread principal, que poderá livrar-se dela terá que sincronizar o acesso com a thread que a está manipulando. Note que session_destroy() cancela a thread da sessão antes de dealocar os dados e fechar o descritor. Por isso, não precisaremos sincronizar todos os acessos aos membros…

Ainda não decidi se mutexes são a alternativa melhor ou se uso simples spin locks, que são extremamente mais rápidos…

Com base na estrutura de sessão, eis a rotina de leitura de um header de requisição:

#define SAFE_FREE(p) \
    do { free((p)); (p) = NULL; } while (0)

/* Essa função é simples. Um monte de ifs. */
extern process_attribute(avl_tree_t *, char *);

extern void send_bad_request_msg(int);
extern void send_internal_server_error(int);

int session_read_request_header(session_t *s)
{
  jmpbuf jb;
  buffobj_t boReqHdr;
  buffobj_t boPayload;
  pthread_mutex_lock(&s->mutex);
  session_state_e state = s->state;
  pthread_mutex_unlock(&s->mutex);

  /* A requisição encontra-se fechada? */
  if (state == SESSION_CLOSED)
  {
    errno = EBADF;
    return -1;
  }

  /* Já processamos a requisição? */
  if (state == SESSION_BUSY)
    return 0;

  /* Aloca espaço para os buffers objects. */
  if (buffobj_create(&boReqHdr, 
                     REQUEST_MAX_SIZE) == -1)
  {
    errno = ENOBUFS;
    return -1;
  }
  if (buffobj_create(&boPayload, 
                     REQUEST_MAX_SIZE) == -1)
  {
    buffobj_destroy(&boReqHdr);
    errno = ENOBUFS;
    return -1;
  }

  if (setjmp(jb))
  {
    buffobj_destroy(&boReqHdr);
    buffobj_destroy(&boPayload);
    return -1;
  }

  /* Obtém a requisição e processa. */
  if (RECV_REQ_HRD(s->fd, &boReqHdr, 
                   &boPayload) == -1)
  {
    /* Erro! */
    if (errno == EBADMSG)
    {
      /* Retorna resposta de erro padrão para
         má formação de requisições e fecha a 
         conexão! */
      send_bad_request_msg(s->fd);
    }
    else if (errno != ECANCELLED)
      send_internal_server_error_msg(s->fd);

    /* Em caso de qualquer erro, fecha a 
       conexão. */
    change_session_state(s, SESSION_CLOSED);
    close(s->fd);

    longjmp(jb, 1);
  }
  /* Ok. temos uma header na requisição! 
     OBS: teremos que lidar com o payload mais
          tarde aqui. */

  /* Ponteiros usados para manter o estado de
     strtok_r. */
  char *reql, *reql2;

  /* Separamos a primeira linha do resto! */
  char *p, *q;
  if (!!(p = strtok_r(boReqHdr.begin, "\r\n", 
                      &reql)))
  {
    /* Tenta pegar o método. */
    if (!(q = strtok_r(p, " ", &reql2)))
    {
      errno = ENOTSUP;
      longjmp(1);
    }
    s->method = request_get_method(q);
    if (s->method == METHOD_INVALID)
    {
      errno = ENOTSUP;
      longjmp(1);
    }

    /* Tenta pegar o path. */
    if (!(q = strtok_r(NULL, " ", &reql2)))
    {
      errno = ENOTSUP;
      longjmp(1);
    }
    s->path = strdup(q); /* duplica-o */

    /* Tenta pegar a versão do HTTP */
    char *version;
    if (!(version = strtok_r(NULL, "\r\n", &reql2)))
    {
      SAFE_FREE(s->path);
      errno = ENOTSUP;
      longjmp(1);
    }

    /* Valida a versão. */
    if (!strcmp(version, "HTTP/1.1"))
    {
      SAFE_FREE(s->path);
      errno = ENOTSUP;
      longjmp(1);
    }

    /* --- Acabamos com a primeira linha. */
  }

  /* Processando cada um dos atributos... */
  while (!!(p = strtok_r(NULL, "\r\n", &reql)))
  {
    char *r = NULL;

    /* Os atributos são sempre formatados como
       Nome: Valor\r\n. */
    q = strtok_r(p, ": \r\n", &reql2);
    if (q)
      r = strtok_r(NULL, "\r\n", &reql2);
      
    process_attribute(&req, q, r);
  }

  /* --- Pronto! temos toda a requisição armazenada. 
         Podemos nos livrar do buffer object. */
  buffobj_destroy(&boReqHdr);
  buffobj_destroy(&boPayload);
  change_session_state(s, SESSION_REQUEST);
  return 0;
}

 

Sockets: Criando um httpd simples (Parte 2)

Muita coisa foi dita no artigo anterior. Uma delas foi a de que algumas chamadas a syscalls podem bloquear a thread a que pertencem, pelo kernel. É o caso da função recv(), por exemplo… Isso nos deixa num mato sem cachorro: Se o protocolo HTTP não nos fornece o tamanho total de uma requisição de antemão, como é que saberemos quando devemos parar de ler? Afinal, recv() bloqueará a thread se tentarmos ler mais bytes do stream do que os que foram realmente transmitidos…

No caso do header da requisição a solução é simples: Basta verificar se a sequência de caracteres “\r\n\r\n” foi recebida… Mas, pera ai… e se o requisitante não for um browser, mas um programinha mal intencionado que jamais envie essa sequência?

Uma possível solução para esse problema é não permitir que recv() bloqueie a thread e, felizmente, existe um jeito de fazer isso… Um file descriptor (e um socket é um!) pode ser marcado como “não bloqueável”. Assim, uma função como recv(), ao usar tal descritor, simplesmente retornará um código de erro se não houverem dados disponíveis e, de outro modo, for bloquear. Ou seja, retornará -1 e ajustará o valor em errno para a constante EAGAIN ou EWOULDBLOCK.

Marcar o descritor é bem simples, basta usar outra syscall chamada fcntl() — essa á a abreviação de file control:

int fcntl(int fd, int cmd, ...);

O argumento fd, é claro, é o nosso descritor. O cmd é um inteiro que determina o que queremos fazer com ele e, os demais argumentos, se houverem, são usados como complemento do comando. Para ajustar os flags do descritor usamos os comandos F_GETFL (de “pegar os flags”) e F_SETFL (de “setar os flags”, o que mais?). O flag que queremos setar é O_NONBLOCK:

/* Pega os flags atuais */
if ((old = fcntl(fd, F_GETFL)) == -1)
{ ... trata o erro aqui ... }

/* Seta o bit do flag O_NONBLOCK apenas */
if (fcntl(fd, F_SETFL, old | O_NONBLOCK) == -1)
{ ... trata o erro aqui ... }

Pronto! A partir de agora, as funções que bloqueiam a thread não o farão mais… Mas, de novo, espere um pouquinho só…

Acontece que o recurso do bloqueio automático é bem útil. Ele evita que tenhamos que escrever algo assim:

do {
  bytes = recv(rd, buffer, size, 0);
} while (bytes == -1 && errno == EWOULDBLOCK);

Esse simples loop fará com que recv() seja chamado, em loop, mesmo quando não existirem dados para serem lidos! Se os dados jamais chegarem e a conexão continuar em pé, o loop será infinito e todos sabemos o que acontece com loops infinitos: Consumo excessivo de CPU!

Precisamos de algum mecanismo de bloqueio de thread para deixá-la “dormindo” enquanto não há dados disponíveis para leitura através do descritor! Ao invés de retornamos ao mesmo problema, tem que existir um mecanismo que faça isso, mas ofereça o recurso de timeout. Se não conseguirmos ler nada em, por exemplo, 3 segundos, gostaríamos de abortar o bloqueio!

A especificação POSIX.1-2008 ABI oferece duas syscalls para esse fim. Elas lidam apenas com file descriptors. São elas: select() e poll(). Das duas, se altíssima performance não for um problema, eu prefiro poll(). Ela é mais simples do que select():

int poll(struct pollfd *fds, int nfds, int timeout);

A função retorna 0 se houve um “timeout” sem nenhuma atividade do descritor, -1 em caso de erro e um valor maior que zero, informando a quantidade de descritores onde ocorreu algum “evento”. Atenção: Ela não retorna o índice do array, mas a quantidade de descritores onde houveram eventos. Você terá que percorrer o array todo verificando a resposta, se usar nfds > 1.

Os argumentos de poll() são, na ordem, um array contendo as estruturas que têm os descritores e os requisições/respostas de eventos, seguido do número de itens do array (nfds) e do número de milissegundos do timeout. A estrutura pollfd tem apenas três membros:

struct pollfd {
  int fd;     /* o descritor que queremos
                 monitorar/bloquear. */
  int event;  /* Evento requisitado. */
  int revent; /* Evento de resposta. */
}

Tipicamente os eventos requisitados para o descritor podem ser POLLIN e POLLOUT (existem outros!) e os eventos respondidos por poll() para o descritor podem ser POLLIN, POLLOUT, POLLERR (existem outros!). Assim, o que temos que fazer para usar essa função com um único descritor é preencher a estrutura e chamar a função, simples assim:

int r;

struct pollfd pfd = { 
  .fd = fd, 
  .event = POLLIN /* Acorda quando
                     o descritor tiver
                     dados de entrada! */
};

r = poll(&pdf, 1, 3000);
switch (r)
{
  case -1: /* erro! */ ... break;
  case 0: /* timeout! */ ... break;
  default:
    bytes = recv(fd, buffer, size, 0);
}
...

Enquanto o descritor fd não tiver dados para serem lidos poll colocará a thread para dormir. Se passarem 3000 milissegundos (3 segundos) ele retorna 0, gritando “timeout!”. Se retornar -1 houve um erro e devemos tratá-lo (afinal, poll() também é uma syscall), caso contrário podemos usar recv() sem medo que ela não bloqueará!

Note que aqui pedimos para poll verificar apenas POLLIN. se tivéssemos pedido POLLOUT também (usando OR para informar os dois), teríamos que verificar se o membro revent está com o bit POLLIN setado, antes de usar recv. É óbvio que POLLOUT significa “acorde” se o socket não bloqueará para escrita!

O uso de poll() resolverá o nosso problema inicial: Leremos tantos bytes quantos necessários via recv(), mas só se poll() permitir. Nos casos em que não temos como saber quantos dados devem, de fato, ser lidos, usaremos o recurso de timeout para interrompermos o bloqueio e decidirmos que já lemos o suficiente (parece que o requisitante não vai mandar mais nada!)…

Uma nota interessante sobre poll() (e select(), que faz a mesma coisa, mas de um jeito mais esquisito) é que existe algumas syscalls um tanto mais rápidas, que funcionam igualzinho: epoll_create(), epoll_ctl() e epoll_wait(). A diferença é que epoll_create devolve um int, como se fosse um descritor, epoll_ctl() é usada para informar os eventos que devemos testar e podemos adicionar, modificar ou deletar eventos e epoll_wait() faz o bloqueio e trata do timeout. Essencialmente, as funções epoll fazem a mesma coisa que poll, com um cadinho mais de controle, mas são exclusivas do Linux! Na família BSD, por exemplo, não existe epoll, mas temos a função kqueue().

Se performance é um problema, ao invés de usar epoll ou kqueue, prefira bibliotecas externas como libev ou libevent.