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.

Anúncios