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;
}

 

Anúncios