O que são e como funcionam os Sockets

Aprenda como os sockets funcionam em baixo nível e como ele é fundamental para os sistemas distribuídos

Formalmente falando os sockets foram a forma de permitir que dois processos se comuniquem (Inter-process communication). Esses processos podem ou não estar na mesma máquina. Nesse artigo iremos entender como os sockets são implementados dentro do ambiente Unix, como linguagens e aplicações fazem uso dele.

Imagem com uma tomada de parede que representa uma analogia ao sockets

Diversas aplicações que utilizamos no dia-a-dia fazem uso de sockets pra se comunicar. Nosso navegador web utiliza sockets pra requisitar páginas. Quando um sistema se integra com um banco de dados ele abre um socket. Quando fazemos um ssh em um servidor estamos abrindo e utilizando um socket. 


Originalmente a implementação dos sockets foi feita no 4.2BSD em 1983. Essa implementação foi portada para o Linux com poucas modificações. Na forma de uma API, os sockets abstraem a camada de rede para que uma aplicação possa se comunicar com outra sem ter que se preocupar com detalhes da pilha TCP/IP que gere a rede abaixo dessa aplicação.

Pra entender melhor o funcionamento dos sockets vamos começar voltando na estrutura onde ele se encaixa enquanto API. O modelo OSI especifica um padrão de redes para criação de protocolos. Esse modelo divide uma pilha de redes em 7 camadas, cada uma com suas responsabilidades.

A Internet utiliza como base para todas as suas comunicações a pilha TCP/IP que possui apenas 4 camadas. Baseado no modelo OSI, o TCP/IP é, atualmente, o padrão de comunicação em redes. A imagem abaixo apresenta uma comparação do modelo OSI com suas 7 camadas em relação as respectivas 4 camadas do modelo TCP/IP:

Imagem que mostra lado a lado as 7 camadas do modelo OSI comparando com as 4 camadas do modelo TCP/IP

Considerando a Internet e o TCP/IP, os sockets estão entre a camada de transporte e a de aplicações. Estando nesse ponto de intercessão, eles conseguem fazer uma interface entre a aplicação e rede de maneira bem transparente. Assim, aplicações são implementadas através de uma comunicação lógica. Lógica no sentido de que para esses programas, eles estão se comunicando diretamente um com o outro, mas na prática, eles estão passando pela rede para trocar mensagens.


A imagem a seguir mostra essa divisão lógica e física de comunicação dentro da pilha TCP/IP e onde exatamente entre as camadas de transporte e aplicação se encaixa a API de sockets:

Comparação entre a comunicação lógica e física em uma aplicação distribuída utilizando sockets

Até agora vimos que os sockets foram criados na forma de uma API que possibilita aplicações/processos se comunicarem. Quais são essas interfaces que permitem essas comunicações? Abaixo listamos alguns das principais funções utilizadas ao criar um programa utilizando sockets.

/**
 * Principais funções para escrever programas com sockets
 */

getaddrinfo()  // Traduz nomes para endereços sockets
socket()       // Cria um socket e retorna o descritor de arquivo
bind()         // Associa o socket a um endereço socket e uma porta
connect()      // Tenta estabelecer uma conexão com um socket
listen()       // Coloca o socket para aguardar conexões
accept()       // Aceita uma nova conexão e cria um socket
send()         // caso conectado, transmite mensagens ao socket
recv()         // recebe as mensagens através do socket 
close()        // desaloca o descritor de arquivo
shutdown()     // desabilita a comunicação do socket

Quando se programa utilizando sockets, uma arquitetura muito comum para esses programas, é utilizar o Cliente / Servidor (client/server). Para essa arquitetura temos que implementar um programa cliente e um programa servidor. Ambos fazem uso da mesma API de sockets.


Exemplos de programas nessa arquitetura e que utilizam sockets seriam os pares de cliente -> servidor: ssh -> sshd, chromium -> nginx, ftp -> ftpd. A lista de funções mostradas acima são a implementação dos sockets em C. No geral, a maioria das linguagens não rescreve essas funções. Elas fazem uso da biblioteca compilada disponível no sistema operacional. Assim, internamente essas linguagens estão chamando exatamente essa biblioteca em C. 

No sistema operacional Linux por exemplo, essa API faz chamadas de sistema, logo a linguagem precisa explicitamente usar essa API pra conseguir abrir um socket. É até uma questão de segurança! Apenas o núcleo (Kernel) do sistema operacional pode abrir sockets. Ele controla inclusive qual processo (programa em execução) tem direito a ler e escrever em determinado socket aberto.

Usando as mesmas funções vistas acima, um cliente e um servidor trocam mensagens através de seus sockets para tomar decisões de aplicação. Um exemplo do fluxo de trabalho (workflow) utilizando sockets seria:

Imagem com o fluxo de interação do cliente com o servidor utilizando as API de sockets

Cliente

O programa cliente primeiro cria um socket  através da função socket(). Em seguida ele se conecta ao servidor através da função connect() e inicia um loop (laço) que fica fazendo send() (envio) e recv() (recebimento) com as mensagens específicas da aplicação. É no par send, recv que temos a comunicação lógica. Quando alguma mensagem da aplicação diz que é o momento de terminar a conexão, o programa chama a função close() para finalizar o socket.

Servidor

O programa servidor também utiliza a mesma API de sockets. Ou seja, inicialmente ele também cria um socket. No entanto, diferentemente do cliente, o servidor precisa fazer um bind(), que associa o socket a uma porta do sistema operacional, e depois utilizar o listen() para escutar novas conexões de clientes nessa porta.

Quando um novo cliente faz uma nova conexão, a chamada accept() é utilizada para começar a se comunicar. Da mesma forma que no cliente, o servidor fica em um loop (laço) recebendo e enviando mensagens através do par de funções send()  e recv(). Quando a comunicação com o cliente termina, o servidor volta a aguardar novas conexões de clientes.

Imagem ilustrando a arquitetura cliente / servidor onde mostramos o loop de requisições / respostas.

Tipos de sockets

No geral existem dois tipos de sockets: TCP e UDP. Os dois tipo são controlados pela API de sockets de maneira a abstrair detalhes da rede para o desenvolvedor. Esses tipos são exatamente os protocolos mostrados na figura que situamos a API de sockets na pilha TCP/IP. Vimos que ela está imediatamente acima da camada de transporte e abaixo da camada de aplicação. 

Os sockets do tipo TCP são orientados a conexão e tem um canal exclusivo de comunicação entre cliente e servidor. Eles garantem a ordem dos pacotes, são considerados confiáveis e sem perda. No entanto, quando se trata de se recuperar de falhas e perda de pacotes ele é mais burocrático e lento.

Já os sockets do tipo UDP desconsidera ordem de pacotes, recuperação de falhas e garantia de ordem. No entanto, por ser extremamente menos burocrático e simples, ele é mais rápido que o TCP para alguns tipos de aplicações.

Uma aplicação que faz transações bancárias deveria usar o sockets TCP, pois operações financeiras não podem ter dados e valores corrompidos durante sua transmissão via rede. Já um jogo, que envia apenas posições atuais de seus jogadores, pode assumir que pacotes podem se perder e que o mais importante é a posição atual. O mesmo pode ser dito para uma transmissão de vídeo. Se eu perdi um frame (pedaço) do vídeo porque minha rede atrasou a entrega dos pacotes, não é necessário pedir novamente o envio do pacote perdido. É muito mais importante receber o atual e continuar a transmissão. 

Conclusão

Os sockets abstraem as camadas de rede para que programadores possam se preocupar com a comunicação de maneira distribuída de seus processos e aplicações. A implementação dos sockets foi concebida como uma API com interface para o sistema operacional; que é o responsável por controlar e garantir segurança da criação e destruição desses sockets. Como um padrão, os sockets hoje estão presentes em praticamente todos programas que utilizam a rede para se comunicar e permitindo que  novos sistemas distribuídos apareçam.

Posts em português para fortalecer a comunidade brasileira de Ciência da computação. Caso tenha dúvidas, críticas ou sugestões de temas ou para o blog deixe nos comentários :)