Sockets em Ruby Curso de Tecnologia em Redes de Computadores Programação para Redes
Sockets em Ruby A biblioteca padrão de Ruby oferece um conjunto de classes para a manipulação de sockets. require socket Para aplicações baseadas em TCP: Classes TCPServer e TCPSocket. Para aplicações baseadas em UDP: Classe UDPSocket. 2
Classe TCPSocket Representa um socket baseado em TCP. É utilizado pelo cliente e pelo servidor para a troca de mensagens. Construtor new(host, porta) Cria e retorna um novo TCPSocket capaz de enviar dados para um servidor TCP localizado em host (nome ou endereço IP). Utilizado pelo cliente para conectar-se a um servidor. Método close() Fecha o socket, não sendo mais possível a E/S de dados. A entrada e saída de dados pode ser realizados pelos métodos de saída normalmente utilizados com streams (puts, print, write, syswrite, gets e read) ou pelos métodos específicos a sockets (recv, recfrom e send). 3
Classe TCPServer Classe TCPServer: representa um socket servidor baseado em TCP. Construtor new(porta) Cria e retorna um novo socket TCPServer na porta especificada. Método accept() Coloca o socket em modo de espera. A execução fica bloqueada até que seja recebida uma requisição cliente. Retorna uma instância de TCPSocket que é utilizada para receber e enviar dados para o cliente. 4
Exemplo: servidor calculadora (1/2) require 'socket' def processar(linha) tokens = linha.split if tokens[0] == 'soma' return tokens[1].to_f + tokens[2].to_f elsif tokens[0] == 'raiz_quadrada' return Math.sqrt(tokens[1].to_f) else return 'Mensagem invalida' end end 5
Exemplo: servidor calculadora (2/2) serversocket = TCPServer.new(2000) puts 'Servidor calculadora iniciado' loop { } clientsocket = serversocket.accept while linha = clientsocket.gets end resposta = processar(linha) clientsocket.puts(resposta) clientsocket.close 6
Exemplo: cliente calculadora require 'socket' hostname = 'localhost' port = 2000 socket = TCPSocket.new(hostname, port) socket.puts('soma 5 3') puts socket.gets socket.puts('raiz_quadrada 100') puts socket.gets socket.puts('maluco 5 3') puts socket.gets socket.close 7
Exemplo: protocolo Na mesma conexão, o cliente pode solicitar várias operações de cálculo. Cada operação deve ser enviada como uma string finalizada com o caractere de nova linha (\n). O primeiro token da linha indica a operação (soma ou raiz quadrada). Os tokens seguintes indicam os argumentos da operação. A operação de soma necessita de dois argumentos. A operação de raiz quadrada necessita de um argumento. Mensagens desconhecidas são respondidas com Mensagem invalida. Após solicitar as operações, o cliente encerra a conexão. O servidor responde cada operação com o resultado do cálculo. Após ler todas as operações solicitadas (linhas), o servidor encerra a conexão. 8
Exemplo: protocolo Cliente Servidor Cliente Servidor soma <a> <b> <a+b> raiz_quadrada <a> <raiz de a> Cliente Servidor <não especificado> Mensagem invalida 9
Exercício 1) Escreva um programa servidor que lê uma frase e uma palavra enviados pelo cliente e responde com a quantidade de ocorrências da palavra presentes na frase. Teste a implementação com um cliente Telnet e com um programa cliente escrito por você. 2) Especifique o protocolo utilizado pelo jogo jokenpo disponibilizado na página disciplina (jokenpo.zip). 3) Implemente um jogo de par ou ímpar em modo clienteservidor. Um dos jogadores será um usuário humano que deve utilizar um programa cliente. O outro jogador deve ser o próprio programa servidor. Após a implementação, escreva a implementação do protocolo utilizado na aplicação. 10
Servidor multithread Servidores tipicamente devem ser capazes de gerenciar vários clientes de forma simultânea. Caso contrário, cada cliente deve aguardar a sua vez na fila para ser atendido. Para tal, devemos utilizar threads. Threads são linhas de execução paralelas que podem ocorrer em um mesmo programa. Threads também são chamadas de processos leves. 11
Exemplo servidor sem threads require 'socket' serversocket = TCPServer.new(2000) loop { clientsocket = serversocket.accept clientsocket.gets #simula uma operação demorada #como E/S em disco sleep(2); clientsocket.puts('ok') clientsocket.close } 12
Exemplo cliente require 'socket' hostname = 'localhost' port = 2000 threads = [] #armazena as threads que serão criadas inicio = Time.now for i in 1..5 do #cria thread que solicita uma nova conexão com o servidor #cada thread funciona como um programa cliente t = Thread.start(TCPSocket.new(hostname, port)) do socket socket.puts('msg') puts socket.gets socket.close end threads << t #adiciona a thread ao array end 13
Exemplo cliente (cont.) #avisa este programa para aguardar até que todas as threads #tenham terminado sua execução threads.each do t t.join end fim = Time.now #calcula e imprime o tempo de execução puts "#{fim - inicio} s" 14
Cliente fluxo de execução para 3 Programa Principal 3ª thread threads 2ª thread 1ª thread Cria 1ª thread Cria 2ª thread Cria 3ª thread socket.puts(msg) socket.puts(msg) socket.gets() socket.puts(msg) socket.gets() socket.gets() socket.close() socket.close() Aguarda as threads (join) socket.close() Fim 15
Exemplo servidor com threads require 'socket' serversocket = TCPServer.new(2000) puts 'Servidor multithread iniciado' loop { Thread.start(serverSocket.accept) do clientsocket msgcliente = clientsocket.gets #simula uma operação demorada como a gravação em disco sleep(2); #dorme 2 segundos clientsocket.puts('ok') clientsocket.close end } 16
Mutex Devemos estar atentos aos recursos compartilhados por múltiplas threads (variáveis, arquivos, objetos, etc). Quando o acesso simultâneo puder causar inconsistências, podemos utilizar um objeto Mutex para criar blocos de código sincronizados. Estes blocos serão acessados a cada vez por uma única thread. 17
Exemplo servidor sem mutex require 'socket' serversocket = TCPServer.new(2000) puts 'Servidor iniciado' saldo = 100.0 loop { Thread.start(serverSocket.accept) do clientsocket saque = clientsocket.gets.to_f if saldo >= saque sleep(1) saldo = saldo - saque clientsocket.puts('ok') else clientsocket.puts('negado') end clientsocket.close puts " saldo: #{saldo}" end } 18
Exemplo múltiplos clientes require 'socket' realizando saque host = 'localhost' port = 2000 threads = [] for i in 1..2 do t = Thread.start(TCPSocket.new(host, port)) do socket socket.puts('90') puts socket.gets socket.close end threads.push(t) end threads.each do t t.join end 19
Exemplo servidor com mutex require 'socket' serversocket = TCPServer.new(2000) puts 'Servidor iniciado' saldo = 100.0 mutex = Mutex.new loop { Thread.start(serverSocket.accept) do clientsocket saque = clientsocket.gets.to_f mutex.synchronize{ if saldo >= saque sleep(1) saldo = saldo - saque clientsocket.puts('ok') else clientsocket.puts('negado') end puts "saldo #{saldo}" } clientsocket.close end } 20
Exercício 4 Escreva um servidor para um sistema de votação entre três candidatos. O servidor deve ser capaz de processar as seguintes mensagens de entrada: candidatos? Deve retornar uma string com os nomes e números dos candidatos. Cada candidato deve ser separado pelo caractere #. Exemplo de resposta: 1 paulo#2 pedro#3 ana votar n Adiciona um voto para o candidato com número n. Deve retornar a string ok caso a operação seja bem sucedida ou zero em caso contrário. resultado senha Caso senha seja válida, deve retornar uma string com o total de votos de cada candidato. Cada candidato deve ser separado pelo caractere #. No caso de senha inválida, deve retornar zero. A senha é armazenada pelo servidor. Para quaisquer outras mensagens, o servidor deve retornar zero. Escreva um programa que cria 1000 clientes paralelos, onde cada cliente solicita a lista de candidatos e vota em um de forma aleatória. Escreva um programa cliente para obter o resultado da eleição. 21
Transmissão de dados binários Os métodos gets e puts são adequados apenas para dados do tipo string. Para dados binários, devemos utilizar os métodos read e syswrite. read(n): lê n bytes. Os bytes lidos são retornados como uma string codificada em ASCII-8BIT (string binária). Caso o tamanho da string seja menor que n, significa que foi atingido o fim do arquivo (EOF). Retorna nil caso a leitura seja direto no EOF. syswrite(bytes): escreve bytes, o qual que deve ser uma string binária. Retorna a quantidade de bytes escritos. 22
Exemplo: transmissão de arquivo - cliente require 'socket' sock = TCPSocket.new('localhost', 2000) begin nome_arquivo = gets.chomp arquivo = File.new(nome_arquivo, "r") tamanho = File::size(nome_arquivo) bytes_arquivo = arquivo.read(tamanho) sock.puts(nome_arquivo) sock.puts(tamanho.to_s) sock.syswrite(bytes_arquivo) ensure sock.close arquivo.close end 23
Exemplo: transmissão de arquivo - servidor require 'socket' serversocket = TCPServer.new(2000) clientsocket = serversocket.accept begin nome_arquivo = clientsocket.gets.chomp tamanho_arquivo = clientsocket.gets.chomp.to_i bytes_arquivo = clientsocket.read(tamanho_arquivo) arquivo = File.new("copia_#{nome_arquivo}", "wb") arquivo.syswrite(bytes_arquivo) ensure serversocket.close end 24
Exemplo: transmissão de arquivo - protocolo Cliente Servidor <nome do arquivo> <tamanho do arquivo> <bytes> 25
Exemplo protocolo binário Formato das mensagens do cliente: operação argumento 1 argumento 2 Campos (todos com 1 byte): operação: indica a operação desejada, soma (01) ou potência quadrada (02). argumento 1: primeiro termo da soma, ou base da potência. Tipo inteiro. argumento 2: segundo argumento da soma. Não utilizado na potência. Tipo inteiro. 26
Exemplo - protocolo binário O servidor sempre retorna dois bytes que formam um único campo ordenado em network byte order. Este campo armazena o resultado da operação solicitada ou FFFF caso a mensagem de solicitação do cliente não tenha sido reconhecida. 27
Exemplo protocolo binário servidor require 'socket' serversocket = TCPServer.new(2000) begin socket = serversocket.accept loop{ byte_um = socket.read(1).unpack('c')[0] if byte_um == 0x00 campos = socket.read(2).unpack('cc') soma = campos[0] + campos[1] out = [soma].pack('n') elsif byte_um == 0x01 byte_dois = socket.read(1).unpack('c')[0] quadrado = byte_dois * byte_dois out = [quadrado].pack('n') else out = [0xFFFF].pack('n') end socket.syswrite(out) } ensure serversocket.close end 28
Exemplo protocolo binário cliente require 'socket' sock = TCPSocket.new('localhost', 2000) begin #soma msg = [0x00, 203, 85].pack('CCC') sock.syswrite(msg) resposta = sock.read(2).unpack('n')[0] puts "<= #{resposta}" #potencia msg = [0x01, 7].pack('CC') sock.syswrite(msg) resposta = sock.read(2).unpack('n')[0] puts "<= #{resposta}" #nao reconhecida msg = [0x31].pack('C') sock.syswrite(msg) resposta = sock.read(2).unpack('n')[0].to_s(16) puts "<= #{resposta}" ensure sock.close end 29
Exercício Implementar uma aplicação cliente/servidor para download de arquivos usando o protocolo binário descrito a seguir. Formato das mensagens enviadas pelo cliente: ID (1 byte) argumento (20 bytes) ID: indica o tipo da mensagem. Obrigatório. 00 solicita a lista de arquivos disponíveis. 01 solicita um arquivo para download. O nome do arquivo desejado deve ser informado no campo argumento. argumento: informações complementares. Opcional. 30
Exercício 5 Formato das mensagens enviadas pelo servidor: ID (1 byte) tamanho (2 bytes) payload (variável) ID: indica o tipo da mensagem. Obrigatório. 00 informa a lista de arquivos disponíveis. O campo payload contém os nomes dos arquivos, cada um separado pelo caractere $. 01 retorna um arquivo solicitado para download. O campo payload contém os dados (bytes) do arquivo. FF indica que a mensagem de entrada não foi reconhecida pelo servidor. Sem payload. tamanho: indica o tamanho, em bytes, do campo payload. Devido a este campo, só é possível transmitir arquivos com até 64kbytes. Obrigatório. payload: dados solicitados conforme a mensagem de entrada. Opcional. 31
Exercício 5 Terminologia: Sessão: par de mensagens trocadas entre cliente e servidor. Mensagem de entrada: mensagem enviada pelo cliente. Mensagem de retorno: mensagem enviada pelo servidor em resposta a uma mensagem de entrada. Comportamento: Protocolo base: TCP. Uma conexão TCP deve suportar várias sessões do protocolo de aplicação. Uma sessão sempre é iniciada pelo cliente. O servidor sempre deve responder a uma mensagem de entrada. Dica: consulte os métodos Array.pack e String.unpack e o artigo disponível em http://www.happybearsoftware.com/bytemanipulation-in-ruby.html. 32
Classe UDPSocket Socket para transmissão de dados baseados no protocolo UDP. Um UDPSocket pode ser utilizado como cliente (emissor de dados) ou como servidor (receptor de dados), já que o protocolo UDP não é orientado à conexão. Construtor new() Método bind(host, port) Utilizado em um socket servidor, associa o socket a uma interface de rede e a uma porta. Para qualquer interface de rede utilize uma string vazia. Método gets() Utilizado com um socket servidor, lê o próximo pacote de dados. Método recvfrom(maxlen) => data, [sender_info] Utilizado com um socket servidor, lê até maxlen bytes de dados. Retorna os dados lidos e um array com informações do emissor: protocolo, porta, hostname e endereço IP. 33
Classe UDPSocket Método send(data, flags, host, port) Utilizado em um socket servidor ou cliente, envia um pacote de dados ao host especificado pelos parâmetros host e port. Método connect(host, port) Utilizado em um socket cliente. Os parâmetros indicam endereço e porta do destinatário dos dados. Método puts(data) Utilizado em um socket cliente, envia um pacote de dados ao host especificado na chamada ao método connect. 34
Exemplo Servidor echo require 'socket' socket = UDPSocket.new socket.bind("", 2000) loop{ data, sender = socket.recvfrom(1024) s_ip = sender[3] s_port = sender[1] puts "Dados recebidos do cliente #{s_ip}:#{s_port}: #{data} " socket.send(data.upcase, 0, s_ip, s_port) } socket.close 35
Exemplo Cliente echo require 'socket' socket = UDPSocket.new socket.connect('localhost', 2000) msg = gets socket.puts msg puts socket.gets socket.close 36
Exercício 6 Usando sockets UDP, implemente uma aplicação de calculadora semelhante a do exemplo com sockets TCP. 37
Referências http://www.ruby-doc.org/stdlib- 1.9.3/libdoc/socket/rdoc/index.html http://rubylearning.com/satishtalim/ruby_soc ket_programming.html Jones, M. Tim. Sockets programming in Ruby. IBM Developer Works, 2005. 38