Colocando o seu código para dormir, esperando um evento (sockets).

Lidar com sockets é como lidar com arquivos via funções como open(), read(), write() e close(). Ao invés de abrir um arquivo (ou criar um) com open(), usamos socket() para obter um file descriptor. Podemos usar read() e write() para ler e enviar dados usando esse descritor, mas é preferível usar send() e recv() [Óbviamente estou falando de TCP!] para maior controle do fluxo de dados.

O problema é que as funções que lidam com I/O são, por definição, blocking… Ou seja, se não há dados a serem lidos, o kernel coloca a chamada para dormir até que alguma coisa aconteça. A mesma coisa acontece se os buffers de saída estiverem cheios, por exemplo. Escrever nessa circunstância pode fazer seu código tirar uma soneca por algum tempo… Quanto tempo? Pode ser indefinido! A alternativa é mudar o estado do descritor, dizendo ao kernel que ele é non-blocking, e verificar o status da leitura/escrita. Isso é fácil de fazer:

int oldflags;
int disconected;
int sz = DATA_SIZE; // tamanho do buffer.
int size;           // o que foi lido por recv.
char *p = buffer;

oldflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, oldflags | O_NONBLOCK);

/* Trabalhar com o descritor 'fd' aqui, que agora é 'non-blocking.
   Isso significa que funções como read() e write() retornarão -1,
   indicando um erro, se elas não puderem fazer seus trabalhos.
   Você terá que verificar o motivo analizando o valor de 'errno',
   neste caso. */
disconnected = 0;
while (1)
{
  errno = 0;
  if ((size = recv(fd, p, sz, 0)) == -1)
    if (errno == EINTR) // Se fomos interrompidos,...
      continue;         // ... tentamos de novo!
    else
      break;            // ... caso contrário, saímos
                        // do loop e precisaremos verificar
                        // errno.
  // provavelmente o host remoto desconectou!
  if (size == 0)
  {
    disconnected = 1;
    break;
  }

  // Conseguiu ler tudo?
  if (size == sz)
    break;

  // Ajusta o ponteiro para a posição do buffer
  // onde a próxima leitura o preencherá e
  // também ajusta o tamanho do bloco que falta.
  p += size;
  sz -= size;
}

// Se necessário, podemos colocar o socket em estado blocking, de novo.
fcntl(fd, F_SETFL, oldflags);

Como recv() funciona?O código (incompleto) acima parece ser promissor. A função recv() retornará logo depois de ser chamada com o tamanho do bloco que conseguiu receber ou -1, em caso de erro… Ou, ainda, zero, em caso de desconexão… O valor zero é o equivalente ao valor EOF… Infelizmente esse não é o melhor método porque tem o potencial de causar alto consumo de processamento.

O bloqueio de recv(), quando o socket está em seu estado padrão, não é causado por algum loop, mas a thread que o chamou é colocada em estado de dormência até que existam dados para serem lidos. Infelizmente essa dormência não tem limitação de tempo. Temos que arrumar um jeito de imitar o que o kernel faz, mas impondo um período máximo (timeout). A idéia é colocar a thread para dormir até que o kernel nos notifique de algum evento e, para isso, podemos usar polling ou até mesmo signals.

Usar sinais é uma boa idéia, mas existem duas syscalls para realizarmos polling com base no status de descritores de arquivos: select e poll. Essas funções facilitam muito a vida. Embora select seja mais tradicional que poll, prefiro a segunda. Acho mais simples e fácil de explicar.

A função poll coloca a thread para dormir até que algum evento ocorra com um descritor. Para usá-la, precisamos preencher uma estrutura com o descritor e um bitmap contendo um flag com os eventos que estamos interessados:

// Queremos monitorar se existem dados
// disponíveis para leitura via descritor 'fd'...
struct pollfd pfd =
  { .fd = fd, .events = POLLIN };
int pollret;

  // Coloca a thread para dormir por, no máximo,
  // 3 segundos (3000 milissegundos), até que um
  // evento ocorra.
again:
  if ((pollret = poll(&pfd, 1, 3000)) == -1)
    if (errno == EINTR) // um sinal interrompeu o poll?
      goto again;
    else
    {
      // trata erro aqui.
    }
  // Ocorreu um timeout sem que um evento tenha ocorrido?
  if (pollret == 0)  // timeout?
  {
    ... trata aqui ...
  }

  // Recebeu um evento POLLIN? Temos dados para ler!
  if (pdf.revents & POLLIN)
  {
    // chama recv() aqui.
  }
}

Aqui poll() bloqueará a thread pelo tempo que nós definimos. Isso é diferente de usar recv() com o descritor “bloqueado”, que pode deixar o processo inativo por um tempo indeterminado. Note que recv() ainda poderá retornar -1 ou 0, no caso de erros ou conexão quebrada. Esses tratamentos você ainda terá que fazer, mas, pelo menos, ele não bloqueará na leitura, já que sabemos que existem dados para serem lidos.

O mesmo princípio aplica-se a send(), só que queremos verificar os eventos POLLOUT.

Outra vantagem de poll (e também select()) é que podemos monitorar diversos descritores ao mesmo tempo. No caso de poll() podemos usar um array de estruturas pollfd, cada item com um descritor diferente. Os membros revents, de cada item, serão preenchidos de acordo. Tudo o que temos que fazer é perforrer o array testando-os, quando poll() retornar.

Eis um exemplo de uso de poll() e sockets non-blocked que não esteja relacionado com send() ou recv(): A função connect() também pode bloquear durante a negociação com o host remoto. Daí, podemos marcar o descritor como O_NONBLOCK, como fizemos no primeiro exemplo, executar um connect() e, em caso de erro (se retornar -1), fazer polling, monitorando eventos POLLIN e POLLOUT para determinar se connect conseguiu o seu feito:

int tryconnect(int fd, struct sockaddr_in *sin, unsigned timeout)
{
  struct pollfd pfd =
    { .fd = fd, .events = POLLIN | POLLOUT };
  int pollret;
  int retries;
  int oldflags;
  int retval = 0;

  // Coloca socket em O_NONBLOCK.
  oldflags = fcntl(fd, F_GETFL);
  fcntl(fd, F_SETFL, oldflags | O_NONBLOCK);

  for (retries = 5; retries; --retries)
  {
    if (connect(fd, sin, sizeof(struct sockaddr_in)) == -1)
    {
again:
      if ((pollret = poll(&pfd, 1, timeout)) == -1)
        if (errno == EINTR)
          goto again;
        else
        {
          retval = -1;
          goto done;
        }
    }
    else
      break;  // CONECTOU!
  }

  // Se retries == 0, não conseguiu conectar!
  if (retries == 0)
    retval = -1;

done:
  // Coloca, de volta, em modo blocking.
  fcntl(fd, F_SETFL, oldflags);
  return retval;
}

Se chamarmos a rotina acima com tryconnect(fd, &sin, 3000) ela vai tentar a conexão 5 vezes em 15 segundos e retornar -1 em caso de erro ou 0 em caso de conexão bem sucedida (do mesmo jeito que connect(), sozinha, faria). Note que não verifiquei se poll() retornou os eventos certos… Só estou usando ele para aproveitar o recurso de timeout. Mas precisamos dizer que queremos monitorar POLLIN e POLLOUT, já que connect envia e recebe pacotes para completar o handshake (em 3 estapas!).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s