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
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.