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