Lab5 Tiago Porto

aluno: Tiago Porto Barbosa
ano/sem: 2008/2o.
data do laboratório (8) : 15/12/2008 (2)

Introdução

Os laboratórios 5 e 6 possuem como objetivo desenvolver uma aplicação cliente servidor simples com protocólo próprio.
Neste laboratório 5, implementou-se a parte referente ao servidor da aplicação, feito em Ruby. Utilizou-se conceitos importantes visto no curso como Threads, IO e implementação do padrão Decorator.
Grande parte desse laboratório ja foi desenvolvido antes de iniciar o treinamento do laboratório propriamente dito, foi implementado algumas melhorias.

A aplicação desenvolvida nesse laboratório foi um "bolão virtual", onde apostadores podem dar palpites sobre resultados de partidas esportivas na qual exista um placar final (ex.: jogos de futebol). Cada usuário precisa fazer um cadastro para dispor de uma senha de acesso que garante a autenticidade das suas apostas.
O servidor da aplicação agrega todos os jogos, apostas e usuários em um local central. Cada partida tem um "tempo de finalização", a partir do qual não será mais possível fazer apostas.

Desenvolvimento

Arquivo 'intilize.rb'

Um importante arquivo que inicia todos os outros arquivos importantes.
Inicia os builders para tratar ações relacionados a partidas e usuarios.

require 'list'
require 'bet'
require 'match'
require 'match_list'
require 'user'
require 'user_list'
require 'decorators/decorator'
require 'decorators/list_persistance_decorator'
require 'decorators/list_thread_safe_decorator'
require 'decorators/user_list_logger_decorator'
require 'decorators/match_list_logger_decorator'
require 'decorators/match_logger_decorator'
require 'printers/match_printer'
require 'printers/match_list_printer'
require 'printers/yaml_match_list_printer'
require 'decorators/match_thread_safe_decorator'
require 'decorators/match_list_thread_safe_decorator'
require 'decorators/match_persistance_decorator'
require 'decorators/match_list_persistance_decorator'
require 'decorators/user_list_builder'
require 'decorators/match_list_builder'
require 'request_reader'
require 'printers/yaml_match_list_printer'
require 'printers/yaml_user_list_printer'
require 'main'
require 'logger'
 
$logger = Logger.new("bolao.log")
$logger.level = Logger::INFO
 
include Decorators
$match_list = MatchListBuilder.new.add_all("matches.yml")
$user_list = UserListBuilder.new.add_all("users.yml")
 
class String
  def to_score
    self.strip.gsub(/\s+/, '').downcase
  end
end

Classe Decorator

Super classe que é usada para gerar herdança para as classes que utilizam o padrão "Decorator"

module Decorators
  class Decorator
    def initialize(obj)
      @decorated = obj
    end
 
    attr_reader :decorated
 
    def method_missing(method_name, *args)
      @decorated.send(method_name, *args)
    end
 
    #o método está implementado na classe Object, portanto precisamos delegá-lo,
    #uma vez que não será capturado pelo method_missing.
    def to_a
      @decorated.to_a
    end
  end
end

Classes ListPersistencDecorator e MatchListPersistenceDecorator

Classes usadas para armazenar as informações dos usuários e das partidas em arquivos com a serialização YAML

require 'yaml'
module Decorators
  class ListPersistanceDecorator < Decorator
 
    def initialize(obj, filename)
      super(obj)
      @filename = filename
      load_list
    end
 
    def add(*args)
      obj = @decorated.add(*args)
      save
      obj
    end
 
    def load_list
      plain_items = begin
        YAML.load_file(@filename)
      rescue
        []
      end
      # redecora os items salvos e armazena no array de items
      @decorated.items = plain_items.map { |plain_item| self.decorated.item_decorator.call(plain_item) }
    end
 
    def save
      File.open(@filename, 'w+') do |f|
        #recuperamos o objeto simples, sem decoração, antes de armazená-lo no disco
        f << @decorated.map{|match_dec| get_plain_object_from_decorator(match_dec)}.to_yaml
      end
    end
 
    private
 
    def get_plain_object_from_decorator(decorated)
      if decorated.respond_to?(:decorated)
        get_plain_object_from_decorator(decorated.decorated)
      else
        decorated
      end
    end
 
  end
end
require 'yaml'
module Decorators
  class MatchListPersistanceDecorator < Decorator
 
    def initialize(obj, filename)
      @filename = filename
      super(obj)
      old_match_decorator = self.decorated.item_decorator
      self.decorated.item_decorator = lambda do |match|
        MatchPersistanceDecorator.new(old_match_decorator.call(match), @filename, self)
      end
    end
 
    def add(name, end_time)
      obj =  @decorated.add(name, end_time)
      save
      obj
    end
 
    def load_list
      plain_items = begin
        YAML.load_file(@filename)
      rescue
        []
      end
      # redecora os items salvos e armazena no array de items
      @decorated.items = plain_items.map { |plain_item| self.decorated.item_decorator.call(plain_item) }
    end
 
    def save
      File.open(@filename, 'w+') do |f|
        #recuperamos o objeto simples, sem decoração, antes de armazená-lo no disco
        f << @decorated.map{|match_dec| get_plain_object_from_decorator(match_dec)}.to_yaml
      end
    end
 
    private
 
    def get_plain_object_from_decorator(decorated)
      if decorated.respond_to?(:decorated)
        get_plain_object_from_decorator(decorated.decorated)
      else
        decorated
      end
    end
 
  end
end

Classes ListThreadSafeDecorator e MatchListThreadSafeDecorator

Classes usadas para cuidar do sincronismo entre as várias requisicoes, principalmente para persistencia.

require 'monitor'
module Decorators
  class ListThreadSafeDecorator < Decorator
    include MonitorMixin
 
    def add(*args)
      synchronize do
        @decorated.add(*args)
      end
    end
 
  end
end
require 'monitor'
 
module Decorators
  class MatchListThreadSafeDecorator < Decorator
    include MonitorMixin
 
    def initialize(decorated)
      super
      old_match_decorator = self.decorated.item_decorator
      self.decorated.item_decorator = lambda do |match|
        MatchThreadSafeDecorator.new( old_match_decorator.call(match) )
      end
    end
 
    def add(name, end_time)
      synchronize do
        @decorated.add(name, end_time)
      end
    end
 
  end
end

Classes UserListLoggerDecorator e MatchListLoggerDecorator

Classes responsaveis por gravar no arquivo de logger as acões gerais relacionadas ao bolão, como fazer aposta, criar usuario, etc.

module Decorators
  class UserListLoggerDecorator < Decorator
    def add(username, password)
      user = @decorated.add(username, password)
      $logger.info "User #{username} was added successfully."
      user
    end
    def authenticate(username, password)
      user = @decorated.authenticate(username, password)
      if user
        $logger.info "User #{username} logged in."
      else
        $logger.warn "User #{username} (pass: #{password})  couldn't log in."
      end
      user
    end
  end
end
module Decorators
  class MatchListLoggerDecorator < Decorator
 
    def initialize(decorated)
      super
      old_match_decorator = self.decorated.item_decorator
      self.decorated.item_decorator = lambda do |match|
        MatchLoggerDecorator.new( old_match_decorator.call(match) )
      end
    end
 
    def add(name, end_time)
      match = @decorated.add(name, end_time)
      $logger.info "Match '#{name}' was added successfully."
      match
    end
  end
end

Classe Bet

Classe que caracteriza uma aposta: placar, apostadores, partida, etc.

class Bet
  def initialize(score, match)
    @score, @match = score.to_score, match
    @holders = []
  end
 
  attr_reader :score, :match, :holders
 
  def add_holder(holder)
    @match.bets.each{ |b| b.holders.delete_if {|h| h.username == holder.username } }
    @holders << holder
  end
end

Classe List

Super classe usada para dar as funções de lista para UserList e MatchList, como decorar seus itens.

class List
  include Enumerable
  attr_writer :items
  attr_accessor :item_decorator
  attr_accessor :item_class
 
  def initialize
    @item_decorator = lambda {|item| item} # não decora com nada
    @items = []
  end
 
  def each(&block)
    @items.each(&block)
  end
 
  def add(*args)
    # @item_class é a classe do item que será adicionado, deve ser definida
    @items << ( item = @item_decorator.call(@item_class.new(*args)) )
    item
  end
end

Classe Match

Classe que caracteriza uma partida, com placar, apostas, tempo de termino, nome, ganhadores.

class Match
 
  def initialize(name, end_time)
    @name, @end_time, @bets = name, end_time, []
  end
 
  attr_reader :name, :bets, :final_score, :end_time
 
  def make_bet(score, user)
    raise "Can't make bet in a finalized match." if finalized?
    raise "Can't make bet in a match that already started." if Time.now > end_time
    score = score.to_score
    bet = @bets.find { |b|  b.score == score }
    @bets << bet = Bet.new(score, self) if bet.nil?
    bet.add_holder(user)
    bet
  end
 
  def bet_count
    @bets.inject(0) {|count, b| count += b.holders.length; count}
  end
 
  def winners
    winners = @bets.find{|b| b.score == @final_score}
    (winners) ? winners.holders : nil
  end
 
  def finalized?
    !!@final_score
  end
 
  def finalize(final_score)
    @final_score = final_score.to_score
  end
end

Classe User

Classe que caracteriza um usuário, com username, password encriptado.
Para encriptar o password foi usado o Digest::SHA1.hexdigest().

require 'digest/sha1'
 
class User
  def initialize( username, password )
    @salt = rand
    @username, @crypted_pass = username, encrypt_password(password)
  end
 
  attr_reader :username, :crypted_pass
 
  def encrypt_password(password)
    Digest::SHA1.hexdigest("#{password} #{@salt}")
  end
 
  def make_bet(score, match)
    match.make_bet(score, self)
  end
 
  def current_bets(match_list = $match_list)
    bets_from_match_list match_list.list_current
  end
 
  def finalized_bets(match_list = $match_list)
    bets_from_match_list match_list.list_finalized
  end
 
  def winning_bets(match_list = $match_list)
    finalized_bets(match_list).find_all {|bet| bet.score == bet.match.final_score}
  end
 
  private
 
  def bets_from_match_list(match_list_array)
    match_list_array.map { |m| m.bets.find_all { |b| b.holders.find { |u| u.username == self.username } } }.flatten
  end
end

Classe MatchList

Classe que herda de List e que armazena todas as partidas do compeonato (Match).

require 'list'
class MatchList < List
 
  def initialize
    super
    @item_class = Match
  end
 
  def each(&block)
    @items.sort_by {|m| m.end_time }.each(&block)
  end
 
  def add(name, end_time)
    raise "Match name already in use." if find {|m| m.name == name}
    super
  end
 
  def list_current
    reject { |m| m.finalized? }
  end
 
  def list_finalized
    find_all { |m| m.finalized? }
  end
 
  def find_by_name(name)
    find{ |match| match.name == name }
  end
end

Classe UserList

Classe que herda de List e armazena todos os usuário do Bolão Virtual.

require 'list'
class UserList < List
 
  def initialize
    super
    @item_class = User
  end
 
  def add(username, password)
    raise "Username taken!" if find {|u| u.username == username}
    super
  end
 
  def authenticate(username, password)
    user = find{|u|  u.username == username}
    user && (user.crypted_pass == user.encrypt_password(password)  ?  user : nil)
  end
end

Classe RequestReader

Classe que faz o tratamento das requisições e encaminha a respostas para os clientes.
Compreende um protocólo próprio desenvolvido nesse laboratório, que usa a serialização com YAML.

require 'initialize'
 
class RequestReader
  include Printers
  def initialize(input= $stdin, output = $stdout)
    @input, @output = input, output
    @user = nil
    puts "initialize"
  end
 
  def wait(io = @input)
    puts "wait"
    loop do
    string = io.gets
    break if string && string.include?("INICIO")
    end
    yaml = io.gets("\nFIM") #depois do INICIO, lê tudo até a palavra FIM
    yaml = yaml.chomp("FIM") #tira o FIM do final do string que foi lido do io
    @requisicao = YAML.load(yaml)
 
    @user_list_printer = YamlUserListPrinter.new($user_list, @output)
    @match_list_printer = YamlMatchListPrinter.new($match_list, @output)
    puts "soh pra saber"
    if @requisicao.include?("request")
      if @requisicao["request"]["operation"] == "user_create"
        user_create
      elsif @requisicao["request"]["operation"] == "user_autenticate"
        user_autenticate
      elsif @requisicao["request"]["operation"] == "current_matches"
        current_matches
      elsif @requisicao["request"]["operation"] == "finalized_matches"
        finalized_matches
      elsif @requisicao["request"]["operation"] == "match_information"
        match_information
      elsif @requisicao["request"]["operation"] == "own_bets_information"
 
        own_bets_information
      elsif @requisicao["request"]["operation"] == "make_bet"
 
        make_bet
      elsif @requisicao["request"]["operation"] == "close_connection"
        puts "entrou aki"
        close_connection
      end
    else
      nil
    end
  end
 
  def user_create
    username = @requisicao["request"]["parameters"][0]
    password = @requisicao["request"]["parameters"][1]
    if !!(user = $user_list.add(username, password))
      @user_list_printer.user_create_succed
    else
      @user_list_printer.user_create_unsuceed
    end
  end
 
  def user_autenticate
    username = @requisicao["request"]["parameters"][0]
    password = @requisicao["request"]["parameters"][1]
    @user = $user_list.authenticate(username, password)
    if !!@user
      @user_list_printer.user_autenticate_succed
    else
      @user_list_printer.user_autenticate_unsucced
    end
  end 
 
  def current_matches
    if !!@user
      @match_list_printer.print_current_matches
    end
  end
 
  def finalized_matches
    if !!@user
      @match_list_printer.print_finalized_matches
    end
  end
 
  def match_information
    if !!@user
      puts "user guardado"
      name = @requisicao["request"]["parameters"]
      @match_list_printer.print_match_information(name)
    end
  end
 
  def own_bets_information
    if !!@user
      @match_list_printer.print_own_bets(@user)
    end
  end
 
  def make_bet
    if !!@user
      name = @requisicao["request"]["parameters"][0]
      score = @requisicao["request"]["parameters"][1]
      match = $match_list.find_by_name(name)
      if !(match.finalized?)
        match.make_bet(score, @user)
        @match_list_printer.print_make_bet
      else
        @match_list_printer.print_not_make_bet
      end
    end
  end
 
  def close_connection
    puts "passo 2 ok"
    puts "1"
    @user_list_printer.print_close_connection
    puts "2"
    @input.close
  end
end

Class YamlMatchListPrinter e YamlUserListPrinter

Classe que utilizam o IO padrão da aplicação para retornar as infomação que foram pedidas ou confirmações de operações bem sucedidas.
Imprimem na forma do protocólo que foi desenvolvido para esse laboratório.

require 'yaml'
module Printers
  class YamlMatchListPrinter
    def initialize(match_list, io = $stdout)
      @match_list, @io = match_list, io
    end
 
    def print_matches
      @io.puts "INICIO"
      @io.puts(
        {
          :response => @match_list.to_a.map do |m|
            {
              :name => m.name,
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :finalized => "#{(m.finalized?) ? '' : 'not ' }finalized",
              :final_score => m.final_score || "_ x _",
              :bet_count => "#{m.bet_count} bets",
              :winners => (m.winners) ? m.winners.map{|u|u.username}.join(", ") : "no winners"
            }
          end
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def print_current_matches
      @io.puts "INICIO"
      @io.puts(
        {
          :response => @match_list.list_current.map do |m|
            {
              :name => m.name,
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :bet_count => "#{m.bet_count} bets",
            }
          end
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def print_finalized_matches
      @io.puts "INICIO"
      @io.puts(
        {
          :response => @match_list.list_finalized.map do |m|
            {
              :name => m.name,
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :finalized => "#{(m.finalized?) ? '' : 'not ' }finalized",
              :final_score => m.final_score || "_ x _",
              :bet_count => "#{m.bet_count} bets",
              :winners => (m.winners) ? m.winners.map{|u|u.username}.join(", ") : "no winners"
            }
          end
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def print_match_information(name)
      m = @match_list.find_by_name(name)
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
 
              :name => m.name,
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :finalized => "#{(m.finalized?) ? '' : 'not ' }finalized",
              :final_score => m.final_score || "_ x _",
              :bet_count => "#{m.bet_count} bets",
              :holders => !!(m.bet_count) ? (m.bets.to_a.map{|bet| bet.holders.to_a.map{|u| u.username}.join(", ")}).join(", ") : "no holders",
              :winners => (m.winners) ? m.winners.map{|u|u.username}.join(", ") : "no winners"
            }
       }.to_yaml
       )
 
      @io.puts "FIM"
    end
 
    def print_own_bets(user)
      matches = @match_list.to_a.find_all { |m|  !m.bets.to_a.find_all{ |b| !b.holders.to_a.find_all{ |h| h.username == "Tiago" }.empty? }.empty?} 
 
      @io.puts "INICIO"
      @io.puts(
        {
          :response => matches.map do |m|
            {
              :name => m.name,
              :end_time => m.end_time.strftime("%d/%m/%Y %H:%M"),
              :finalized => "#{(m.finalized?) ? '' : 'not ' }finalized",
              :final_score => m.final_score || "_ x _",
              :bet_count => "#{m.bet_count} bets",
              :holders => !!(m.bet_count) ? (m.bets.to_a.map{|bet| bet.holders.to_a.map{|u| u.username}.join(", ")}).join(", ") : "no holders",
              :winners => (m.winners) ? m.winners.map{|u|u.username}.join(", ") : "no winners"
            }
          end
       }.to_yaml
      )
 
      @io.puts "FIM"
    end
 
    def print_make_bet
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
              :bet_made => true
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def print_not_make_bet
      @io.puts "INICIO"
      @io.puts(
        {
          :response =>
            {
              :bet_made => false
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
  end
end
require 'yaml'
module Printers
  class YamlUserListPrinter
    def initialize(user_list, io = $stdout)
      @user_list, @io = user_list, io
    end
 
    def user_create_succed
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
              :create => true
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def user_create_unsucced
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
              :create => false
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def user_autenticate_succed
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
              :autenticate => true
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def user_autenticate_unsucced
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
              :autenticate => false
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
 
    def print_close_connection
      @io.puts "INICIO"
      @io.puts(
        {
          :response => 
            {
              :connection => false
            }
       }.to_yaml
      )
      @io.puts "FIM"
    end
  end
end

Arquivo 'main.rb'

Arquivo que inicializa um novo objeto do tipo RequestReader a cada nova conexão, e mantem ele escutando a conexão.

require 'initialize'
 
def main(input=$stdin, output=$stdout)
  puts "main"
  @request_reader = RequestReader.new(input, output)
  loop do
    @request_reader.wait(input)
  end
 
end

Arquivo 'server.rb'

Arquivo que abre uma conexão servidor na porta 4344, e fica escutando até uma nova conexão chegar.
Então encaminha para o método main() do arquivo 'main.rb' o socket correspondente.

require 'initialize'
require 'socket'
 
server = TCPServer.new(4344) 
 
while ( io_socket = server.accept ) 
  puts "conectou"
  Thread.new do
    main(io_socket, io_socket)
  end  
end

PROTOCOLO

* Criar usario

INICIO
---
request: 
 operation: create_user 
 parameters: 
  - (name) 
  - (password)
FIM

-* Criado com sucesso

INICIO
---
:response:
- :create: true
FIM

-* Falha ao criar

INICIO
---
:response:
- :create: false
FIM

* Autenticar Usuario

INICIO
---
request: 
 operation: autenticate_user 
 parameters: 
  - (name) 
  - (password)
FIM

-* Autenticado com sucesso

INICIO
---
:response:
- :autenticate: true
FIM

-* Falha ao autenticar

INICIO
---
:response:
- :autenticate: false
FIM

* Listar partidas atuais

INICIO
---
request: 
 operation: current_matches 
FIM

INICIO
---
:response:
- :name: (name)
  :end_time: ("%d/%m/%Y %H:%M")
  :bet_count: (bet counter)
FIM

* Listar partidas encerradas

INICIO
---
request: 
 operation: finalized_matches 
FIM

INICIO
---
:response:
- :name: (name)
  :end_time: ("%d/%m/%Y %H:%M"),
  :finalized: (finalized or not)
  :final_score: ("_ x _")
  :bet_count: (bet counter)
  :winners: (winner users)
FIM

* Informações detalhadas de uma partida

INICIO
---
request: 
 operation: match_information 
FIM

INICIO
---
:response:
- :name: (name)
  :end_time: ("%d/%m/%Y %H:%M"),
  :finalized: (finalized or not)
  :final_score: ("_ x _")
  :bet_count: (bet counter)
  :holders: (holders)
  :winners: (winners)
FIM

* Fazer aposta em uma partida

INICIO
---
request: 
 operation: make_bet
 parameters:
  - (name of match)
  - (score)
FIM

-* Aposta realizada com sucesso

INICIO
---
:response:
- :bet_made: true
FIM

-* Falha ao realizar aposta

INICIO
---
:response:
- :bet_made: false
FIM

* Encerrar a conexão

INICIO
---
request: 
 operation: close_connection
FIM

INICIO
---
:response:
- :connection: close
FIM

Arquivo anexado

Lab5: Bolao Virtual

Conclusão

O Laboratório 5 não foi complicado no sentido de desenvolvimento, porém de compreensão. Por tratar-se de uma linguagem com tipagem dinamica, a aplicação feita em Ruby e usando o padrão decorator foi um pouco dificil de entender o seu funcionamento.
Fora isso, o resto foi mais tranquilo.
Para desenvolver o protocolo, foi necessário um estudo em YAML, um padrão de serialização de estruturas de dados com fácil compreensão humana e com suporte em várias outras linguagem de programação.
Não foi necessário enteder bem como funciona o TCPServer, pois abstraiu-se o conceito para IO do ruby.
Acho que com mais testes diferente ajudaria para entender mais rápido o funcionamento e objetivo da aplicação
Considero que aprendi bastante nesse laboratório e adquiri prática com Ruby, conseguindo escrever funcionalidades muito complexas em outras linguagens com apenas 1 linha.

Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License