Projeto: Servidores Web e Sincronização

web-server

Meta-dados

Datas de entrega: 29/11/2013

Forma de entrega: Todos os alunos devem enviar via e-mail um arquivo .zip (arquivos .rar ou com qualquer outra extensão serão desconsiderados) com todo o código fonte do programa e uma breve descrição sobre seu funcionamento.

Grupos: Este projeto poderá ser realizado em grupos de até duas pessoas.

Sobre o projeto

O objetivo deste projeto é desenvolver um servidor web totalmente funcional. Para simplificar o início do projeto, fornecerei o código fonte de uma versão extremamente básica do servidor. Esta versão básica opera com um único thread e parte do seu trabalho será alterar o código para que o servidor funciona de forma mais eficiente utilizando múltiplos threads.

Servidor Web Base

O código para o servidor web que você deverá utilizar como base está disponível aqui. Você deve fazer um fork do repositório no GIT e trabalhar a partir dai em seu projeto.

Para executar o servidor web é preciso fornecer o número da porta que será utilizada pelo servidor. Esse número deve ser necessariamente superior a 2000 para evitar conflitos com portas já utilizadas. Quando você se conectar ao servidor web se certifique de utilizar a mesma porta especificada na criação do servidor.

Por exemplo, se você rodar o servidor na sua máquina local passando o número de porta 6789 como parâmetro o endereço para acessar o servidor será http://localhost:6789

O servidor web base possui apenas algumas poucas centenas de linhas de código em Java. Para manter o código o menor possível estou fornecendo apenas o básico necessário para o desenvolvimento do projeto. Por exemplo, o servidor lida apenas com requisições do tipo GET e CGI e não trata MIME types. O servidor também não é muito robusto; por exemplo, se o cliente web fechar a conexão o servidor pode travar. Você não precisa tratar estes problemas!

Novas Funcionalidades

Você adicionará três funcionalidades chave  ao servidor web base. Primeiro, você deverá fazer o sistema utilizar múltiplos threads para atender as requisições dos clientes, garantido uma sincronização apropriada. Em seguida, você deverá implementar diferentes políticas de escalonamento para que as requisições sejam atendidas em ordens diferentes. Finalmente, você deverá adicionar um módulo estatístico para coletar dados sobre o desempenho do servidor. Você deverá modificar também a forma como o servidor web é executado para permitir a passagem de novos parâmetros (o número de threads, por exemplo).

Além disso, você deverá adicionar novas funcionalidades ao cliente web fornecido para fins de teste. Você deve pensar em como estas novas funcionaliades lhe ajudarão a testar se o servidor web está implementado corretamente. Você modificará o cliente para que ele também utilize múltiplos threads e envie requisições ao servidor utilizando grupos diferentes e bem controlados.

Parte 1: Servidor multi-threaded

O servidor web base fornecido possui um único thread de controle. Servidores com thread único sofrem de um problema fundamental de desempenho uma vez que apenas uma requisição HTTP poderá ser atendida por vez. Portanto, qualquer outro cliente acessando o servidor precisa aguardar até que a requisição HTTP atual seja concluída.  Portanto, a extensão mais importante que você fará no servidor web base é a adição de múltiplos threads.

A abordagem mais simples para contruir um servidor multi-threaded é criar um novo thread para cada nova requisição HTTP. O sistema operacional será então responsável por escalonar estes threads de acordo com sua política própria. A vantagem dessa técnica é que requisições mais curtas não precisarão aguardar pela finalização de requisições longas. Além disso, quando um thread estiver bloqueado aguardando uma operação de E/S os outros threads podem continuar a atender as demais requisições. No entanto, a desvantagem dessa abordagem é o overhead envolvido na criação de um novo thread para cada nova requisição.

Portanto, a abordagem geralmente utilizado é criar um pool fixo de threads durante a inicialização do servidor web. Nessa abordagem, cada thread do pool fica bloqueada enquanto não houver trabalho (requisições para atender). Além disso, se o número de requisições em um dado momento for maior que o tamanho do pool, algumas requisições aguardarão em uma fila até que algum thread esteja disponível para atendê-la.

Em sua implementação você deverá ter um thread mestre que inicia com a criação de um pool de worker threads, cuja quantidade será especificada como parâmetro da linha de comando. O seu thread mestre será o responsável por aceitar novas conexões HTTP através da rede e colocar os descritores dessas requisições em um buffer de tamanho fixo. O thread mestre não deve ler dados da conexão de rede. O tamanho do buffer também deve ser especificado como parâmetro da linha de comando. Note que a implementação base do servidor web possui um único thread que aceita as conexões e imediatamente faz o tratamento da requisição. Em sua implementação o thread mestre deve aceitar a conexão, colocar o descrito da requisição no buffer e voltar a aceitar novas conexões.

Os worker threads devem ser responsáveis por atender as requisições na fila (buffer) de acordo com as políticas de escalonamento descritas a seguir. Assim que um worker thread acorda ele analisa o descritor da requisição, obtém o conteúdo (lê o arquivo requisitado), e retorna o conteúdo para o cliente escrevendo os dados lidos no descritor apropriado. O worker thread então volta a dormir aguardando pela próxima requisição.

Note que o thread mestre e os worker threads estão em uma relação de produtor-consumidor e isso exige que o acesso ao buffer seja sincronizado. Especificamente, o thread mestre precisar ser bloqueado e aguardar sempre que o buffer estiver cheio e os worker threads devem aguardar sempre que o buffer estiver vazio. Neste projeto você deve utilizar semáforos para realizar o controle de acesso ao buffer. Não será admitidas implementações utilizando espera ocupada.

Parte 2: Políticas de Escalonamento

Neste projeto você deverá implementar algumas políticas de escalonamento. Note que quando seu servidor utiliza múltiplos threads ele não tem qualquer controle sobre a ordem em que os threads são executados. Sua função é apenas determinar qual requisição HTTP deverá ser atendida por cada worker thread em seu servidor.

A política de escalonamento é definida como argumento da linha de comando ao iniciar o servidor:

  • FIFO: Os worker threads processam as requisições na ordem em que elas são recebidas, sempre pegando a mais antiga no buffer. Note que as requisições não irão, necessariamente, terminar na mesma ordem já que o escalonamento dos threads depende do sistema operacional.
  • RANDOM: Os worker threads escolhem aleatoriamente uma requisição para atender.
  • SJF: Os worker threads escolhem para atender dentre as requisições aguardando aquela que solicita o arquivo com o menor tamanho em bytes (note que essa política pode implicar em starvation para as requisições a arquivos maiores)
  • PGET: Os worker threads dão prioridade às requisições do tipo GET. Uma requisição do tipo CGI só é executada quando não há nenhum requisição do tipo GET na fila.
  • PCGI: Os worker threads dão prioridade às requisições do tipo CGI. Uma requisição do tipo GET só é executada quando não há nenhum requisição do tipo CGI na fila.

Parte 3: Estatísticas de Uso

Você deverá modificar o servidor para coletar uma variedade de estatísticas. Algumas estatísticas devem ser coletadas por requisição enquanto que outras devem ser coletadas por thread. Todos os resultados devem ser retornados via cabeçalhos HTTP junto a cada requisição. O servidor web base já mostra como fazer isso retornando valores default.

Para cada requisição você deve gravar os seguintes valores e tempos; todos com granularidade de milisegundos.

  • id-requisicao: o número de requisições que chegaram antes desta. Note que este é um valor compartilhado por todos os threads.
  • tempo-chegada-requisicao: o momento em que a requisição chegou.
  • cont-requisicao-agendada: o número de requisições agendas (alocada para um worker thread) antes desta.
  • tempo-agendamenteo-requisicao: o momento em que a requisição foi agendada (alocada para um worker thread)
  • cont-requisicao-concluida: o número de requisições concluídas antes desta.
  • tempo-requisicao-concluida: o momento em que a requisição foi concluída.
  • idade-requisição: o número de requisições que passaram a frente desta por conta da prioriade, ou seja, o número de requisições que chegaram após está mas que foram agendadas para execução antes dela.

Todos os tempos são relativos ao momento em que o servidor web foi iniciado.

Você deve também manter estatísticas para cada thread:

  • id-thread: número de identificação do thread (entre 0 e n – 1)
  • contador-thread: número total de requisição atendidas por esta thread

Portanto, para uma requisição atendida pelo thread i, seu web server deve retornar as estatísticas para a requisição e para o thread i.

Parte 4: Cliente multi-threaded

O cliente web fornecido junto com o servidor base possui também um único thread que envia requisições para o servidor e imprime os resultados. Este cliente base imprime todo o cabeçalho HTTP, incluindo os referentes as estatísticas coletadas. Apesar de o cliente base poder ajudar em alguns testes iniciais, ele não consegue gerar um carga suficientemente grande para o servidor web para checar se o servidor está sincronizando e escalonando corretamente seus threads. Portanto, você precisa modificar o cliente para enviar mais requisições utilizando múltiplos threads. Especificamente, seu novo cliente web deve implementar dois tipos de carga de trabalho (especificados através de um argumento de linha de comando que você adicionará junto com o valor referente a quantidade de threads).

  • Grupos Concorrentes: O cliente cria N threads e usa essas thread para realizar N requisições concorrentes ao servidor para um mesmo arquivo. Esse comportamento deve se repetir indefinidamente até que o cliente seja interrompido. Um thread só deve enviar uma segunda requisição depois que todos os N threads tenham recebido as respostas para a primeira requisição e assim por diante.
  • Grupos FIFO: O cliente cria N threads e usa essas threads para enviar N requisições ao servidor, no entanto, o cliente deve garantir que as requisições são enviadas em ordem sequencial. Especificamente, depois de enviar sua requisição uma thread do cliente deve sinalizar para a thread seguinte que esta já pode enviar sua requisição e assim por diante. Em seguinda, as N threads aguardam pelas respostas do servidor. Depois que todas as threads tenham recebido suas repostas o processo se repete até que o cliente seja interrompido.

Especificações do Programa

Para este projeto você implementará tanto o servidor quanto o cliente HTTP. Seu servido deve ser invocado exatamente como segue:

java br.ufpb.ci.so.p20132.WebServer porta numero-threads  tamanho-buffer  alg-escalonamento

Os argumentos de linha de comando são:

  • porta: número da porta do servidor
  • numero-threads: número de worker threads no servidor
  • tamanho-buffer: tamanho do buffer, representa a quantidade máxima de requisições simultâneas
  • alg-escalonamento: o algoritmo de escalonamento a ser utilizado. Pode ser FIFO, RANDOM, SJF, PGET ou PCGI.

Por exemplo, se o servidor for executado da seguinte forma:

java br.ci.ufpb.so.p20132.WebServer 6789 8 16 FIFO

ele deverá aguardar conexões na porta 6789, criar 8 worker threads e alocar 16 posições no buffer de conexões simultâneas, utilizando o algoritmo FIFO para atendê-las.

O cliente web deve ser invocado da seguinte forma:

java br.ci.ufpb.so.p20132.WebClient servidor porta  numero-threads  alg-escalonamento

Os argumentos da linha de comando são:

  • servidor: endereço do servidor (pode ser localhost ou 127.0.0.1 para servidor rodando na mesma máquina do cliente)
  • porta: número da porta do servidor
  • numero-threads: número de threads a serem criadas no cliente
  • alg-escalonamento: algoritmo de escalonamento a ser utilizado. Precisa ser CONC ou FIFO.

Avaliação

Você deve incluir no arquivo .zip com o código do seu projeto um arquivo README.txt com as seguintes informações:

  • O nome dos membros do grupo e uma breve descrição de como o trabalho foi dividido entre eles
  • Visão geral sobre o projeto: Alguns parágrafos descrevendo a estrutura geral do seu código
  • Bugs ou problemas conhecidos: Uma lista de funcionalidades que você não implementou ou que podem não estar funcionando corretamente
  • Processo de testes (o mais importante!): descreva como você testou seu código.  Especificamente, apresenta quais foram os parâmetros que você utilizou para demonstrar que o seu código funciona corretamente e para checar se ela obedece as diferentes políticas de escalonamento especificadas.