Conhecendo mais sobre sockets - Parte 1

por Sérgio Henrique Miranda Júnior em 11 Oct 2014

Você sabia que um socket é aberto toda vez que você visita uma página web? Socket é uma forma de realizar comunicação entre processos ou entre um client (seu browser) e um server (o site acessado) através de uma rede, ou seja, torna possível a comunicação entre computadores. Isso tornou possível a existência de programas de mensagens instantâneas como: Google Talk, Messenger, ICQ, IRC, entre outros. Este post trará uma explicação mais detalhada do que é um socket e como trocar dados entre eles, assim você entenderá mais profundamente o que acontece quando dois computadores se comunicam.

Um pouco de história

A API para trabalhar com socket, desenvolvida na universidade de Berkeley, apareceu na versão 4.2 do sistema operacional BSD. Foi a primeira implementação do protocolo Transport Control Protocol (TCP) e continua a mesma API desde 1983. Uma das principais razões para essa API ser utilizada até os dias de hoje é que: você pode utilizar um socket sem saber os detalhes do protocolo que está sendo utilizado por trás. A API desenvolvida em Berkeley opera um nível acima do protocolo, ela se concentra em tornar simples a conexão entre dois endpoints (ou seja, um client e um server) e a troca de dados entre eles. A implementação da API de Berkeley é feita em C, mas a maioria das linguagens modernas incluem códigos que vinculam as interfaces escritas em C.

Criando seu primeiro socket

As classes que trabalham com socket em Ruby não são carregadas por padrão, é preciso realizar um require 'socket' para que elas fiquem disponíveis para utilização. A biblioteca para trabalhar com socket é parte da biblioteca padrão (standard library) do Ruby. Vamos criar nosso primeiro socket:

    
      require 'socket'
      socket  = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
    
  

Isso criará um socket do tipo stream no domínio INET. Basicamente, INET é um atalho para Internet e se refere a um socket da família IPv4 (Internet Protocol Version 4), já stream significa que você realizará a comunicação utilizando um fluxo contínuo de dados, funcionalidade oferecida pelo protocolo TCP. Basicamente, um stream de comunicação é um fluxo onde os dados são transmitidos sem começo e sem fim, mas a ordem de envio é preservada. Um exemplo para deixar mais claro:

    
      #Esse código enviará as 3 letras através de um client socket
      dados = ['a', 'b', 'c']
      dados.each do | letra |
        # imagine que essa função é uma abstração de código que realiza
        # a escrita de dados em uma conexão utilizando a API de um Socket
        escreva_na_conexao(letra)
      end
    
  
    
      # Esse código recebe, através de um server socket, as 3 letras
      # em apenas uma operação de leitura
      # Imagine que a função 'ler_da_conexao' é uma abstração de código que realiza
      # a leitura de dados de uma conexão utilizando a API de um Socket
      resultado = ler_da_conexao
      => ['a', 'b', 'c']
    
  

Estabelecendo conexão

Para uma conexão acontecer é preciso um client e um server. Existem dois tipos de sockets: connection sockets e listening sockets. Sockets do tipo connection são os que inicializam a conexão, ou seja, são os clients. Já os sockets do tipo listening são os que recebem a conexão, ou seja, são os servers. O client precisa utilizar apenas o socket do tipo connections sockets, já o server precisa utilizar ambos os tipos de sockets, um para aceitar conexão e outro para trocar dados com o cliente. Para a comunicação entre os sockets acontecer é preciso utilizar uma rede. Os computadores utilizam endereços IPs para se identificarem em uma rede. O protocolo de transporte que iremos utilizar, para trafegar dados de um computador até o outro, é o TCP. Para entendê-lo de forma simples basta imaginar vários tubos conectando duas pontas, os dados serão passados entre os tubos. Trazendo essa explicação para o mundo real a coisa fica um pouco mais complicada, uma vez que os dados são agrupados em pacotes TCP/IP e podem visitar vários roteadores e hosts até chegar ao seu destino final. Mas isso é assunto para outro post, vamos escrever um client e um server para entender como a API funciona.

    
      #Socket do tipo servidor:
      require 'socket'

      server = Socket.new(:INET, :STREAM)
      addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
      server.bind(addr)
      server.listen(Socket::SOMAXCONN)

      # aceitando conexao
      loop do
        connection, _ = server.accept
        puts connection.read
        connection.close
      end
    
   
     
      # Socket do tipo cliente:
      require 'socket'

      cliente = Socket.new(:INET, :STREAM)
      remote_addr = Socket.pack_sockaddr_id(4481, 'localhost')
      cliente.connect(remote_addr)
      cliente.write 'teste'
      cliente.close
    
  

Nesse exemplo fica bem claro as fases que cada tipo de socket realiza. O socket do tipo server se vincula a uma porta através da chamada ao método bind. É nessa porta que o server receberá as conexões. Em seguida é realizada a chamada ao método listen para que o socket começe a aceitar possíveis novas conexões que podem vir a ser feitas. Finalmente, a chamada ao método accept faz com que uma conexão seja, de fato, aceita pelo socket. Essa chamada faz com que o programa fique bloqueado até alguma conexão ser estabelecida. Após a comunicação ter sido finalizada é preciso fechar a conexão através da chamada ao método close. Já o socket do tipo client precisa apenas se conectar a um endereço. Isso é feito executando o método connect. O mesmo passo de fechar a conexão é feito pelo cliente também. Resumidamente, o server realiza os seguintes passos: bind, listen, accept e close; já o client executa os passos: connect e close.

Fazendo um teste com o protocolo HTTP

Agora que vocês sabem como criar um socket fica fácil realizar testes com o protocolo HTTP. Para fazer um request a um servidor que entende o protocolo HTTP precisamos passar apenas o método que vamos utilizar (e.g., GET), o path do recurso que queremos acessar (e.g., www.google.com), qual versão do protocolo http vamos utilizar e os headers que desejarmos (e.g., Host: www.seuhost.com). Isso é o mínimo necessário para uma requisição ser válida. Exemplo:

      
        require 'socket'

        #criando o socket
        cliente = TCPSocket.new('www.google.com', 80)

        #fazendo a requisição http
        cliente.write 'GET / HTTP/1.1 \r\n'
        cliente.write 'Host: www.seuhost.com \r\n'
        cliente.write '\r\n'
        cliente.close_write

        #lendo a resposta
        cliente.read