Neste artigo veremos como desenvolver aplicações em Java que podem comunicar-se via rede local ou via internet, usando sockets. Os sockets são compostos por um conjunto de primitivas do sistema operacional e foram originalmente desenvolvidos para o BSD Unix. Podem ser utilizados nos mais variados sistemas operacionais com recursos de comunicação em rede, sendo suportados pela maioria das linguagens de programação. Sockets são suportados em Java desde o JDK 1.0, para sua utilização devemos fazer uso das classes contidas no pacote java.net. Um exemplo interessante da programação de sockets em Java são os drivers JDBC do tipo 4, que usam sockets para comunicar-se diretamente com a API de rede do banco de dados.

Estrutura básica de uma aplicação de rede

Uma aplicação que utiliza sockets normalmente é composta por uma parte servidora e diversos clientes. Um cliente solicita determinado serviço ao servidor, o servidor processa a solicitação e devolve a informação ao cliente (ver Figura 1). Muitos serviços podem ser disponibilizados numa mesma máquina, sendo então diferenciados não só pelo endereço IP, mas também por um número de porta. Porém, o mais comum é termos uma máquina dedicada oferecendo apenas um ou dois serviços, evitando assim a concorrência.

Fluxo de troca de dados com sockets

Figura 1 Fluxo de troca de dados com sockets

Como primeiros passos na criação do servidor, é necessário importar o pacote java.net e em seguida instanciar um objeto do tipo ServerSocket, responsável por atender pedidos via rede e em determinada porta. Após receber uma conexão, um objeto do tipo Socket deve ser criado para manter a comunicação entre o cliente e o servidor.

Vejamos um exemplo. A seguinte linha cria o ServerSocket, que irá esperar conexões na porta 12345 (caso esta porta já esteja em uso, uma exceção será lançada):

ServerSocket server = new ServerSocket(12345);

Em seguida criamos um objeto Socket, o qual irá tratar da comunicação com o cliente, assim que um pedido de conexão chegar ao servidor e a conexão for aceita:

Socket client = server.accept();

Como vimos no exemplo, um socket servidor precisa definir o número da porta para receber conexões dos clientes. Este número pode variar entre 0 e 65535, porém, em nossas aplicações só devemos utilizar de 1024 em diante, pois as portas com números abaixo deste são reservados para o uso do sistema (por exemplo a porta 80 é usada pelo protocolo HTTP, 25 pelo SMTP, 110 pelo POP3, entre vários outros serviços).

Endereços IP

Cada máquina conectada a uma rede possui um endereço IP único de maneira que possa ser identificada na rede. A classe InetAdress nos permite obter informações sobre um computador conectado a rede. Os principais métodos desta classe são os seguintes:

getAddress(): Este método retorna um array de bytes contendo o endereço IP. Para isso, o nome do host que se deseja obter o endereço IP é fornecido ao método getByName da classe  InetAddress, veja um exemplo:

byte[] b = InetAddress.getByName("localhost").getAddress();
System.out.println(b[0] + "." + b[1] + "." + b[2] + "." + b[3]);

getHostAddress(): Este método retorna uma String contendo o endereço IP no formato 999.999.999.999, veja um exemplo:

System.out.println("Endereço: " + InetAddress.getByName("localhost").getHostAddress() );

getHostName(): Dado um array de bytes contendo o endereço IP de um host, este método retorna uma String com o nome do host, veja um exemplo:

byte[] addr = {127,0,0,1};
System.out.println(InetAddress.getByAddress(addr).getHostName());

O protocolo TCP

Quando necessitamos de uma troca confiável de informações, isto é, quando é necessária a confirmação de recebimento da mensagem enviada, devemos utilizar o protocolo TCP (Transmission Control Protocol). Este protocolo estabelece uma conexão entre dois pontos interligados. Por exemplo, uma mensagem enviada de um host (o termo host representa uma máquina conectada na rede) a outro é confirmada pelo host receptor indicando o correto recebimento da mensagem. Uma mensagem pode ser enviada em vários pacotes, o TCP cuida para que os pacotes recebidos sejam remontados no host de destino na ordem correta (caso algum pacote não tenha sido recebido, o TCP requisita novamente este pacote). Somente após a montagem de todos os pacotes é que as informações ficam disponíveis para nossas aplicações. A programação do TCP com sockets utiliza streams, o que simplifica muito o processe de leitura e envio de dados pela rede.

Streams são objetos Java que permitem obter dados de qualquer fonte de entrada, seja o teclado, um arquivo ou até mesmo um fluxo de bytes recebidos pela rede (o que é o nosso caso). Isto torna a manipulação de dados da rede como se fossem arquivos, ao ler dados enviados é como se estivéssemos lendo um arquivo e ao enviar dados é como se estivéssemos gravando dados em um arquivo.

Um primeiro servidor TCP

Vamos começar agora a trabalhar na prática com sockets. Primeiro vamos montar um servidor TCP que permite a seus clientes solicitarem a data e a hora atuais do servidor. A primeira versão deste servidor tem uma limitação (que mostraremos mais tarde como resolver): apenas um cliente pode ser atendido por vez.

Uma das características importantes do TCP é que os pedidos de conexões dos clientes vão sendo mantidos em uma fila pelo sistema operacional até que o servidor possa atendê-los. Isto evita que o cliente receba uma negação ao seu pedido, pois o servidor pode estar ocupado com outro processo e não conseguir atender o cliente naquele momento.

Cada sistema operacional pode manter em espera um número limitado de conexões até que sejam atendidas. Quando o sistema operacional recebe mais conexões que esse limite, as conexões mais antigas vão sendo descartadas.

Veja como funciona o nosso primeiro exemplo:

  • Ao ser iniciado o servidor fica ouvindo na porta 12345 a espera de conexões de clientes;
  • O cliente solicita uma conexão ao servidor;
  • O servidor exibe uma mensagem na tela com o endereço IP do cliente conectado;
  • O servidor aceita a conexão e envia um objeto Date ao cliente;
  • O cliente recebe o objeto do servidor e faz o cast necessário, em seguida exibe na tela as informações de data;
  • O servidor encerra a conexão.

Na Listagem 1 é apresentado o código do nosso primeiro exemplo de servidor e na Listagem 2 é apresentado o código do cliente que utiliza o nosso servidor.

Listagem 1: Código do servidor TCP básico

public class ServidorTCPBasico {
  public static void main(String[] args) {
    try {
      // Instancia o ServerSocket ouvindo a porta 12345
      ServerSocket servidor = new ServerSocket(12345);
      System.out.println("Servidor ouvindo a porta 12345");
      while(true) {
        // o método accept() bloqueia a execução até que
        // o servidor receba um pedido de conexão
        Socket cliente = servidor.accept();
        System.out.println("Cliente conectado: " + cliente.getInetAddress().getHostAddress());
        ObjectOutputStream saida = new ObjectOutputStream(cliente.getOutputStream());
        saida.flush();
        saida.writeObject(new Date());
        saida.close();
        cliente.close();
      }  
    }   
    catch(Exception e) {
       System.out.println("Erro: " + e.getMessage());
    }
    finally {...}  
  }     
}

Listagem 2: Código do cliente TCP básico

public class ClienteTCPBasico {
  public static void main(String[] args) {
    try {
      Socket cliente = new Socket("paulo",12345);
      ObjectInputStream entrada = new ObjectInputStream(cliente.getInputStream());
      Date data_atual = (Date)entrada.readObject();
      JOptionPane.showMessageDialog(null,"Data recebida do servidor:" + data_atual.toString());
      entrada.close();
      System.out.println("Conexão encerrada");
    }
    catch(Exception e) {
      System.out.println("Erro: " + e.getMessage());
    }
  }
}

Na Figura 2 podemos ver as mensagens exibidas pelo servidor ao receber conexões e na Figura 3 é apresentada a data atual do servidor recebida pelo cliente.

Mensagens exibidas pelo servidor ao receber conexões de clientes

Figura 2: Mensagens exibidas pelo servidor ao receber conexões de clientes

Data e hora recebidas pelo cliente

Figura 3: Data e hora recebidas pelo cliente

O protocolo UDP

Quando necessitamos de uma troca não confiável de informações podemos usar o protocolo UDP (User Datagram Protocol), pois este protocolo não garante a entrega dos pacotes (o UDP não espera uma mensagem de confirmação do host de destino). É de responsabilidade da aplicação receptora a remontagem dos pacotes na ordem correta e a solicitação de reenvio de pacotes que não foram recebidos. O UDP utiliza datagram sockets para a troca de mensagens. As principais aplicações do UDP são aplicações como transmissões de vídeo, skype, voip, etc... Para exemplificar imagine um serviço de voz sobre IP onde um pacote é perdido enquanto dois usuários conversam, não faz sentido reenviar o pacote pois o usuário da outra ponta precisaria saber que ainda faltam pacotes a receber. Veja uma simulação abaixo da conversa entre dois usuários:

  • Erika: Olá, Paulo.
  • Paulo: Olá, Erika.
  • Erika: Como você está?
  • Paulo: Tudo bem e vc? (este pacote foi perdido)

No exemplo acima o TCP enviaria novamente o pacote e o usuário da outra ponta deveria ficar esperando. Não faria nenhum sentido isso tratando-se de uma ligação telefônica. Este é um típico caso onde o UDP se aplica perfeitamente.

Os datagram sockets são mensagens que podem ser enviadas pela rede quando não existe a necessidade de confirmação de entrega, de tempo de entrega e nem mesmo garantia de conteúdo. Datagramas são úteis em aplicações que não necessitam do estabelecimento de uma conexão para o envio da mensagem. Um bom exemplo do seu uso é o envio de mensagens em broadcast para clientes de uma rede (o servidor pode enviar um datagrama para todos os clientes avisando que irá reiniciar, por exemplo). Em Java podemos trabalhar com datagramas utilizando as classes DatagramPacket e DatagramSocket do pacote java.net.