Ruby-Yatax: Yet another TODO application eXtreme edition

yatax

Yet another TODO application eXtreme edition

Que isso?

O objetivo principal do Yatax é, ao longo do desenvolvimento da aplicação, que deve ser a princípio uma simples TODO app em Ruby, mostrar tópicos e conceitos elementares da Web.

Tópicos

Cada tópico vai ser um Pull Request, onde terá explicação dos conceitos juntamente com o código feito no PR. Lista de tópicos (PR's):

  1. Comunicação TCP/IP

Comments

  • HTTP headers & Cookies
    HTTP headers & Cookies

    Jan 17, 2022

    Link para o Blog

    Nosso servidor HTTP, até o momento, responde apenas um texto simples "Hello".

    require 'socket'
    
    socket = TCPServer.new(3000)
    puts 'Listening to the port 3000...'
    
    loop do
      client = socket.accept
    
      response =
    """
    HTTP/1.1 200\r\n
    \r\n
    \r\n
    Hello
    """
    
      client.puts(response.strip.gsub(/\n+/, "\n"))
      client.close
    end
    

    Ao acessarmos http://localhost:3000 no navegador, o resultado é este:

    Screenshot 2022-01-17 at 21 34 02

    Entretanto o conteúdo apresentado é um texto simples. Se decidirmos escrever uma página inteira apenas com texto simples, seria irritante para o usuário. Por isto, precisamos formatar o conteúdo, marcar algumas partes com destaque e prover uma experiência melhor ao usuário do site.

    Um pouco de HTML

    HTML é uma linguagem de marcação para conteúdo hypertexto, que pode ter diferentes características de acordo com a marcação desejada.

    Vamos supor que queremos responder com conteúdo HTML um contador de quantas vezes uma mesma pessoa visita a página. Vamos alterar a resposta HTTP para conter o corpo da mensagem em formato HTML:

    # ...
      response =
    """
    HTTP/1.1 200\r\n
    \r\n
    \r\n
    <h1>Counter: 1</h1> 
    """
    # ...
    

    O texto com o counter (<h1>Counter: 1</h1>) deve ser destacado em forma de título na página. Ao entrarmos no site:

    Screenshot 2022-01-17 at 22 29 51

    O conteúdo foi mostrado exatamente da forma como enviamos no socket, pois tudo é string de dados sendo transportada via socket TCP.

    HTTP headers

    Mas como fazer o navegador "interpretar" aquele conteúdo HTTP como sendo HTML? Devemos enviar este "metadado" em uma parte especial da mensagem HTTP, que se chama HTTP header.

    # ...
      response =
    """
    HTTP/1.1 200\r\n
    Content-Type: text/html\r\n
    \r\n
    <h1>Counter: 1</h1> 
    """
    # ...
    

    Desta forma, estamos instruindo o HTTP client, no caso o web browser, que o conteúdo é do tipo HTML, assim o browser consegue renderizar o HTML corretamente:

    Screenshot 2022-01-17 at 22 39 14

    Ok, mas o counter está sempre fixo no valor "1". Como podemos deixar isto dinâmico?

    # ...
    counter = 1       # <-- como fazer o counter ser dinâmico a ponto de ser enviado entre browser e server diversas vezes? 
    
      response =
    """
    HTTP/1.1 200\r\n
    Content-Type: text/html\r\n
    \r\n
    <h1>Counter: #{counter}</h1> 
    """
    # ...
    

    HTTP é stateless por definição

    Vamos lembrar que, por estar condicionado a uma conexão TCP, o HTTP não guarda estado, isto é, a cada vez que um user fizer refresh à página, o server não consegue saber, a princípio, quem é o cliente, sempre tratando o pedido como se fosse um novo cliente.

    # (...)
    loop do
      client = socket.accept                        # <-- aguarda até chegar novo cliente
    
      client.puts("HTTP/1.1  (etc ....)")       # <-- envia resposta ao cliente
      client.close                                          # <-- fecha conexão com cliente e o ciclo de aguardo inicia novamente
    end
    # (...)
    

    Para que o cliente seja "lembrado", é preciso que o server envie algum metadado ao cliente, para que este reencaminhe o metadado de volta ao server nos pedidos subsequentes.

    A especificação HTTP contempla este cenário onde podemos ter algum tipo de "estado" entre conexões HTTP distintas de um mesmo cliente, sendo que web browsers e servidores web já implementam este envio mútuo de metadado de forma automática.

    Já sabemos que para enviar um metadado, é através de HTTP headers, como vimos no exemplo do Content-Type.

    Para o caso do "counter" ser enviado entre vários pedidos, o HTTP especifica o envio deste metadado através de headers que são HTTP Cookies.

    HTTP Cookies

    1. o server envia um metadado qualquer através do header Set-Cookie
    2. o browser recebe a mensagem e verifica que há um header Set-Cookie, então pega o valor do cookie e coloca numa área específica da memória do navegador chamada cookie storage.
    3. para pedidos futuros neste mesmo site, o browser já sabe que tem que enviar o cookie de volta ao servidor, portanto, no request HTTP, inclui um header chamado Cookie com o valor armazenado.
    4. o server verifica se o browser enviou algum header Cookie, e caso tenha sido enviado, lê o valor do cookie para manipular/atualizar a informação de alguma forma e o ciclo se repete

    Como funciona um típico sistema de Login na Web

    Convém lembrar que um sistema de login web funciona exatamente desta forma, onde a primeira resposta do servidor é através de Set-Cookie que contém a identificação do user ou algo do gênero. Desta forma, o browser envia o cookie nos pedidos subsequentes e com isto temos um sistema de login onde temos a impressão que estamos "autenticados".

    "Autenticados" com muitas aspas. É apenas uma sensação, pois a informação é sempre trocada através de headers em cima de um protocolo que NÃO guarda estado por definicão.

    Para enviar um metadado ao cliente, já vimos que é preciso utilizar HTTP headers. Neste caso não será diferente. E a especificação HTTP contempla um header chamado Set-Cookie que permite "persis

    Voltando ao nosso exemplo do counter

    Vamos então ver como fica o response HTTP com o header Set-Cookie enviando o valor do counter:

    # ...
    counter = 1 
    
      response =
    """
    HTTP/1.1 200\r\n
    Content-Type: text/html\r\n
    Set-Cookie: counter=#{counter}; path=/; HttpOnly\r\n
    \r\n
    <h1>Counter: #{counter}</h1> 
    """
    # ...
    

    A resposta HTTP que chega ao browser é esta:

    Screenshot 2022-01-17 at 23 04 35

    Pelo que o browser guarda o cookie no cookie storage do próprio browser, ou seja, fica na memória do browser:

    Screenshot 2022-01-17 at 23 04 51

    Por conta disto, se o user apagar as cookies, o próximo pedido ao server não vai ser o valor no header portanto para o server será como se fosse a "primeira vez" daquele cliente.

    Vamos ver como fica o request HTTP enviado do browser ao servidor:

    Screenshot 2022-01-17 at 23 08 45

    Nesta imagem, podemos ver que há diversos HTTP headers que o browser envia ao server, dentre eles o nosso cookie:

    Cookie: counter=1
    

    Yay!

    Lendo o valor do request HTTP Cookie

    Apesar de que conseguimos enviar do server ao browser, nosso server ainda não é capaz de ler os headers da mensagem, pois ainda não escrevemos este código.

    Para isto, precisamos ler a mensagem através do socket TCP, uma linha de cada vez no socket:

    client = socket.accept
    
    first_line = client.gets  
    second_line = client.gets
    
    # and so on...
    

    Já deu pra entender quantas linhas teríamos que escrever para ler a mensagem toda, pois não? Vamos então fazer um loop e ler todas as linhas enquanto houver mensagem no socket:

    request = ''
    
    while line = client.gets
      break if line == "\r\n"
    
      request +=  line
    end
    

    Só isto não basta, temos que agora conseguir encontrar um padrão Cookie: <qualquerChave>:<qualquerValor> no meio da mensagem. Para encontrar padrões, vamos utilizar expressões regulares:

    cookie = {}
    
    if cookie_match = request.match(/Cookie:\s(.*)=(.*)\r$/)
      cookie[cookie_match[1]] = cookie_match[2]
    end
    

    Vamos pular explicação de expressões regulares por agora, pode ser tema para outra sessão. Mas com este código conseguimos guardar numa hash todos os cookies vindo do request HTTP.

    Próxima linha é buscar o valor do counter na hash cookie, caso esteja ausente (primeiro request de um cliente, por exemplo), o valor é 0. Caso contrário, é o valor encontrado no cookie.

    A este valor, incrementamos o valor 1, dando assim a característica de um counter:

    counter = cookie.fetch('counter', 0).to_i + 1
    

    E pra finalizar, nosso response com os devidos headers:

      response =
    """
    HTTP/1.1 200\r\n
    Content-Type: text/html\r\n
    Set-Cookie: counter=#{counter}; path=/; HttpOnly\r\n
    \r\n
    <h1>Counter: #{counter}</h1>
    """
    

    Ao rodarmos o server e entrarmos 2 vezes na página:

    Screenshot 2022-01-17 at 23 17 01

    Yay! Já temos nosso counter funcionando com HTTP cookies!

    Conclusão

    Esta sessão foi uma explicação de como funcionam HTTP headers, HTTP cookies e como podemos tirar proveito disto para envio mútuo de metadados para que sempre consigamos "lembrar" do cliente, mesmo utilizando um protocolo que não guarda estado.

    Reply
  • Protocolo HTTP
    Protocolo HTTP

    Jan 13, 2022

    O que é HTTP

    Fundamentos de HTTP no Blog

    Reply
  • 001 - Comunicação TCP/IP
    001 - Comunicação TCP/IP

    Jan 10, 2022

    Vamos supor que um determinado computador "A", chamado client, queira enviar uma mensagem a outro computador "B", chamado server. A mensagem que o client envia, também chamada de request, precisa ser transportada através de algum tipo de rede, ou network. O server, por sua vez, pode enviar uma resposta ao client, chamada de response.

    Screenshot 2022-01-09 at 23 12 55

    Rede

    Mas o quê é esta "rede" que fica ali no meio de dois computadores distintos? Podem ser diversos tipos de redes, públicas e privadas, mas quando estamos falando de uma rede global e descentralizada, nos referimos à Internet, onde computadores conectados de lugares distintos no planeta podem se comunicar.

    Screenshot 2022-01-09 at 23 34 19

    Mas do quê a Internet é composta? Quais são as ferramentas e conceitos existentes na Internet que garantem que as mensagens são entregues da forma esperada?

    Vamos dividir estes conceitos e ferramentas em camadas.

    Camada 1: Física

    Para criamos uma rede, precisamos definir a infra estrutura física através de arquitetura de rede, utilizando o cabeamento correto e roteamento de mensagens dentro da rede. Esta camada envolve apenas a parte física e envolve tecnologias como Ethernet, Bluetooth, ondas de rádio, fibra optica, entre outros.

    Camada 2: Rede (IP)

    Como um client poderia enviar uma mensagem a um server? E se enviarmos num formato que muitos de nós já conhecemos, como as correspondências?

    Screenshot 2022-01-09 at 23 42 59

    E se por acaso algum computador na rede, mal-intencionado ou esquecido, resolve enviar uma mensagem com formato errado, difícil de interpretar?

    Screenshot 2022-01-09 at 23 43 05

    Neste caso a mensagem talvez nem conseguiria ser entregue ao destino, pois tem um formato inválido.

    No caso da Internet, esta camada se chama Internet Protocol, ou IP, e define um protocolo para que as mensagens tenham um determinado padrão de envio.

    Camada 3: Transporte (TCP, UDP)

    Como então garantir que as mensagens chegam de um lado para o outro? Serão transportadas de forma correta? Na ordem correta?

    Esta camada define o formato de transmissão das mensagens, onde pode ser definido se há confirmações ou não de envio, tentativas em caso de falhas, dentre outras formas de garantia de envio. Nesta camada, fica um dos protocolos de envio mais conhecidos, o TCP.

    Camada 4: Aplicação

    E se quisermos criar uma abstração por cima do formato da mensagem onde podemos suportar outros formatos e protocolos de mensagens? Como suportar multi-formatos? Envio de "correspondências virtuais" (e-mails) com garantia de entrega?

    Screenshot 2022-01-10 at 00 06 40

    A camada de aplicação é responsável por definir protocolos de formatos de mensagens que serão enviados através de TCP/IP. Dentre estes protocolos, podemos destacar HTTP, SMTP, FTP dentre outros.

    Então aqui temos o resumo das 4 camadas definidas no standard TCP/IP:

    Screenshot 2022-01-10 at 00 14 04

    Computadores e TCP/IP

    Como um computador consegue enviar e receber mensagens na rede? Indo além, como um computador consegue conectar com outro computador?

    Os sistemas operacionais fornecem uma funcionalidade para que possamos utilizar recursos de rede em diversos "pontos". Tais pontos são chamados de "endpoints", ou sockets.

    Screenshot 2022-01-10 at 00 19 32

    Com sockets conseguimos conectar a outro computador, e também enviar e receber mensagens. O sistema operacional também permite que alguns endpoints possam receber um identificador único no computador, também chamado de porta.

    => lsof -i tcp:3000
    
    COMMAND     PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
    com.docke 21027 leandro  104u  IPv6 0xd7f6748eef91a4e7      0t0  TCP localhost:hbci->localhost:53696 (CLOSED)
    com.docke 21027 leandro  106u  IPv6 0xd7f6748ee3098827      0t0  TCP localhost:hbci->localhost:53697 (CLOSED)
    com.docke 21027 leandro  112u  IPv6 0xd7f6748ef2704e87      0t0  TCP localhost:hbci->localhost:53702 (CLOSED)
    com.docke 21027 leandro  124u  IPv6 0xd7f6748ef0efbe87      0t0  TCP *:hbci (LISTEN)
    

    Destaque para a coluna FD, que representa o endpoint, ou socket. E a DEVICE, que representa o dispositivo físico de rede do computador.

    Comunicação TCP/IP em Ruby

    Ruby fornece uma API já incluída no pacote standard para manipulacão de sockets TCP/IP.

    Vamos criar um TCP server muito simples em Ruby, que abre um socket na porta 3000:

    server.rb

    require 'socket'
    
    # Abre o socket e atribui na porta 3000
    socket = TCPServer.new('0.0.0.0', 3000)
    
    # Socket aberto à espera de novas conexões na rede
    # O programa fica bloqueado nesta linha enquanto não houver nova conexão
    client = socket.accept
    
    # Uma vez que uma nova conexão chega, o programa continua
    # E aqui podemos fazer a leitura de mensagens que chegaram no socket
    request = client.gets
    
    # Envia uma mensagem para o client através do socket aberto
    client.puts('Yo, client')
    
    # Fecha a conexão com o client
    client.close
    
    # Encerra o socket e liberta a porta 3000 para ficar livre no sistema operacional
    socket.close
    

    Agora, um client TCP simples em Ruby:

    client.rb

    require 'socket'
    
    # Conecta em algum socket já aberto na porta 3000
    server = TCPSocket.new('0.0.0.0', 3000)
    
    # Envia mensagem ao server através do socket
    server.puts('Yo, server!') 
    
    # Recebe mensagem do server através do docket
    response = server.gets
    
    # Fecha o socket deste lado do client
    server.close
    

    Server em loop

    Obviamente, não podemos ter um servidor que encerra o socket na porta a cada conexão. Para resolver isto, podemos colocar o server em loop, onde a cada vez que uma conexão é fechada, o server volta para o início do loop e fica bloqueado a espera de uma nova conexão.

    server.rb

    ...
    loop do
      client     = server.accept
      request = client.gets
    
      client.puts 'My response'
      client.close
    end
    

    Padrão TCP/IP

    Podemos reparar que o padrão de envio de mensagens através de TCP/IP é:

    1. server abre socket em alguma porta de rede
    2. server fica bloqueado a espera de conexão no socket
    3. server recebe mensagem do client
    4. server envia mensagem para o client
    5. server fecha conexão com client
    6. server fica novamente bloqueado a espera de nova conexão

    O quê acontece quando o mesmo client tenta novamente conectar no servidor? Nosso servidor consegue se "lembrar" quem era o primeiro client?

    Por definição, o server não guarda informações dos clients nesta camada de rede, não há estado, o que caracteriza que o TCP/IP é stateless pode definição. Este conceito será importante para que possamos entender, posteriormente, as particularidades de aplicações Web que rodam através de HTTP.

    Reply