Introdução às Redes de Computadores/Programação com sockets

Inicialmente precisa conceituar o que é socket. A comunicação entre processos de software tornou-se indispensável nos sistemas atuais.

O elo entre os processos do servidor e do cliente é o socket. Ele é a “porta” na qual os processos enviam e recebem mensagens. De acordo com JAMES F KUROSE: “socket é a interface entre a camada de aplicação e a de transporte dentro de uma máquina”.

Então foram desenvolvidas diversas aplicações cliente/servidor onde cliente(s) e servidor poderiam estar em máquinas diferentes, distantes umas das outras. Os aplicativos do cliente e do servidor utilizam protocolos de transporte para se comunicarem. Quando um aplicativo interage com o software de protocolo, ele deve especificar detalhes, como por exemplo se é um servidor ou um cliente. Além disso, os aplicativos que se comunicam devem especificar detalhes adicionais (por exemplo, o remetente deve especificar os dados a serem enviados, e o receptor deve especificar onde os dados recebidos devem ser colocados).

Analisando o esquema acima percebemos que tudo acima da interface do socket, na camada de aplicação, é controlado pelo criador da aplicação. O controle da camada de transporte é feito pelo Sistema Operacional.

Temos dois tipos de serviços de transporte via socket: o confiável orientado a cadeia de bytes (byte steam) e os datagramas não confiáveis. O protocolo na qual é implementado o primeiro é o TCP, já o segundo é implementado no protocolo UDP.


Histórico editar

Na década de 1980 nos Estados Unidos a ARPA (Advanced Research Projects Agency of the Department of Defense) deu à Berkeley, Universidade da California a responsabilidade de construir um sistema operacional que pudesse ser utilizado no suporte à ARPAnet, que é o antecessor da internet atual.

Neste sentido, foi desenvolvida, com uma fonte genuina uma interface e adicionada ao sistema operacional, Unix BSD (Berkeley Software Distribution). Tal interface tinha justamente a função de suporte a comunicação em rede. Esta interface ficou então conhecida como Berkeley Sockets Interface, e é a base para a maioria das interfaces entre protocolos de internet TCP/IP existente.

Existe uma nomenclatura específica para se referir a cada “lado” da comunicação. Temos o servidor, que fica esperando por conexões de entrada e que fornece certos tipos de serviços à outra parte. Já o Cliente vem a ser quem solicita a conexão ao servidor para fazer alguma requisição, algum pedido. É importante dizer que não é o computador que distingue quem é servidor e quem é cliente, mas sim a forma como certo programa usa os sockets. Às vezes também se faz confusão no fato de se pensar que um servidor precisa ser um mainframe. Desktops como os que usamos em casa funcionam tanto como cliente quanto como servidor, e é o que ocorre freqüentemente.

Cada socket tem um endereço único na internet. Este endereço é formado por um número IP e por um número de porta. Devido às grandes dimensões da internet, não há como uma pessoa, ou mesmo uma máquina, saber o endereço de todas as outras. Para resolver este problema foi criado protocolo DNS (Domain Name Service). Este protocolo tem a função de traduzir os nomes ou endereços de alto nível das máquinas para o seu respectivo número IP. Assim, ao se passar o endereço de um socket de um servidor, não se passa diretamente seu número IP, mas sim um nome mais fácil de recordar e então o DNS traduz para o endereço real, ou endereço IP.

Os sockets podem ser usados para comunicação via qualquer um dos protocolos UDP ou TCP. Assim, é possível termos tanto comunicação orientada a conexão (via TCP), quanta não orientada a conexão (via UDP). O socket abstrai esse conceito, permitindo assim a utilização de qualquer um dos meios.

Programação de aplicações TCP editar

Inicialmente o cliente deve contactar o servidor. Para isso, o processo servidor já deve estar executando o programa antes de ser contactado além de já ter criado o socket (porta) que aceita o contato do cliente. O cliente contacta o servidor criando um socket TCP local e especifica o endereço IP e o número da porta do processo servidor. Quando o servidor é contactado o servidor cria um novo socket para se comunicar com o cliente, permitindo assim a liberação do socket de “boas-vindas” para que possa ser contactado por outros clientes.

Abaixo temos uma ilustracão:

Ficheiro:Socket tcp.JPG

Do ponto de vista da aplicação, a conexão TCP é um fluxo cotínuo de dados, a mensagem é fragmentada em pacotes, não há duplicação, ele garante a entrega e a ordem dos pacotes. A conexão é ponto-a-ponto: um remetente e um destinátario conectado por sockets.

Abaixo é ilustrado uma aplicação cliente-servidor em Java:

Ficheiro:Cliente servidor tcp java.JPG

Este texto não tem como objetivo o aprendizado da linguagem Java, portanto só serão demonstradas e explicadas as linhas de códigos referentes às instruções que são utilizadas no programa exemplificado pela ilustração acima.

Começando com a aplicação que será hospedada no Servidor.

No início do programa, deverá ser inserida essas linhas que importam a biblioteca que contém as classes que são utilizadas em uma aplicação com Socket(java.net.*) e as classes de recepção de informação do teclado ou Socket do cliente ou servidor(java.io.*).

   import java.net.*;

Aqui criamos um objeto welcomeSocket é o socket do lado do servidor, numero_da_porta deverá ser substituido pelo número da porta pela qual a aplicação cliente usará para conectar com o servidor. Este socket esperará a requisição de conexão de um cliente.

   ServerSocket welcomeSocket = new ServerSocket(numero_da_porta);

Como o programa servidor normalmente fica funcionando por tempo indefinido, coloca-se o restante das instruções dentro de um loop infinito, como segue:


   while(true) {
        ...
   }

A próxima instrução cria um objeto connectionSocket do tipo Socket quando um cliente conectar ao servidor. O TCP encarregará de criar uma conexão virtual direta entre esse socket e o socket do cliente de forma que todos os bytes serão enviados ao servidor na ordem certa.

   Socket connectionSocket = welcomeSocket.accept();

No caso do envio de um objeto do tipo String do cliente para o servidor, utilizamos as seguintes instruções para receber os dados do cliente.


   BufferedReader infoDoCliente = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream());
   String mensagemDoCliente = infoDoCliente.readLine();

Depois de processar a informação enviada pelo cliente, queremos enviar um outro objeto(mensagem_para_cliente) do tipo String de volta para o cliente, como um mensagem de dado recebido ou algo do tipo.

   DataOutputStream infoParaCliente = new DataOutputStream(connectionSocket.getOutputStream());
   infoParaCliente.writeBytes(mensagem_para_cliente);

Com isso termina-se o programa do lado do Servidor. Agora analisaremos o programa cliente.

Como no caso do programa servidor, o código inicia com a importação das bibliotecas que contém as classes de sockets e de envio de informações.

   import java.io.*;
   import java.net.*;

Primeiramente, criamos o socket que conectará com o servidor. O primeiro parâmetro passado ao construtor é o nome do servidor, por exemplo, 127.0.0.1 se a aplicação servidor estiver rodando no mesmo computador que a aplicação cliente. O segundo parâmetro é o número da porta que é informado ao socket servidor.

   Socket clientSocket = new Socket("nome_do_servidor", numero_da_porta);

Após a criação do socket cliente, temos que criar os objetos de cadeia que serão ligados ao socket. O objeto infoParaServidor será a cadeia que enviará informações para o servidor e o objeto infoDoServidor será a cadeia que receberá informações do servidor.

   DataOutputStream infoParaServidor = new DataOutputStream(clientSocket.getOutputStream());
   BufferedReader infoDoServidor = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Utilizaremos o mesmo caso do servidor, e enviaremos(mensagem_do_cliente) e receberemos(mensagem_para_cliente) dois objetos do tipo String para exemplificar.

   infoParaServidor.writeBytes(mensagem_do_cliente);
   String mensagem_para_cliente = infoDoServidor.readLine();

Agora já podemos fechar o socket, e também a conexão TCP entre cliente e servidor.

   clientSocket.close();

Sockets UDP são mais rápidos que sockets TCP; São mais simples, porém menos confiável; Em UDP não é necessário abrir conexão, deste modo a comunicação ocorre apenas com o envio da mensagem; Um mensagem é um datagrama, que é composto de um remetente (sender) e um receptor (receiver) e a mensagem (content).

Programação de sockets com UDP editar

A comunicação entre processos também é possível por UDP. Entretanto, UDP é um serviço sem conexão. Com isso, não há o three-way handshaking (ou apresentação de três vias)inicial, e assim não há um canal pré-estabelecido entre os processos. Para que a comunicação entre os processos seja possível, deve-se incorporar ao conjunto de bytes enviados tanto o endereço IP do destino quanto a porta do processo de destino. Este conjunto (bytes + endereço IP + porta) recebe o nome de pacote.

Com o pacote criado, ele é colocado na rede através do socket. O processo receptor deverá abrir o pacote para retirar as informações pertinentes. Sendo o UDP um serviço sem conexão, não há garantias de que o pacote realmente chegará ao seu destino.

Na comunicação por TCP, não é necessário que todas as cadeias de bytes recebam endereço IP e número de porta, já que existe uma tubulação virtual, pela qual as cadeias fluem, que possui estas informações adicionais. Já na comunicação por UDP, não havendo esta tubulação virtual, torna-se necessário que as cadeias de bytes sejam organizados em pacotes, todos com endereço IP e número de porta.

Abaixo segue uma exemplo de programação em socket com UDP. Assim como dito anteriormente, o objetivo aqui não é ensinar java, mas sim exemplificar programação com sockets. Comecemos pela parte do servidor.

   DatagramSocket serverSocket = new DatagramSocket(9876);

Diferentemente do TCP, aqui criamos um objeto do tipo DatagramSocket e não ServerSocket. Os dados que serão enviados e recebidos pelo servidor passarão todos através deste socket do tipo DatagramSocket. Não se faz necessário a criação de um objeto DatagramSocket para cada nova conexão. Todas as conexões dos clientes passarão por esse único socket serverSocket. Isto não é um problema porque cada vez que recebe um pacote, o servidor responde e fica livre novamente, pois inexiste conexão entre ambos. O socket escuta na porta 9876.

   while (true) { ... }

Loop infinito, onde o servidor fica esperando o recebimento de pacotes.

   DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
   serverSocket.receive(receivePacket);

O objeto receivePacket aloca memória onde serão salvos os dados de entrada, recebidos pelo servidor. O método receive fica esperando o recebimento e armazena o pacote em receivePacket.

   String sentence = new String(receivePacket.getData());
   InetAddress IPAdress = receivePacket.getAddress();
   int port = receivePacket.getPort();

Estas linhas desmontam o pacote, retirando informações necessárias à retransmissão. O objeto sentence recebe os dados. IPAdress é o número de IP do cliente, que é necessário para mandar a resposta assim como o número de porta port. Todos estes dados foram enviados pelo cliente. Após o processamento a resposta é enviada para o respectivo endereço IP e número de porta.

Do lado do cliente temos a criação do socket pelo qual será enviado um pacote da seguinte forma:

   DatagramSocket clientSocket = new DatagramSocket();

Note que a única diferença entre o socket cliente e o servidor é de que para o servidor foi necessário especificar um número de porta, e para o cliente não. Este comando não cria conexão alguma e nem contata o servidor, e é justamente por isso que não se faz necessário especificar nome do servidor nem seu numero de porta.

   InetAddress IPAddress = InetAddress.getByName(“hostname”);

Aqui criamos um objeto para armazenar o número de IP do Server. Hostname é o nome (apelido) do servidor. O método getByName() faz uma consulta ao servidor DNS para saber qual o número IP do server.

   byte[] sendData new byte[1024];
   byte[] receiveData = new byte[1024];

Estes dois vetores armazenarão os dados enviados e recebidos respectivamente.

   DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAdress, 9876);

Esta linha cria o pacote que será enviado ao Server. Nele estão gravados os dados em si, sendData, o tamanho dos dados, sendData.length, o numero de IP do Server, IPAdress, e o numero da porta onde o Server espera, 9876.

   clientSocket.send(sendPacket);

Depois de pronto, enviamos o pacote através do método send do socket clientSocket. Enviado o pacote o cliente fica esperando uma resposta.

   clientSocket.receive(receivePacket);

Quando ocorre o recebimento do pacote, ele é armazenado em receivePacket.