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… :)

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.

Sockets: Criando um httpd simples (Parte 1)

Me pediram, há tempos, que escrevesse alguma coisa sobre sockets aqui e em meu livro “C & Assembly para arquitetura x86-64”. Não o fiz, até hoje, porque trabalhar com sockets exige um nível de abstração superior ao material proposto tanto no blog, quando lá, no livro… Recentemente vi um projeto no Github (este aqui) que implementa um httpd bem simples e eficiente e resolvi explicar como um httpd pode ser incorporado ao seu projeto sem que tenhamos que usar bibliotecas como libmicrohttpd ou usar a interface com o Apache (APR, ou Apache Portable Runtime).

Deixo claro que esse texto inicial serve apenas para “dar um gostinho”. O código de exemplo, no final, tem vários problemas que sequer citarei, ainda, deixando espaço para refinamentos em outros artigos.

Syscalls relacionadas a arquivos:

A maneira mais fácil de começarmos a entender sockets é fazer comparações com as syscalls que lidam com arquivos: open()read()write()close(). Essas funções lidam com um valor inteiro que simboliza ou descreve um arquivo. Esse valor inteiro é chamado de file descriptor e é sempre positivo. Um file descriptor negativo é inválido e é usado como valor de retorno das funções que indica que um erro ocorreu. Assim, para ao abrir um arquivo, podemos fazer algo assim:

int fd;

/* Tenta abrir o arquivo 'myfile.txt',
   para leitura apenas. */
if ((fd = open("myfile.txt", O_RDONLY)) == -1)
{
  ... trata erro aqui ...
}

Note a comparação do descritor fd contra o valor -1. Alguns preferem compara contra 0, assim:

if ((fd = open("myfile.txt", O_RDONLY)) < 0)
{
  ... trata erro aqui ...
}

Isso não é correto. A especificação das syscalls é clara: O valor de retorno, no caso de erros, é -1.

Estou falando de syscalls aqui. Funções como fopen(), fscanf(), fprintf() e fclose() são abstrações usadas pela glibc que implementam “streams” através de buffers contidos dentro de uma estrutura opaca chamada FILE. Essas funções fazem parte da glibc, não do kernel do seu sistema operacional.

Lendo e escrevendo em um arquivo usando syscalls:

De posse do descritor de arquivo, e se não há erros, podemos usá-lo para realizar operações de leitura e escrita através das funções read() e write(), respectivamente. Ambas retornam um valor inteiro que nos informa o tamanho daquilo que a syscall conseguiu ler (no caso do read()) ou escrever (no caso do write()). Esse valor inteiro pode ser negativo também, indicando erro. De novo, devemos compará-lo contra -1:

ssize_t count;

if ((count = read(fd, buffer, buffer_length, 0)) == -1)
{
  ... trata erro aqui ...
}

A especificação de read() nos diz que count pode ser zero e, neste caso, indica que chegamos ao final do arquivo. Mas a especificação também nos diz que, embora tenhamos pedido para read() ler buffer_length bytes do descritor fd, ele pode ler menos do que esse taamnho e a quantidade de bytes será retornada e colocada em count. Ou seja, count pode ser menor que buffer_length. Isso significa que, se você quer ter certeza que leu buffer_length bytes, deve fazer algo assim:

int read_entire_buffer(int fd, void *buffer, size_t size)
{
  /* Aponta para o início do buffer. */
  char *p = buffer;

  ssize_t count;

  /* Se ainda temos bytes a ler... */
  while (size > 0)
  {
    /* ...tenta lê-los! 
       'count' pode retornar menos que
       'size'! */
    count = read(fd, p, size, 0);

    /* Se chegamos ao final do arquivo ou
       se temos um erro, retorna -1. */
    if ((count == 0) || (count == -1))
      return -1;

    /* Diminui o tamanho para lermos apenas
       o que falta. */
    size -= count;

    /* Avança o ponteiro para o buffer. */
    p += count;
  }

  /* 0 significa OK */
  return 0;
}

Tome cuidado ao usar uma rotina assim. A função read() bloqueará a thread em execução até que tenha algum dado disponível para ser lido ou até que ocorra um erro. Se você requisitar a leitura de 1000 bytes através dessa função e só chegaram 500, a rotina ficará bloqueada até que os outros 500 bytes cheguem… Não é essa a aproximação que usarei no exemplo deste artigo. Aqui, verificarei se uma requisição chegou a seu fim através da verificação da sequência de caracteres terminais (“\r\n\r\n”, como mostrarei), de acordo com a especificação do protocolo HTTP. Enquanto não encontrarmos esses 4 bytes, a leitura continuará… Isso é mais seguro, no momento.

Com relação ao tamanho de transmissão de blocos, algo semelhante aplica-se à função write(). Ela pode escrever menos do que você mandou escrever e retornará esse tamanho. Diferente de read() o valor zero no retorno só indica que nada foi escrito. E, outra vez, temos que verificar se o valor -1 foi retornardo, em caso de erros.

Encerrando a comunicação:

A função close(), é claro, fecha o arquivo. A mesma função é usada para encerrar uma conexão, no caso dos sockets, realizando a sequência de encerramento correta para o par de protocolos TCP/IP.

Sockets e TCP/IP:

Um socket é uma abstração de software. Ele é representado por um file descriptor, da mesma maneira que a função open(), acima faz. A ideia é a de que fazemos uma ligação de uma “tomada” (socket) num dispositivo de rede, de um lado, e de alguma maneira outra ligação é feita do outro lado (na máquina remota), estabelecendo a “conexão” entre ambas. Para que essa conexão aconteça é necessária uma troca de informações entre os dois computadores, de maneira ordenada. Essa sequência de troca de informações é chamada de protocolo (lembra um comportamento diplomático, com suas regras restritas).

Existem diversos protocolos para diversas finalidades. Estamos interessados em apenas dois: IP e TCP. O Internet Protocol é um conjunto de dados que diz, para ambos os interlocutores, de onde a mensagem veio e para onde ela vai. Contém outras informações como o tempo de vida do “pacote” (TTL, Time To Live, é a quantidade de retransmissões que podem ocorrer com o pacote — ou seja, por quantos “roteadores” o pacote pode passar); e o tamanho do pacote (tem mais, mas não é importante aqui).

Dentro do “pacote” IP temos o pacote TCP (Transmission Control Protocol). Esse pacote contém informações sobre o “serviço” ao qual o dado é destinado (e de onde veio), chamado de “porta”. Contém ainda uma série de informações e flags destinados ao controle da transmissão confiável dos pacotes.

Dentro do pacote TCP podem existir outros pacotes, mas, no nosso contexto, temos ou pacotes TCP de controle ou aqueles que contém dados. O importante aqui é saber que TCP é responsável pelo estabelecimento e encerramento de uma conexão, bem como a garantia da entrega de pacotes na sequência correta.

As syscalls para sockets:

Embora sockets sejam uma abstração e são usados através de file descriptors, as funções usadas para manipulá-los são ligeiramente mais especializadas do que open()read()write(). Usamos socket() para criar um socket e retornar um descritor de arquivo. Eis o prototipo e seus 3 argumentos:

int socket(int domain, int type, int protocol);

O “domínio” é a família ao qual o protocolo que pretendemos usar pertence. Existem constantes no header “sys/socket.h” prefixadas com “PF_” (de Protocol Family) que devem ser usadas aqui. Duas delas são importantes para nós: PF_INET e PF_INET6. A primeira diz que usaremos um dos protocolos da família do IPv4. A segunda, que usaremos um da família do IPv6. No momento estou interessado apenas em PF_INET.

O “tipo” refere-se a como o socket tratará os pacotes. As constantes SOCK_STREAM e SOCK_DGRAM são particularmente importantes. A primeira está associada ao TCP e a segunda ao protocolo UDP. O protocolo UDP é mais simples que o TCP porque ele não garante que os pacotes cheguem na sequência correta ou que chegem! Ele também não possui quaisquer mecanismos de conexão. Ou seja, UDP é um protocolo conectionless.

O significado desse “tipo” é mais amplo do que um protocolo de transporte específico. SOCK_STREAM significa que a família de protocolos deve ser “confiável” e “conectada” (TCP é um exemplo), enquanto SOCK_DGRAM seleciona a família de protocolos “não confiáveis” e “desconectados” (exemplo: UDP)…

E, finalmente, fornecemos o “protocolo” ao socket(). No nosso caso usarei a constante IPPROTO_TCP para garantir que TCP será usado. É possível informar um protocolo zerado e, neste caso, se a família de protocolos possuir apenas um que atenda os dois outros argumentos, ele será usado. Mas, se possuir mais que um, o protocolo tem, obrigatoriamente, que ser fornecido… Particularmente, não acho uma boa prática usar zero nesse parâmetro, mas já vi várias implementações que o fazem.

Ligando um socket a uma interface de rede:

Com a chamada à socket(), tudo o que fizemos até o momento foi alocar um file descriptor. Ele não serve para nada, ainda… No caso de um httpd devemos ligá-lo a um endereço de um dispositivo de rede (NIC). Para tanto usamos a função bind():

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

O argumento sockfd, obviamente, é o file descriptor do socket. O último argumento, addrlen, contém o tamanho do buffer apontado por addr. Isso é necessário porque o formato de endereços de dispositivos de rede dependem não somente do protocolo usado pelo socket, mas também da família de endereços que usaremos ao preencher esse buffer… A socket API nos dá uma estrutura genérica, básica, chamada sockaddr, mas ela não é, geralmente, usada. Trata-se de um tipo base. No caso de lidar com TCP/IP estamos interessados em outras três estruturas derivadas de sockaddr:

sockaddr

A estrutura sockaddr_store é usada quando queremos lidar com endereçamento tanto com IPv4 quanto com IPv6. As estruturas sockaddr_in sockaddr_in6 lidam, respectivamente, com IPv4 e IPv6.

Assim, bind() aceita o ponteiro genérico e preencherá o buffer de acordo com a família de protocolos citada na criação do socket e também no tamanho do buffer citado em addlen… Aqui, já que estou interessado apenas em IPv4, usarei a estrutura sockaddr_in e a preencherei com uma porta específica (8080) e um endereço genérico:

struct sockadd_in sin = {
  .sin_family = AF_INET,
  .sin_port = htons(8080),
  .sin_addr.s_addr = INADDR_ANY
};

if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
  ... trata erro aqui...
}

Isso faz com que o socket descrito por fd seja ligado à porta 8080 do endereço default fornecido pela tabela de rotas configurada no seu sistema operacional. No código acima, digo que esse endereço é da família de endereços do IPv4 via a constante AF_INET (prefixo “AF_”, de “Address Family“)… A constante INADDR_ANY, atribuída ao endereço, é simplesmente o valor zero. Cada “octeto” de um endereço IPv4 é traduzido diretamente para um byte e o endereço IPv4 todo é do tipo unsigned int. Só que a ordem em que esses bytes aparecem no endereço é chamada de big endian, onde o MSB (Most Significative Byte) aparece no menor endereço e o LSB (Less Significative Byte), no maior. Um endereço IPv4 como 127.0.0.1, na arquitetura Intel, deve ser codificado como 0x0100007F. Ao armazenar esse valor na memória o 0x7F será colocado na primeira posição, seguido dos dois zeros e o byte 0x01, por último… Esse é o padrão que os endereços da família AF_INET esperam… A mesma coisa com relação à porta: Sendo um valor do tamanho de um unsigned short, ocupando 16 bits ou 2 bytes, o MSB tem que vir primeiro. Por isso o uso da função htons() — de “host to network order short“.

E, por falar em “porta”, não é uma boa ideia atribuir qualquer valor ao membro sin_port da estrutura sockaddr_in. A IANA (Internet Assigned Numbers Authority. Site aqui) regula os valores de acordo com cada tipo de serviço. Por exemplo, a porta 22 é usada para SSH (Secure SHell), as portas 20 e 21 pelo FTP (File Transfer Protocol)… Eis algumas portas reservadas para aplicações:

  • 1194 – OpenVPN
  • 1352 – Lotus Notes
  • 1433-1434 – MS-SQL Server
  • 1521 – Oracle Database
  • 2000 – CISCO SCCP
  • 3306 – MySQL
  • 3690 – Subversion (SVN)
  • 5222 – XMPP client
  • 5269 – XMPP server
  • 6000-6007 – X11
  • 10050-10051 – Zabbix

Outra lista útil para consultas a respeito de atribuições de portas está no Wikipedia (aqui) e na lista oficial do IANA (aqui). No caso de servidores http é aconselhável usar a porta 80 ou 8080. Essa última é definida, pela IANA, como http alternative.

Quanto ao bind(), um aviso: Ele só é necessário nos casos onde você quer ligar um socket diretamente a um dispositivo. No caso de um httpd isso é importante, já que o seu daemon ficará escutando uma porta específica de um dispositivo específico. Dessa forma, você poderá ter daemons  separados respondendo em endereços diferentes e portas diferentes, se quiser… O fato de usarmos um endereço genérico no exemplo acima é somente um atalho conveniente. Se você tiver mais que uma interface de rede e não quiser usar a interface default para responder às requisições HTTP, terá que, necessariamente, ligar seu socket a um endereço específico!

No caso de aplicações cliente, geralmente não queremos que o socket esteja ligado à um endereço e porta específicos… Por quê? Bem… as primeiras 1024 portas são reservadas para o superusuário (root). Se você tentar atribuir a porta 80 no membro sin_port de sockaddr_in, e seu processo não estiver executando sob o usuário root, a função bind() retornará -1, indicando erro. Ainda, a aplicação cliente com socket “não-ligado” escolherá, automaticamente, o endereço e uma das portas maiores que 1023… Essas portas são chamadas de “efêmeras” porque são escolhidas para a conexão do cliente e, quando esse desconectar e reconectar poderá escolher outra porta completamente diferente.

Se não me engano, o sistema operacional escolherá portas não atribuídas no arquivo “/etc/services” para o cliente… Para ver essa atribuição automática funcionando você pode usar o utilitário tcpdump (ou o Wireshark, se preferir um ambiente gráfico). Com ele é comum vermos um fluxo assim:

# tcpdump -tnc 4 -i lo host 127.0.0.1 and port 8080
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
IP 127.0.0.1.48271 > 127.0.0.1.8080: Flags [S], seq 124748774, ...
IP 127.0.0.1.8080 > 127.0.0.1.48271: Flags [S.], seq 4047593347, ack 124748775, ...
IP 127.0.0.1.48271 > 127.0.0.1.8080: Flags [.], ack 1, ...
IP 127.0.0.1.48272 > 127.0.0.1.8080: Flags [S], seq 643118673, ...
4 packets captured
40 packets received by filter
0 packets dropped by kernel

Aqui a porta 8080 é a do socket do meu daemon ligado à interface default (é o meu pequeno código de teste mais adiante). Os endereços e portas efêmeras 48271, 48272 e 48273 são usadas pelo browser e foram escolhidas pela socket API no ato da conexão… Ou seja, o browser (Chrome, no caso) não faz bind() em uma porta específica.

Esperando por conexões:

Depois de ligar (bind) o socket a um endereço e uma porta, precisamos colocá-lo para ficar escutando… Nada mais simples, chama-se a função listen():

int listen(int sockfd, int backlog);

Ela apenas marca o socket como sendo “passivo”, ou seja, incapaz de ser usado numa conexão. Também informa à API que podemos aceitar, no máximo, a quantidade de conexões especificadas no parâmetro backlog. Depois de chamar listen(), o socket passa a existir apenas para receber pedidos de conexão, que deverão ser aceitas por outra função: accept().

Atenção especial ao valor de backlog: O valor máximo depende do seu sistema. Na máquina em que usei para fazer testes o limite superior para a quantidade de file descriptors do sistema era de 65536. E, destes, uns 37000 já estavam em uso:

$ ulimit -n
65536
$ echo "$(lsof | wc -l) - 1" | bc
37550

Deixar que seu processo tenha à disposição uma lista de uns quase 28000 file descriptors não é nem mesmo aconselhável. Primeiro, é improvável que seu processo tenha velocidade e espaço em memória suficiente para atender 28 mil conexões simultâneamente e, em segundo lugar, isso deixaria “poucos” descritores disponíveis para o sistema. Um valor máximo mais aceitável para backlog dependerá do método usado para lidar com as conexões. Num sistema multi threaded, se trabalharmos com uma thread por descritor de conexões aceitas, uns 100 descritores devem ser suficientes para não causarem grande impacto devido ao chaveamento de tarefas, por exemplo.

Aceitando conexões:

Depois de colocar o socket para escutar, precisamos começar a aceitar conexões vindas do cliente. A função accept(), quando chamada, bloqueará a thread em que é executada até que um pedido de conexão chegue ao socket e seja atendida. Neste caso, accept() criará um novo socket, agora “ativo”, e o colocará numa lista associada ao socket “escutador”.Este novo socket é que é o “conectado” e usado para transmissão de dados.

O protótipo de accept() é:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

A função retorna -1 em caso de erro ou um file descriptor relacionado à conexão aceita. Ao mesmo tempo, se os ponteiros seguintes forem diferentes de NULL, o endereço do requisitante e o tamanho da estrutura são armazenados nos endereços dados pelos ponteiros. Podemos saber quem nos chamou, afinal de contas!

Lendo e escrevendo num socket:

Depois de aceita a conexão e obtido o socket correspondente podemos ler e enviar dados via funções recv()send(). Lembra-se de read() e write() no início do texto? As funções recv()send() fazem a mesma coisa, mas elas lidam melhor com condições de erro e a semântica do socket do tipo SOCK_STREAM. E, ainda, permitem informar algumas opções extras, como MSG_WAITALL, MSG_OOB e MSG_PEEK, para recv(), que não são possíveis via readNão use read() e write() com sockets.

Toda aquela história de devolver tamanho diferente do que foi requisitado também se aplica aqui, mas recv(), se retornar zero, não nos indica que chegamos ao final do arquivo, mas sim ao final da conexão… Neste caso, provavelmente, o outro lado desconectou (executou um close()).

Encerrando uma conexão:

Terminadas as leituras e escritas ao socket, podemos fechá-lo, encerrando a conexão. Isso é feito através da mesma função close() citada lá em cima. Essa syscall realizará o handshake de finalização automaticamente para nós.

Existe também uma função chamada shutdown(). Ela não “fecha” uma conexão, mas impede que dados possam ser recebidos e/ou lidos. Esse seria um procedimento preliminar antes de chamarmos close(), no entanto, ela já faz isso por nós.

Subindo o nível – HTTP:

Até agora, tudo o que vimos foi como um socket deve ser criado, colocado para “escutar”, aceitar conexões, enviar e receber dados e fechar a conexão… Mas, e quanto ao protocolo http?

Http é um protocolo onde o cliente (browser) envia um conjunto de linhas com instruções ao web server (chamado de http daemon). Depois de processada essas instruções (requisição) o servidor monta uma resposta e a envia de volta ao cliente… E isso é tudo!

As linhas que compõem uma requisição seguem um padrão bem definido: A primeira linha nos dá o método requisitado, a URI e a versão do HTTP:

GET / HTTP/1.1\r\n

Apenas dois métodos nos interessam no momento: GET e POST. O método GET é usado quando todos os argumentos que precisam ser passados para o deamon podem sê-lo sem fornecer dados adicionais, ou seja, sem um payload. POST, por outro lado, permite dados adicionais… É interessante notar que toda linha do header de uma requisição termina com o par de caracteres \r e \n.

Faço a diferenciação de header e payload aqui para deixar claro que apenas o header é textual e é composto da linha acima seguida ou não de atributos (veja mais adiante). O payload, se houver, seque o header e pode ser binário (também, veja mais adiante)…

No exemplo acima, a URI (Universal Resource Identifier) é relativa. Ela informa apenas o path, sem nos dizer nada sobre o host. Esse parece ser o padrão dos browsers modernos, mas a especficação HTTP/1.1 permite paths absolutos como “http://mysite.com.br/”. Observe como o Chrome monta suas requisições quando peço para acessar o site “http://127.0.0.1/”:

GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36\r\n
DNT: 1\r\n
Accept-Encoding: gzip, deflate, sdch\r\n
Accept-Language: en-US,en;q=0.8,pt-BR;q=0.6,pt;q=0.4\r\n
\r\n

E, agora, veja como o IceWheasel o faz:

GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Firefox/38.0 Iceweasel/38.8.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
\r\n

Repare que, além da linha do GET, temos outras linhas com atributos, no formato “Attributo: Valor”. Geralmente uma requisição tem o atributo com o nome do Host informado pelo cliente, justamente porque a URI é relativa. Outra informação frequente é os tipos (MIME, Multipurpose Internet Mail Extensions) aceitáveis pelo browser, na resposta. Isso diz ao daemon que ele aceita HTML (text/html), XHTML e XML (application/xhtml+xml e application/xml), prioritariamente, mas também aceita qualquer outra coisa (*/*). O Chrome, como pode ser visto acima, cria uma diferenciação para o tipo “image/webp”. Outra informação que pode ser passada pelo browser é como ele aceita a codificação alternativa do payload, se houver. No caso, o Chrome aceita gzip, deflate e um compactador proprietário do Google chamado sdch.

Note que ambos os browser informam para o servidor que enviarão keep alives, mantendo a conexão viva e dizem ao servidor quem eles são (User-Agent). Essa última informação é importante para alguns sites. O site da Microsoft, por exemplo, não aceita requisições sem agentes.

Mas, e quanto ao método POST? A diferença é que a requisição terá que conter um atributo chamado “Content-Length” com a quantidade de bytes do payload. Ainda, os browsers costumam passar um “Content-Type” com a string do tipo MIME deste conteúdo. Por exemplo, poderíamos requisitar alguma coisa de nosso daemon passando uma string JSON e pedindo que a resposta seja em JSON também… Teríamos que usar o método POST mais ou menos assim:

POST /getdata.cgi HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
User-Agent: MyBrowser/1.0\r\n
Accept: application/json\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Content-Type: application/json\r\n
Content-Length: 14\r\n
\r\n
{ "key": "1" }

O atributo “Content-Length” é essencial aqui porque o servidor precisa saber quantos bytes deve esperar depois do header da requisição.

Atenção! Não devemos confiar muito no atributo “Content-Length” de uma requisição. Um cliente malicioso pode passar um valor diferente do tamanho real do payload. Valores diferentes podem causar bloqueios involuntários da função recv() — se “Content-Length” for maior que o tamanho real do payload — e, dependendo de sua implementação, buffers overruns. Noutro artigo mostro como confirmar o valor informado e, até mesmo, ignorá-lo.

Respondendo ao cliente:

Uma vez processada a requisição o servidor monta a resposta num formato também bem definido:

200 OK HTTP/1.1\r\n
Content-Type: text/html\r\n
Content-Length: 43\r\n
\r\n
<html><body><h2>Success!</h2></body></html>

A primeira linha contém um código de retorno (200), uma mensagem de erro (OK) e a versão do HTTP. As linhas seguintes, se houverem, seguem o mesmo padrão usado na requisição. São atributos repassados para o browser… O header da resposta termina da mesma forma que acontece na requisição: com uma linha vazia. E, se existir um atributo “Content-Length”, seguem n bytes do payload.

Repare que em ambos os casos o conteúdo adicional não precisa ser textual. O conteúdo depende do tipo definido no atributo “Content-Type”. A lista de tipos MIME, mantida pela IANA pode ser vista aqui. E aqui o “Content-Length” não é problemático. Nós mesmo estamos calculando-o.

Os códigos de erro da resposta:

Existem vários: Todos com 3 algarismos. Os códigos da série 100 são históricos e raramente usados. Os códigos da série 200 indicam sucesso do daemon em processar a requisição. Os da série 300 são usados em redirecionamentos. A série 400 são erros no processamento da requisição (ou seja, o erro é do cliente!) e, finalmente a série 500 são erros do próprio daemon.

Alguns desses erros são nossos velhos conhecidos: 401 (Unauthorized), 404 (Not Found), 500 (Internal Server Error)… Outros, nem tanto: 413 (Payload Too Large), 414 (URI Too Long)… Consulte a RFC 2616 (aqui) para maiores detalhes.

Código de exemplo – mostrando uma requisição:

Abaixo temos um código exemplo simples que somente mostrará a requisição feita por um browser e retornará “Success!” para ele (ou código de erro 413 se a requisição for muito grande).

/* vim: set ts=2 et sw=2: */
/* httpd_test1.c 

   Compilar com:
     gcc -o httpd_test1 httpd_test1.c

   Executar com:
     $ ./httpd_test1 8080

     Para executar o "server" com o seu usário normal e
     ouvir a porta 8080, ou...

     $ sudo ./httpd_test1 80

     Para escutar a porta 80, usando o root.
*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>     /* strlen() & memcmp() */
#include <unistd.h>     /* getuid() */
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> /* IPPROTO_TCP & 
                           struct sockaddr_in */
#include <arpa/inet.h>  /* inet_ntoa(). */

/* Coloquei o buffer como global a esse módulo porque,
   declarado assim, ele será colocado no segmento BSS.
   Se o fizesse local à main() ele seria alocado na 
   pilha. */
#define MAX_BUFFER_SIZE 2048
static char buffer[MAX_BUFFER_SIZE + 1];

static void send_request_too_long_error(int);

/* Este teste apenas lê uma requisição vinda de um 
   browser. A idéia aqui é analizar o conteúdo de uma 
   requisição real e confrontá-la com o que a 
   especificação HTTP/1.1 nos diz.

   Lerei apenas uma requisição e a mostrarei a string 
   resultante já que o protocolo HTTP é textual, no que 
   se refere à requisição e aos headers. Não estou 
   preocupado com a implementação estrita do protocolo,
   por enquanto. */

/* Usarei apenas um argumento: A porta que o 'server' 
   ouvirá. O valor de retorno será de 0 até 9, dependendo
   do erro. */
int main(int argc, char *argv[])
{
  int port,
      port_min = 1, /* porta 0 é reservada! */
      listen_fd, conn_fd; /* socket descriptors */

  /* Se não forneceu a porta, reclama! */
  if (!*++argv)
  {
    fputs("\033[1mUsage\033[0m: http_test1 <port#>\n", 
        stderr);
    return 1;
  }

  /* Pega o UID do usuário que chamou esse processo.
     Isso é útil porque não podemos atribuir portas
     abaixo de 1024 se o processo não roda sob root. */
  if (getuid() != 0)
  {
    port_min = 1024;
    fputs("\033[1mWarning\033[0m: User is not root.\n"
          "         "
          "Only ephemeral ports allowed (>= 1024).\n", 
          stderr);
  }

  port = atoi(*argv);

  if (port < port_min || port > 65535)
  {
    fprintf(stderr, 
        "\033[1mError\033[0m: Invalid port number (%d)\n",
        port);
    return 1;
  }

  /* Cria o socket que vai "escutar" - 
     usando IPv4 e TCP */ 
  if ((listen_fd = socket(AF_INET, 
                         SOCK_STREAM, 
                         IPPROTO_TCP)) == -1)
  {
    perror("socket()");
    return 1;
  }

  /* Antes de ligarmos o socket recém-criado a um 
     endereço e uma porta, precisamos dizer à API que 
     queremos "reusar" o endereço, caso ele já esteja em
     uso. */
  int enable = 1;
  if (setsockopt(listen_fd, SOL_SOCKET, 
                           SO_REUSEADDR, 
                           &enable, sizeof(int)) == -1)
  {
    perror("setsockopt()");
    return 1;
  }

  /* Vamos ligar o socket "escutador" no endereço 0.0.0.0,
     porta 'port', usando IPv4. Um endereço zerado é um 
     "coringa" e nos diz que o socket será ligado ao
     dispositivo default. */
  struct sockaddr_in listen_addr = { 
    .sin_family = AF_INET,
    .sin_port = htons(port),
    .sin_addr.s_addr = INADDR_ANY
  };

  /* Ligando o socket ao endereço e porta colocados em 
     'listen_addr'. */
  if (bind(listen_fd, (struct sockaddr *)&listen_addr, 
                     sizeof(listen_addr)) == -1)
  {
    perror("bind()");
    return 1;
  }

  /* Colocando o socket para ouvir. Precisamos de apenas
     1 socket na fila. */
  if (listen(listen_fd, 1) == -1)
  {
    perror("listen()");
    return 1;
  }

  /* A função accept() bloqueará a thread do processo, 
     esperando por uma conexão. Ela aceitará a conexão 
     (3-way handshake) e retorna o socket conectado, 
     bem como informações sobre o requisitante. */
  fputs("Waiting for connection...", stdout);
  fflush(stdout);

  struct sockaddr_in conn_addr;
  socklen_t n;

  if ((conn_fd = accept(listen_fd, 
                           (struct sockaddr *)&conn_addr, 
                           &n)) == -1)
  {
    perror("accept()");
    return 1;
  }

  /* Mostra as informações do requisitante. */
  printf("\nAccepted connection from: "
         "\033[32m%s\033[0m:\033[32m%u\033[0m\n", 
    inet_ntoa(conn_addr.sin_addr), 
    conn_addr.sin_port);

  /* Lê a requisição inteira. */
  char *p, *q;
  ssize_t bytes, remind = MAX_BUFFER_SIZE;

  p = buffer;
  for (;;)
  {
    bytes = recv(conn_fd, p, (size_t)remind, 0);

    /* Quando recv retorna 0 isso pode significar
       duas coisas: Ou não há dados para leitura ou
       a conexão foi quebrada. Não há possibilidade
       de obtermos o primeiro caso, já que o socket
       está no modo 'bloqueável'. */       
    if (bytes == 0)
    {
      fputs("\033[1mError\033[0m: Connection lost!\n", 
          stderr);
      return 1;
    }

    /* Um erro ocorreu! */
    if (bytes == -1)
    {
      /* EINTR só acontece quando há uma interrupção
         (signal). O que não é o caso neste exemplo.
         Só coloquei isso como exemplo. */
      if (errno == EINTR)
        continue;

      perror("recv()");
      return 1;
    }

    /* Avança o ponteiro para o próximo ponto de 
       leitura do buffer, diminui o espaço disponível
       e coloca um '\0' no final, para testarmos pelo
       final da requisição. */
    p += bytes;
    remind -= bytes;
    *p = '\0'';

    /* A requisição não cabe no buffer?
       Diz isso pro requisitante! */
    if (remind <= 0) 
    { 
      send_request_too_long_error(conn_fd);
      fputs("\033[1mError\033[0m: Request too large.\n", 
        stderr); 
      return 1; 
    } 

    /* Temos, pelo menos, 4 bytes no buffer? */
    if ((size_t)(p - buffer) > 4)
    {
      /* Encontramos o final da requisição? */
      q = p - 4;
      if (!memcmp(q, "\r\n\r\n", 4))
        break;  /* Se sim, saimos do loop. */
    }
  }

  /* Mostra o bloco recebido. */
  buffer[bytes] = '\0';
  printf("Received %zd bytes:\n%s", bytes, buffer);

  /* Manda uma resposta (hardcoded). */
  const static char resp[] = "HTTP/1.1 200 OK\r\n"
                             "Content-Type: text/html\r\n"
                             "Content-Length: 43\r\n\r\n"
                             "<html>"
                               "<body>"
                                 "<h2>Success!</h2>"
                               "</body>"
                             "</html>";

  bytes = strlen(resp);
  printf("Sending OK response (%zu bytes)...\n", bytes);

  p = (char *)resp;
  for (;;)
  {
    remind = send(conn_fd, p, bytes, 0);

    if (remind == -1)
    {
      /* EINTR só acontece quando há uma interrupção
         (signal). O que não é o caso neste exemplo.
         Só coloquei isso como exemplo. */
      if (errno == EINTR)
        continue;

      perror("send()");
      return 1;
    }

    bytes -= remind;
    if (bytes <= 0)
      break;

    p += remind;
  }

  /* Fecha o socket da conexão e o listener. */
  close(conn_fd);
  close(listen_fd);

  return 0;
}

void send_request_too_long_error(int fd)
{
  const static char resp[] = 
    "HTTP/1.1 413 Request Entity Too Large\r\n\r\n";

  /* Não me preocupo com o tamanho do buffer aqui. */
  send(fd, resp, strlen(resp), 0);
}

 

ffmpeg avançado: Usando filtros

Além de copiar streams de vídeo, áudio e legendas, mudar características desses streams (bitrate, framerate, tamanho), podemos fazer algumas estripulias com o ffmpeg usando um ou mais arquivos de vídeos e imagens. Isso graças aos filtros. Existem filtros de vídeo e de áudio . A forma de usá-los é montando um diagrama de associações desses filtros (um “grafo”). Por exemplo: Suponha que eu queira colocar um segundo vídeo do lado superior direito do vídeo original, mas com tamanho menor (coisa conhecida por ai como “Picture in Picture”, ou PIP)…

A maneira mais simples de fazer é mudar a escala do segundo vídeo usando o filtro scale e adicionar este vídeo como um “overlay” ao primeiro, especificando a posição onde ele aparecerá. O grafo é este:

ffmpeg-filter

Onde master.mpg é o vídeo principal (o grandão) e pip.mpg é o vídeo que ficará pequeno, sobre o master. O filtro overlay aceita duas entradas: Uma “mestre” e a do “overlayed” e fornece uma saída que, no nosso caso, será o arquivo output.mpg. A maneira de especificar isso no ffmpeg é:

$ ffmpeg -i master.mpg -i pip.mpg \
         -filter_complex "[1] scale=320:240 [pip]; [0][pip] overlay=10:10" \
         output.mpg

Repare: Cada “caixinha” tem um conjunto de especificações de entrada ([1], no caso de scale, e [0][pip], no caso de overlay). A saída do filtro scale, neste caso, também foi nomeada para [pip] (poderia ser qualquer nome) e usada como entrada no filtro overlay.

Já que existem dois vídeos de entrada, temos que usar a opção -filter_complex. Note que o primeiro filtro (scale) muda o tamanho do vídeo cuja fonte é 1 (o segundo vídeo), enquanto o filtro overlay usa a fonte 0 (o primeiro vídeo) e a saída de scale. O filtro scale aceita dois parâmetros, relativos ao tamanho para o qual a fonte será escalonada… Se quiséssemos mudar a escala do vídeo pip.mpg em termos do seu tamanho original, poderíamos usar os argumentos iw e ih para “input width” e “input height”, e realizar alguma operação aritmética como em “[1] scale=0.8*iw:0.8*ih [pip]”, ao invés de usar valores absolutos…

Aliás, a opção -filter_complex pode ser abolida, e usarmos apenas a versão -vf, desde que o segundo filme seja informado na forma de um filtro:

$ ffmpeg -i master.mpg \
         -vf "movie='pip.mpg', scale=320:240 [pip]; [in][pip] overlay=10:10" \
         out.mpg

Prefiro a primeira sintaxe… Nesta, a especificação [in] refere-se ao vídeo de entrada (único!) e o filtro “movie” informa o segundo “vídeo”, que pode ser, inclusive, um arquivo gráfico… Por exemplo, um PNG com fundo transparente! Mas, isso também pode ser feito usando “filtros complexos”… Note também que os filtros moviescale fazem parte da mesma “cadeia” de filtros (scale está ligado ao filtro movie), por isso só precisamos especificar a saída [pip], nessa cadeia e usá-la como entrada para a próxima.

Da mesma maneira que o filtro scale, overlay também aceita dois parâmetros contendo a posição onde a segunda entrada será colocada “sobre” a entrada “master”. No caso, estamos colocando na posição x=10, y=10 (repare bem: argumentos são separados por ‘:’!), mas poderíamos colocar no canto inferior direito facilmente, usando: “[0][pip] overlay=main_w-overlay_w-10:main_h-overlay_h-10”. Aqui, “main_w”, “main_h”, “overlay_w” e “overlay_h” têm significado óbvio.

Para obter uma lista dos filtros disponíveis, use:

$ ffmpeg -filters | less

Você vai ver uma listagem de filtros para vídeo e áudio, misturados… Existem, ainda, “bitstream filters” que são um pouco diferentes (e permitem lidar com filtragens com legendas e fazer certos tweaks com os codecs de vídeo).

E, por falar em “misturados”, note que se ambos os vídeos têm áudio, pode ser interessante criar filtros para eles ou usar as opções -map, para selecionar apenas o áudio de um dos vídeos especificados…

Uma referência mais completa sobre filtros pode ser lida aqui.

Ogg Vorbis, OpenAL e double buffering para áudio

Num outro blog e neste post falei algo sobre OpenAL. Claro que lá eu falava sobre a manipulação de sons em 3D, mas o OpenAL pode ser usado como uma alternativa mais simples para lidarmos com o driver de áudio. Tudo o que temos que fazer é criar buffers, preenche-los com dados PCM, criar uma fonte de áudio e executar um play. Aqui vou te mostrar como fazer double buffering com o OpenAL e também como usar a biblioteca libvorbis (mais especificamente a libvorbisfile) para decodificar arquivos Ogg Vorbis.

Como vamos lidar com OpenAL para tocar áudio estereo sem nos preocuparmos com a localização espacial, os buffers podem ser preenchidos com PCM estéreo que a biblioteca os colocará nos canais normalmente. O problema com o OpenAL é que, se tivermos PCM estéreo e tentarmos criar um listener, colocando-o em outra coordenada que não seja a origem, a biblioteca não conseguirá fazê-lo…. Para lidar com áudio 3D os buffers precisam ter dados monofônicos… Mas, como eu disse, não estamos preocupados com isso aqui.

Ao invés de decodificar todo arquivo ogg vorbis e colocarmos os dados PCM em um grande buffer, temos que decodificá-lo em pedaços e colocando os pedaços da stream PCM em pequenos buffers e ir tocando-os até que todo o arquivo tenha sido decodificado. Podemos fazer isso usando apenas um buffer, mas há um problema: Quando o buffer terminar de ser tocado haverá um pequeno delay entre a decodificação, preenchimeto e o play. Isso causará, na melhor das hipóteses, um “click” na música… E não queremos isso, não é?

Uma solução é usarmos dois buffers. Enquanto um está sendo tocado o outro estará sendo preenchido… Assim, quando o OpenAL terminar de tocar um buffer ele continuará tocando o outro… e assim por diante. Isso pode ser feito com as funções alSourceQueueBuffers()alSourceUnqueueBuffers(). A primeira cria uma fila de buffers e os associa a uma fonte de áudio. A segunda, retira um ou mais buffers da fila. Outra função útil é alGetSourcei(), usando o parâmetro AL_BUFFERS_PROCESSED. Assim podemos obter a quantidade de buffers já processados que estão na fila… Eis um código de exemplo, usando a libvorbisfile (e a libopenal e libalut):

#include <stdlib.h>
#include <stdio.h>
#include <AL/alut.h>
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>

/* Por que 2 buffers? Porque usarei double buffering aqui.
   Enquanto um buffer é tocado o outro fica carregado e
   na espera.

   Podemos usar triple buffering ou mais até... mas,
   double buffering é suficiente. */
#define MAX_OPENAL_BUFFERS 2

/* O tamanho do buffer contendo o PCM decodificado 
   (32 kB é mais que suficiente!). */
#define MAX_PCM_BUFFER_SIZE 32*1024

int main (int argc, char **argv)
{
  OggVorbis_File vf;
  ALuint source;
  ALuint buffers[MAX_OPENAL_BUFFERS];
  ALuint released[MAX_OPENAL_BUFFERS];
  ALint count;
  FILE *fp;
  int i, current_section;
  long pos, ret;
  static char pcmout[MAX_PCM_BUFFER_SIZE];
  int eof = 0;

  if (!*++argv)
  {
    fprintf(stderr, "Uso: vorbisplay <oggfile>\n");
    return 1;
  }

  if ((fp = fopen(*argv, "rb")) == NULL)
  {
    fprintf(stderr, "Erro abrindo arquivo '%s'\n", *argv);
    return 1;
  }

  if (ov_open_callbacks(fp, &vf, NULL, 0, OV_CALLBACKS_DEFAULT) < 0)
  {
    fprintf(stderr, "Arquivo não parece ser um OGG VORBIS.\n");
    fclose(fp);
    return 1;
  }

  vorbis_info *vi = ov_info(&vf, -1);
  printf("Sampling rate: %d, Canais: %d\n", vi->rate, vi->channels);

  /* Tenta criar e carregar o PCM inicial de 2 buffers. */
  alutInit(&argc, argv);
  alGenBuffers(MAX_OPENAL_BUFFERS, buffers);

  /* Temos apenas uma fonte de audio. */
  alGenSources(1, &source);

  /* Carrega os buffers com os streams iniciais,
     decodificados. */
  for (i = 0; i < MAX_OPENAL_BUFFERS; ++i)
  {
    pos = 0;

    while (pos < MAX_PCM_BUFFER_SIZE)
    {
      /* Decodifica para o buffer no formato S16LE. */
      if ((ret = ov_read(&vf, 
                         pcmout + pos, 
                         MAX_PCM_BUFFER_SIZE - pos, 
                         0, 2, 1, 
                         &current_section)) < 0)
      {
        fclose(fp);
        fprintf(stderr, "Erro decodificando stream.\n");
        return 1;
      }

      if (ret == 0)
      {
        eof = 1;
        break;
      }

      pos += ret;
    }

    /* Manda o stream PCM para um buffer do
       OpenAL. */
    alBufferData(buffers[i], AL_FORMAT_STEREO16, pcmout, pos, vi->rate);
  }

  /* Enfilera os buffers e começa a tocar... */
  alSourceQueueBuffers(source, MAX_OPENAL_BUFFERS, buffers);
  alSourcePlay(source);

  if (alGetError())
  {
    fclose(fp);
    fprintf(stderr, "Error playing with OpenAL.\n");
    return 1;
  }

  /* Enquanto não chegamos ao final do arquivo... */
  while (!eof)
  {
    /* Pega a quantidade de buffers já processada e os handles */
    alGetSourcei(source, AL_BUFFERS_PROCESSED, &count);
    alSourceUnqueueBuffers(source, count, released);

    /* Se tem buffers já processados, decodifica e carrega no buffer
       livre. */
    for (i = 0; i < count; ++i)
    {
      pos = 0;

      while (pos < MAX_PCM_BUFFER_SIZE)
      {
        if ((ret = ov_read(&vf, 
                           pcmout + pos, 
                           MAX_PCM_BUFFER_SIZE - pos, 
                           0, 2, 1, 
                           &current_section)) < 0)
        {
          fclose(fp);
          fprintf(stderr, "Erro decodificando stream.\n");
          return 1;
        }

        if (ret == 0)
        {
          eof = 1;
          break;
        }

        pos += ret;
      }

      /* Manda stream PCM para o novo buffer. */
      alBufferData(released[i], AL_FORMAT_STEREO16, pcmout, pos, vi->rate);
    }

    /* Enfilera o buffer preenchido. */
    alSourceQueueBuffers(source, count, released);

    /* Espera 50 ms para tentar de novo... só para não
       matar a CPU de fome. */
    alutSleep(50.0);
  }

  /* Ok, acabou a música. Fecha a loja e apaga a luz. */
  alutExit();
  fclose(fp);

  return 0;
}

Coloque o seu fonte de ouvido e teste o programinha com:

$ gcc -O3 -mtune=native -o vorbisplay vorbisplay.c -lopenal -lalut -lvorbisfile
$ ./vorbisplay music.ogg

Note que o código cria 2 buffers (poderiam ser 3, 4, …), os preenche com os dados decodificados por ov_read(), coloca os buffers numa fila associada à fonte de áudio e chama alSourcePlay(). No outro loop ele fica verificando se existem buffers já processados. Se houver um, ele desinfileira o buffer processado, o preenche e o re-enfileira… E continua fazendo isso até que a música termine!

A chamada a alutSleep() só é necessária (e poderia muito bem ser usleep()) para não causar muita carga na CPU (CPU indo a 100% de uso!)… Provavelmente as funções alGetSourcei e alSourceUnqueueBuffers não são pontos de cancelamento de threads ou não realizam syscalls, assim, enquanto não houver buffers processados, o loop ficará “em vazio” e o kernel pode achar que temos um alto consumo de CPU… O valor de 50 ms foi escolhido arbitrariamente, tentando levar em conta que podem ocorrer task switches (tipicamente uma task switch pode ocorrer a cada 20 ms, nos sistemas modernos).