Artigo Sockets
-
Upload
ezequiel-oliveira -
Category
Documents
-
view
74 -
download
1
Transcript of Artigo Sockets
Programação de Sockets em Java
Atualmente, a programação em redes de computadores tem se tornado uma regra, deixando
de ser apenas uma tecnologia presente em poucos sistemas, mas que vem ganhando espaço
nos mais diversos tipos de aplicações. A principal vantagem nesse modelo de programação é a
capacidade, física e lógica, de distribuir operações computacionalmente pesadas e grandes
quantidades de dados e informações entre diversas máquinas que, trabalhando em conjunto,
ocasionam a descentralização de tarefas.
O modelo de programação distribuída, realizada através da separação de aplicações entre
servidores (aplicações que fornecem serviços a serem consumidos) e clientes (aplicações que
consomem serviços disponibilizados por outras máquinas), foi a principal arquitetura de
distribuição nos anos de 1990. Por meio desta arquitetura, houve aumento na confiabilidade e
redução de custos.
A confiabilidade tornou-se um fator importante nesta arquitetura, pois, mesmo havendo falha
durante um processo em uma determinada máquina, o sistema não se inviabilizava como um
todo, e com a utilização de máquinas mais simples executando serviços isoladamente, houve
uma grande diminuição nos custos para execução de operações por máquinas mais poderosas.
Os aplicativos de servidor e cliente são programas que, executados em diferentes máquinas,
trocam informações por meio de redes de computadores. A aplicação cliente é capaz de
realizar requisições de serviços que são providos pela aplicação servidora. Para isso, o
consumidor (ou cliente) deve conhecer o fornecedor dos serviços (ou servidor) a partir de um
endereço além de ter conhecimento sobre o protocolo pré-estabelecido utilizado para realizar
a solicitação.
No entanto, pelo fato de haver a necessidade de trocas de mensagens entre as máquinas
envolvidas no processo, há um custo e tempo adicional para efetivar essa troca.
Outra característica importante é que os dispositivos envolvidos na troca de mensagens
devem utilizar a mesma linguagem ou protocolo. Em uma rede de computadores há diversos
protocolos, organizando regras sobre o modo como são realizadas as comunicações entre as
partes envolvidas em uma comunicação. Um dos principais conjuntos de protocolos na
programação de redes de computadores é estabelecido pela arquitetura TCP/IP, operando por
meio de um software oferecido pelo sistema operacional a partir de uma máquina ligada na
rede.
A tecnologia Java suporta a troca de bytes entre um cliente e um servidor TCP através do
estabelecimento de uma conexão entre eles.
Pode-se entender uma conexão como uma ligação direta realizada entre dois processos, na
qual dados podem fluir nos dois sentidos.
Para estabelecer uma conexão TCP, as extremidades das máquinas envolvidas nos processos
de servidor e cliente devem ser identificadas. As extremidades dessa conexão são
denominadas de sockets ou soquetes, identificados por um endereço de rede e um número de
porta.
Neste artigo, você irá aprender os conceitos de computação distribuída por meio de sockets
utilizando a tecnologia Java, bem como aplicar todo o conhecimento adquirido, facilitando
assim o desenvolvimento e comunicação de aplicações cliente-servidor.
O que são sockets?
Socket ou soquete é apenas um conceito ou uma abstração. O termo socket é utilizado para
representar um ponto de conexão para uma rede de computadores que utiliza o protocolo
TCP/IP. Quando dois computadores necessitam manter uma comunicação, cada um deles
utiliza um socket.
Um computador, denominado server ou servidor, disponibiliza um socket e aguarda o
recebimento de uma solicitação de conexão, enquanto outro computador, denominado client
ou cliente, executa um socket para se comunicar à máquina servidora, conforme demonstrado
na Figura 1. Para estabelecer uma nova conexão, é necessário apenas um endereço de destino
e um número de porta.
Caso não ocorra nenhum problema, o servidor aceita a conexão gerando um novo socket em
uma porta qualquer do seu lado, criando assim um canal de comunicação entre o cliente e o
servidor. A Figura 2 demonstra esse canal de comunicação.
Cada computador em uma rede TCP/IP possui um endereço exclusivo. As portas representam
conexões individuais dentro desse endereço, porém os dados transmitidos passam por um
processo de roteamento dentro de cada computador a partir de um número da porta. Quando
um socket é criado, ele necessita estar associado a uma determinada porta e deve haver
apenas um socket associado a essa porta.
A principal característica de uma máquina servidora é a de receber e manipular conexões a
partir de solicitações dos clientes, portanto, seu comportamento típico é o de permanecer em
loop aguardando novas conexões e criando sockets com a finalidade de atender as requisições
dos clientes, conforme demonstrado na Figura 3.
Figura 1. Socket de servidor aguardando conexões.
Figura 2. Canal de comunicação criado após conexão de cliente ser recebida.
Figura 3. Aplicação cliente-servidor baseada em conexões.
Sockets em Java
Assim como todas as outras funcionalidades fornecidas pela tecnologia Java, existe um
pacote específico que contém os mecanismos necessários para se trabalhar com sockets: o
pacote java.net.
O pacote java.net contém todas as classes necessárias para criar aplicações de rede. As
classes ServerSocket e Socket também fazem parte desse pacote e são utilizadas para
aplicações que fazem uso do protocolo TCP. Além dessas classes, também existem outras para
conexão com servidores web, criação de secure sockets ou sockets seguros, entre outras
funcionalidades.
Para mais informações, veja a seção “Modos de transmissão de sockets”.
A classe ServerSocket é responsável pela manipulação de sockets do lado do servidor. Por
meio dela, é possível disponibilizar um socket para receber conexões e manipular um novo
socket quando uma requisição de conexão é realizada. Uma vez que uma requisição de
conexão é realizada, o socket de servidor criará um novo socket para que seja possível manter
uma conexão direta com o cliente que requisitou o canal de comunicação.
A classe Socket é responsável pela manipulação de sockets do lado do cliente.
Utilizando-se um socket desse tipo, é possível realizar a solicitação de conexão com um socket
fornecido por um servidor, desde que este esteja aguardando uma requisição por meio de
uma porta previamente disponibilizada.
O estabelecimento da conexão entre duas aplicações dá-se por meio da instanciação de um
objeto da classe Socket, a partir do processo cliente. Uma vez que a conexão entre o cliente e
servidor tenha sido estabelecida pela criação dos correspondentes sockets, os dados da
aplicação podem fluir através de streams a ela associados.
Quando há a necessidade de utilização do protocolo UDP, deve-se utilizar as classes
DatagramSocket e DatagramPacket também presentes no pacote java.net.
A classe DatagramSocket é responsável pelo envio e recebimento de datagramas. Utiliza-se
esse tipo de socket tanto para aplicações servidoras quanto para clientes. Como não há
controle da sequência de envio e recebimento de datagramas, múltiplos pacotes enviados de
uma máquina a outra podem ser roteados de forma diferente e até chegar em ordens
diferentes, além de que a entrega de pacotes não é garantida.
A classe DatagramPacket é responsável pela inserção de dados na forma de bytes em um
pacote UDP denominado datagrama.
Os pacotes de datagramas são utilizados para implementar um serviço de entrega de pacotes
sem conexão. Cada mensagem é roteada da máquina remetente até a destinatária baseando-
se na informação contida dentro do datagrama.
Nota Devman 1: Datagrams ou Datagramas: Um datagrama é, em uma rede de computadores, uma estrutura unitária utilizada para transmitir uma
sequência de dados em um canal de comunicação que utilize a comutação de pacotes. Uma informação a ser transmitida geralmente é quebrada em
inúmeros pacotes, facilitando e agilizando seu envio pela rede. Juntamente com a informação, o datagrama possui um cabeçalho, contendo
informações que auxiliam sua transmissão, como os endereços do destinatário e remetente, soma para checagem de erros (checksum), definição
de prioridades, entre outros dados.
Modos de transmissão de sockets
A tecnologia de sockets permite o desenvolvimento de aplicações distribuídas a partir de dois
modos principais de operação: o modo baseado em conexões e o modo sem conexão.
Analogicamente, é possível comparar o modo baseado em conexões com um telefone;
eles têm de estabelecer uma conexão e suspender a ligação. Todos os dados que fluem entre
esses dois eventos chegam na mesma ordem em que foram transmitidos. Entretanto, os
sockets não orientados a conexão podem ser comparados ao sistema de correios, onde a
entrega não é garantida, e os diferentes itens de correspondência podem chegar em uma
ordem diferente daquela em que foram enviados.
A escolha do modo a ser utilizado é determinada a partir das necessidades de uma aplicação
distribuída. Caso a confirmação de entrega e a sequência de dados sejam importantes, então a
operação baseada em conexões é, sem dúvidas, a melhor opção. Por exemplo, os servidores
de arquivos precisam fazer todos os seus dados chegarem corretamente e em sequência. Se
alguma parte dos dados se perdesse, a utilidade da aplicação servidora seria invalidada.
Porém, o processo realizado pelas aplicações para garantir a sequência e a correção dos dados
exigem processamento extra e maior utilização da memória, ocasionando uma redução no
tempo de resposta de um servidor.
O modo baseado em conexões utiliza o TCP ou Transport Control Protocol para se estabelecer
uma conexão com o dispositivo de destino antes de transmitir os dados.
Assim que a conexão for estabelecida, os sockets são acessados por uma interface de fluxo de
dados, representando as operações de: abertura, leitura, escrita e fechamento.
Por meio dessa conexão, tudo que é enviado por um socket é recebido pela outra extremidade
da conexão, na mesma ordem em que foi transmitido. Apesar de a operação baseada em
conexões ser menos eficiente, pelo tempo extra de processamento e resposta, do que a
operação sem conexão, ela é mais garantida e confiável.
O modo sem conexão utiliza o UDP ou User Datagram Protocol. O termo datagram ou
datagrama é utilizado para determinar uma unidade autônoma que possui todas as
informações necessárias para tentar realizar sua entrega. Analogicamente, um datagrama
possui as mesmas características que um envelope, pois possui os endereços de destinatário e
remetente além de conter em seu interior os dados a serem transmitidos. Um socket nesse
modo de operação não necessita estabelecer uma conexão com um socket de destino; ele
simplesmente envia o datagrama. Caso o dispositivo de destino esteja esperando o datagrama,
ele o recebe; caso contrário, o datagrama é perdido. A operação sem conexão é rápida e
eficiente, porém não garante a entrega nem a ordem dos dados.
Um pouco sobre streams...
Em Java, utiliza-se o termo stream para conceituar quase todas as operações que se baseiam
nas mais diferentes formas de se realizar a leitura e escrita de dados a partir das mais diversas
fontes e para os mais diferentes destinos.
Uma stream é uma abstração que representa uma fonte genérica de entrada de dados ou um
destino genérico para escrita de dados, de acesso sequencial e independente de dispositivos
físicos, formatos e até mesmo de mecanismos de otimização de leitura e escrita. Por ser uma
abstração, deve sempre ser associada a uma entidade física de suporte a dados, como um
disco rígido, um CD-ROM, um DVD-ROM, outro computador da rede, entre outros dispositivos.
A Figura 4 exemplifica o uso de streams.
Figura 4. Streams como abstração de leitura e escrita.
Para que seja possível ler uma determinada informação, um programa abre uma stream sobre
uma fonte de dados, por exemplo, um socket, e lê essa informação sequencialmente, byte a
byte ou caractere a caractere.
Inversamente, um programa pode enviar informação para um destino externo abrindo uma
stream de escrita e escrevendo dados de modo sequencial no fluxo de informações.
Para a leitura sequencial de bytes utiliza-se um objeto da classe InputStream, obtido como
retorno do método getInputStream(). Para transferência de texto, a ponte DataInputStream
pode ser utilizada para usar um reader. Similarmente, para a transferência de dados no
sentido deste socket para o outro extremo da conexão, utiliza-se um objeto da classe
OutputStream. Para transferência de texto neste sentido, a ponte utilizada é
DataOutputStream, permitindo a obtenção de um writer associado a esse stream.
Desenvolvimento de sockets TCP
Conforme apresentado anteriormente, sockets TCP ou orientado a conexões são altamente
utilizados quando se necessita de confirmação de conexão entre o cliente e o servidor para
que seja possível a transmissão correta e confiável de dados. Para essa comunicação, também
se torna necessária a definição de um fluxo de dados, para que os dados sejam enviados e
recebidos sequencialmente, garantindo o sucesso da operação.
Implementação de socket TCP de servidor
A principal funcionalidade do socket do lado do servidor é a de aguardar por solicitações de
conexões e, após serem requeridas, tratá-las e servi-las.
Para realizar a implementação de um socket de servidor, existem alguns passos a serem
seguidos:
1. Estabelecer um socket de servidor que monitore uma determinada porta para receber
conexões. Para isso, deve-se criar uma instância da classe ServerSocket, seguindo uma das
quatro maneiras possíveis:
a. ServerSocket() - Simplesmente cria uma instância de ServerSocket conforme sua
implementação nativa do Java, buscando os valores padrões;
b. ServerSocket(int porta) - Cria uma instância de ServerSocket e vincula o socket a uma
determinada porta;
c. ServerSocket(int porta, int backlog) - Além de criar uma instância de ServerSocket e vinculá-
la a uma determinada porta, também define o tamanho da fila de requisições;
d. ServerSocket(int porta, int backlog, InetAddress endereco) - Cria uma instância de
ServerSocket, vincula-a a uma determinada porta estabelecendo o tamanho da fila de
requisições e associa-a ao endereço InetAddress passado como parâmetro.
ServerSocket server = new ServerSocket(1234);
2. A próxima etapa é determinar a criação de uma socket assim que uma requisição de
conexão for realizada.
Para isso, deve-se recuperar a instância de Socket utilizando-se o método accept() do socket
do servidor;
Socket socket = server.accept();
3. Aceitando a requisição de conexão vinda de um cliente e tendo recuperado o socket por
meio do método accept(), deve-se criar um canal de comunicação direita entre o servidor e o
cliente. Para ser possível receber dados pelo canal de comunicação, recupera-se uma instância
de DataInputStream do socket do cliente por meio do método getInputStream(). Para ser
possível enviar dados pelo canal de comunicação, recupera-se uma instância de
DataOutputStream do socket recuperado durante a conexão por meio do método
getOutputStream().
DataInputStream dis = new DataInputStream
(socket.getInputStream());
DataOutputStream dos = new DataOutputStream
(socket.getOutputStream());
4. Utilizando-se essas duas instâncias do socket recuperado e invocando os métodos
necessários, já é possível receber e enviar dados de diversos tipos como boolean, byte, char,
double, float, int, long, short e cadeias de caracteres por meio de UTF (Unicode Transformation
Format). Para exemplificar segue, respectivamente, as codificações para recebimento e envio
de dados UTF, utilizando-se os métodos readUTF() e writeUTF();
String mensagem = dis.readUTF();
dos.writeUTF(mensagem);
5. Assim que todo o processo de requisição e processamento com o cliente for concluído, o
servidor deve encerrar os canais de comunicação utilizando-se o método close() das instâncias
de DataInputStream e DataOutputStream, assim como a instância do socket do cliente e
também do socket do servidor.
dis.close();
dos.close();
socket.close();
server.close();
A Listagem 1 apresenta uma implementação de socket TCP de servidor. Em resumo, é criada
uma instância de ServerSocket, aguardando conexões na porta “1234”. Assim que uma nova
conexão é solicitada a esse servidor, um socket de cliente é recuperado pelo método accept().
Por meio do socket recuperado, obtém-se uma instância para controlar o fluxo de dados para
recebimento de dados (DataInputStream) e outra para o envio de dados (DataOutputStream).
Utilizando-se o método readUTF() aguardamos o recebimento de uma string pelo canal de
comunicação que, em seguida, é exibido no console Java. Em resposta, o servidor envia outra
cadeia de caracteres pelo canal de comunicação por meio do método writeUTF() e encerra
tanto as instâncias de controle de dados stream como os sockets por meio do método close().
package servidor;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class SocketServidor {
private static final int PORTA = 1234;
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(PORTA);
Socket socket = server.accept();
DataInputStream dis = new DataInputStream
(socket.getInputStream());
DataOutputStream dos = new DataOutputStream
(socket.getOutputStream());
String mensagem = dis.readUTF();
System.out.println("Servidor: Mensagem recebida =>
\"" + mensagem + "\"");
mensagem = "Mensagem recebida com sucesso às " + new Date();
System.out.println("Servidor: Mensagem enviada =>
\"" + mensagem + "\"");
dos.writeUTF(mensagem);
dis.close();
dos.close();
socket.close();
server.close();
} catch (Exception ex) {
/* tratar exceção */
}
}
}
Listagem 1. SocketServidor.java: Implementação de socket TCP de servidor
Tendo desenvolvido a aplicação do lado do servidor, basta implementar a aplicação do lado do
cliente, responsável por conectar-se ao socket e realizar as requisições de envio e recebimento
de dados.
UTF ou Unicode Transformation Format: É um padrão estabelecido pela Unicode Consortium
que permite aos computadores representar e manipular, de maneira consistente, textos de
qualquer escrita existente, sendo também compatível com o ASCII.
Implementação de socket TCP de cliente
O principal objetivo do socket do lado do cliente é o de solicitar uma conexão com um socket
de servidor e comunicar-se por meio de um canal de comunicação estabelecido logo após a
requisição de conexão. A implementação de uma aplicação cliente baseada em conexões pode
ser realizada seguindo as instruções:
1. O primeiro passo é solicitar a conexão com um servidor, seguindo uma das duas maneiras
disponíveis:
a. Criando uma instância de Socket. Para isso, deve-se informar apenas o nome ou IP do host
de conexão e a porta disponibilizada pelo socket de servidor. Para criar uma instância de
Socket, é necessário utilizar um dos sete construtores disponíveis na classe Socket, sendo os
mais usados:
• Socket() - Simplesmente cria uma instância de Socket conforme sua implementação nativa
do Java, buscando os valores padrões;
• Socket(InetAddress endereco, int porta) - Cria uma instância de Socket, conectando-se a um
endereço específico e a uma determinada porta;
• Socket(String host, int porta) - Cria uma instância de Socket, conectando-se a um host
específico e a uma determinada porta.
Socket socket = new Socket(“127.0.0.1”, 1234);
b. Conectando ao servidor sem a passagem de parâmetros durante a instanciação de Socket.
Neste caso, torna-se necessário utilizar a classe SocketAddress.
Socket socket = new Socket();
socket.connect(new SocketAddress(“127.0.0.1”, 1234));
2. A criação das instâncias de manipulação de stream do lado do cliente é exatamente igual a
do servidor. Para ser possível receber dados pelo canal de comunicação, recupera-se uma
instância de DataInputStream do socket recuperado por meio do método getInputStream().
Para ser possível enviar dados pelo canal de comunicação, recupera-se uma instância de
DataOutputStream do socket do cliente durante a conexão por meio do método
getOutputStream().
DataInputStream dis = new DataInputStream
(socket.getInputStream());
DataOutputStream dos = new DataOutputStream
(socket.getOutputStream());
3. Utilizando-se as instâncias de entrada e saída de dados do canal de comunicação,
recuperados a partir do socket conectado, já é possível receber e enviar informações de
diversos tipos. Para exemplificar segue, respectivamente, as instruções utilizadas para
recebimento e envio de dados UTF, por meio dos métodos readUTF() e writeUTF():
String mensagem = dis.readUTF();
dos.writeUTF(mensagem);
4. Assim que todo o processo de requisição e processamento com o cliente for concluído, o
servidor deve encerrar os canais de comunicação utilizando-se o método close() das instâncias
de DataInputStream e DataOutputStream, bem como finalizar as instâncias dos sockets do
cliente e do servidor.
dis.close();
dos.close();
socket.close();
server.close();
A Listagem 2 apresenta uma implementação de socket TCP de cliente. Um socket de cliente é
criado e tenta conectar-se ao endereço “127.0.0.1” por meio da porta “1234”. Utiliza-se o
endereço “127.0.0.1” ou “localhost” quando se deseja realizar testes de conexão na máquina
local, funcionando tanto como servidor como cliente. Caso o servidor esteja sendo executado
em alguma outra máquina na rede, necessita-se recuperar o IP da máquina e trocá-lo na
aplicação desenvolvida.
Assim que o socket da aplicação cliente obter sucesso na conexão com o socket da aplicação
servidora, é necessário obter uma instância para controlar o fluxo de dados para recebimento
de dados (DataInputStream) e outra para o envio de dados (DataOutputStream). Utilizando-se
o método writeUTF() é possível enviar uma string pelo canal de comunicação criado entre o
cliente e o servidor. Após o envio da cadeia de caracteres da aplicação cliente para a aplicação
servidora, utiliza-se o método readUTF() para fazer com que a aplicação aguarde o
recebimento de dados pelo canal de comunicação. Assim que uma string for transmitida em
resposta pelo servidor, ela será exibida no console Java. Ao final do fluxo de dados e do
processamento, encerram-se tanto as instâncias de controle de dados stream como os sockets
através do método close().
package cliente;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Date;
public class SocketCliente {
private static final String HOST = "127.0.0.1";
private static final int PORTA = 1234;
public static void main(String[] args) {
try {
Socket socket = new Socket(HOST, PORTA);
DataInputStream dis = new DataInputStream
(socket.getInputStream());
DataOutputStream dos = new DataOutputStream
(socket.getOutputStream());
String mensagem = "Conectado às " + new Date();
System.out.println("Cliente: Mensagem enviada =>
\"" + mensagem + "\"");
dos.writeUTF(mensagem);
mensagem = dis.readUTF();
System.out.println("Cliente: Mensagem recebida =>
\"" + mensagem + "\"");
dis.close();
dos.close();
socket.close();
} catch (Exception ex) {
/* tratar exceção */
}
}
}
Listagem 2. SocketCliente.java: Implementação de socket TCP de cliente.
Após o desenvolvimento do servidor, conforme Listagem 1, e do cliente, demonstrado na
Listagem 2, é possível testar a comunicação entre essas duas aplicações.
Para isso, deve-se executar, primeiramente, o servidor e, logo em seguida, o cliente. Assim que
a conexão for estabelecida com sucesso, os aplicativos Java serão capazes de enviar e receber
mensagens na forma de string pelo canal de comunicação criado por meio dos sockets de rede.
Desenvolvimento de sockets UDP
Conforme apresentado anteriormente, sockets UDP ou não orientado a conexões são
utilizados quando não há a necessidade de estabelecer uma conexão entre a máquina cliente e
a servidora. Um socket UDP envia um datagram ou datagrama que, similar a um envelope,
contém informações sobre o destinatário, remetente e os dados a serem enviados.
Comparado ao modelo orientado a conexão, o protocolo UDP é mais rápido e eficiente, porém
a entrega e sequência das informações não é garantida ou confirmada, cabendo ao programa
se responsabilizar pela validação e segurança. Com um socket UDP, caso o destinatário não
esteja aguardando uma mensagem, ela é perdida. A Figura 5 demonstra o envio de um
datagrama por meio de um socket. O cabeçalho UDP é extremamente simples, contendo
apenas os números das portas de origem e destino, comprimento da mensagem e checksum,
um campo utilizado para conferência da integridade dos dados transmitidos.
Os campos em laranja, exibidos na Figura 5, são opcionais.
Figura 5. Envio de datagrama por socket UDP.
Para o desenvolvimento de aplicações que fazem uso do protocolo UDP, a tecnologia Java
conta com as classes DatagramSocket e DatagramPacket, presentes no pacote java.net, assim
como as classes ServerSocket e Socket.
Ao contrário da classe Socket, um DatagramSocket não estabelece uma conexão com uma
máquina remota, porém necessita apenas de uma conexão local com a rede de computadores
para que seja possível enviar e receber pacotes.
Os pacotes enviados e recebidos por meio de sockets UDP são instâncias da classe
DatagramPacket, contendo informações sobre o remetente e destinatário e os dados a serem
transmitidos.
Implementação de socket UDP de servidor
A implementação de sockets que utilizam o protocolo UDP é realizada por meio de instâncias
de DatagramSocket e DatagramPacket. Utilizando-se uma instância de DatagramSocket, é
possível determinar qual porta será reservada para o envio e recebimento de dados na forma
de pacotes, enquanto a de DatagramPacket é utilizada para a montagem de uma datagrama a
ser enviado, informando-se o host e porta do cliente, e até mesmo para aguardar informações
transmitidas por outros sockets UDP. Para o desenvolvimento de sockets UDP, existem alguns
passos a serem seguidos:
1. Estabelecer um socket de datagrama que disponibilize uma porta para as operações de
envio e recebimento de dados. Para isso, deve-se criar uma instância de DatagramSocket,
conforme apresentado a seguir:
a. DatagramSocket(int porta) - Cria uma instância de DatagramSocket e vincula o socket a uma
determinar porta, pela qual serão realizados o envio e recebimento de informações.
DatagramSocket datagramSocket =
new DatagramSocket(1234);
2. Recuperar uma instância de InetAddress, responsável por armazenar o host a ser utilizado
no processo de envio de datagramas.
Para isso, deve-se criar uma instância de InetAddress utilizando-se getByName() desta mesma
classe, informando, como parâmetro, uma string com o nome ou IP do host;
InetAddress address = InetAddress.
getByName(“127.0.0.1”);
3. Preparar um arranjo (array) de bytes, com tamanho pré-definido, a ser utilizado como buffer
durante a troca de dados;
byte[] buffer = new byte[1024];
4. Criar uma instância de DatagramPacket, responsável por ser um datagrama de envio e
recebimento de dados. A classe DatagramPacket pode ser instanciada de duas formas:
a. DatagramPacket(byte[] buffer, int tamanho) - Cria uma instância de DatagramPacket
utilizada para o recebimento de dados. As informações recuperadas serão armazenadas em
um arranjo de bytes, passado como parâmetro do construtor, juntamente com o tamanho
determinado;
DatagramPacket datagramPacket = new Datagram
Packet(buffer, buffer.length);
b. DatagramPacket(byte[] buffer, int tamanho, InetAddress endereço, int porta) - Cria uma
instância de DatagramPacket utilizada para o envio de dados. Os dados a serem transmitidos
devem ser transformados em um arranjo de bytes por meio do método getBytes() da classe
String. O arranjo de bytes é passado como parâmetro do construtor da classe DatagramPacket.
String mensagem = "Mensagem recebida com sucesso às " + new Date();
buffer = mensagem.getBytes();
DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length, address,
1235);
5. Possuindo as instâncias de DatagramSocket e DatagramPacket, torna-se possível realizar as
operações de envio e recebimento de dados. Conforme mencionado anteriormente, utiliza-se
as mesmas instâncias para as duas tarefas.
a. Para o recebimento de dados, utiliza-se o método receive(DatagramPacket datagrama),
invocado a partir da instância de DatagramSocket.
Em seguida, deve-se recuperar os bytes transmitidos, utilizando-se o método getData() e
transformá-los em uma cadeia de caracteres, passando o arranjo de bytes como parâmetro do
construtor da classe String.
datagramSocket.receive(datagramPacket);
buffer = datagramPacket.getData();
String mensagem = new String(buffer);
b. Para o envio de dados, utiliza-se o método send(DatagramPacket datagrama), invocado a
partir da instância de DatagramSocket.
datagramSocket.send(datagramPacket);
6. Após o processamento das operações, deve-se concluir a aplicação encerrando o datagrama
por meio do método close().
datagramSocket.close();
A Listagem 3 apresenta uma implementação de socket UDP de servidor.
O objetivo desta aplicação é aguardar o recebimento de um datagrama por uma porta e, logo
em seguida, enviar um novo datagrama para uma máquina por meio de um host e de uma
porta, sem a necessidade de se estabelecer uma conexão.
package servidor;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Date;
public class SocketServidor {
private static final String HOST = "127.0.0.1";
private static final int PORTA_SERVIDOR = 1234;
private static final int PORTA_CLIENTE = 1235;
private static int BUFFER_LENGTH = 1024;
public static void main(String[] args) {
try {
DatagramSocket datagramSocket =
new DatagramSocket(PORTA_SERVIDOR);
InetAddress address = InetAddress.getByName(HOST);
byte[] buffer = new byte[BUFFER_LENGTH];
DatagramPacket datagramPacket =
new DatagramPacket(buffer, buffer.length);
datagramSocket.receive(datagramPacket);
buffer = datagramPacket.getData();
String mensagem = new String(buffer);
System.out.println("Servidor: Mensagem recebida =>
\"" + mensagem + "\"");
mensagem = "Mensagem recebida com sucesso às " + new Date();
System.out.println("Servidor: Mensagem enviada =>
\"" + mensagem + "\"");
buffer = mensagem.getBytes();
datagramPacket = new DatagramPacket
(buffer, buffer.length, address, PORTA_CLIENTE);
datagramSocket.send(datagramPacket);
datagramSocket.close();
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
Listagem 3. SocketServidor.java: Implementação de socket UDP de servidor.
Tendo desenvolvido a aplicação do lado do servidor, basta implementar a aplicação do lado do
cliente, responsável por aguardar o recebimento de dados e transmitir um datagrama de
resposta.
Implementação de socket UDP de cliente
Conforme informado anteriormente, tanto a aplicação servidora quanto a cliente são
implementadas por meio de instâncias de DatagramSocket e DatagramPacket. Portanto, a
aplicação cliente será desenvolvida utilizando-se os mesmos conceitos já apresentados.
A Listagem 4 apresenta uma implementação de socket UDP de cliente.
package cliente;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Date;
public class SocketCliente {
private static final String HOST = "127.0.0.1";
private static final int PORTA_SERVIDOR = 1234;
private static final int PORTA_CLIENTE = 1235;
private static int BUFFER_LENGTH = 1024;
public static void main(String[] args) {
try {
DatagramSocket datagramSocket =
new DatagramSocket(PORTA_CLIENTE);
InetAddress address = InetAddress.getByName(HOST);
String mensagem = "Conectado às " + new Date();
System.out.println("Cliente: Mensagem enviada =>
\"" + mensagem + "\"");
byte[] buffer = mensagem.getBytes();
DatagramPacket datagramPacket =
new DatagramPacket(buffer, buffer.length,
address, PORTA_SERVIDOR);
datagramSocket.send(datagramPacket);
buffer = new byte[BUFFER_LENGTH];
datagramPacket = new DatagramPacket
(buffer, buffer.length);
datagramSocket.receive(datagramPacket);
buffer = datagramPacket.getData();
mensagem = new String(buffer);
System.out.println("Cliente: Mensagem recebida =>
\"" + mensagem + "\"");
datagramSocket.close();
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
Listagem 4. SocketCliente.java: Implementação de socket UDP de cliente.
O objetivo desta aplicação é preparar uma mensagem para ser enviada por um datagrama
para um determinado host e porta e aguardar o recebimento de dados por meio de uma porta
disponibilizada pela aplicação, sem a necessidade de se estabelecer uma conexão.
Com os exemplos das Listagens 3 e 4 pode-se perceber que, utilizando-se sockets no modo
UDP, a aplicação não necessita estabelecer uma conexão entre as máquinas, portanto,
também não é possível garantir a entrega e a sequência correta de dados, já que nem sempre
a máquina destinatária pode estar preparada para receber informações enviadas pela máquina
remetente. O modo sem conexão (ou UDP) é muito mais rápido do que o modo baseado em
conexões (ou TCP) pelo fato de não fazer verificações e por não estabelecer sessões entre
máquinas.
Desenvolvimento de sockets para transmissão e recebimento de arquivos
A tecnologia de Sockets em Java também permite não só a troca de informações por canais de
comunicação, mas também o compartilhamento de arquivos lógicos como textos, imagens,
vídeos, entre outros formatos de dados.
Para implementar uma arquitetura capaz de transmitir e receber arquivos, é necessário
utilizar duas classes já apresentadas: ServerSocket e Socket. Por meio dessas classes,
estabelece-se uma conexão e um canal de comunicação, por onde serão transmitidas as
informações. Em resumo, para o servidor transmitir um arquivo, necessita-se passar por um
processo de transformação de arquivo lógico para um arranjo de bytes. Para o cliente,
receptor do arquivo, o processo ocorre de maneira contrária, transformando-se o arranjo de
bytes em arquivo lógico.
Implementação de socket de servidor para transmissão de arquivos
Conforme informado anteriormente, a classe ServerSocket também pode ser utilizada para
transmitir arquivos lógicos como textos e imagens.
A diferença se dá pelo fato de que o arquivo necessita passar por um processo de
decodificação, sendo transformado em um arranjo de bytes, utilizando-se o método read() de
uma instância de BufferedInputStream. Após essa transformação, a informação pode ser
transmitida assim como qualquer outro tipo de dado, por meio do método write() de uma
instância de DataOutputStream.
A Figura 6 ilustra o arquivo a ser transmitido pela aplicação servidora.
A Listagem 5 apresenta uma implementação de socket de servidor que disponibiliza a
transmissão de um arquivo de imagem. O objetivo desta aplicação é preparar um arquivo para
ser enviado assim que uma conexão for estabelecida.
Figura 6. Logotipo “Google Android”
package servidor;
import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServidor {
private static final int PORTA = 1234;
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(PORTA);
Socket socket = server.accept();
DataOutputStream dos = new DataOutputStream
(socket.getOutputStream());
File arquivo = new File("C:\\Users\\
Guilherme\\Desktop\\android.gif");
byte[] buffer = new byte[(int) arquivo.length()];
FileInputStream fis = new FileInputStream(arquivo);
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read(buffer, 0, buffer.length);
System.out.println("Enviando arquivo
\"" + arquivo.getName() + "\"");
dos.write(buffer, 0, buffer.length);
dos.flush();
dos.close();
socket.close();
server.close();
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
Listagem 5. SocketServidor.java: Implementação de socket de servidor para transmissão de
arquivos.
Tendo desenvolvido a aplicação do lado do servidor, basta implementar a aplicação do lado do
cliente, responsável por conectar-se ao socket e realizar as requisições de recebimento de
dados e transformação do arranjo de bytes em um arquivo lógico.
Implementação de socket de cliente para recebimento de arquivos
Conforme informado anteriormente, a classe Socket também pode ser utilizada para receber
arquivos transmitidos por um servidor utilizando-se um canal de comunicação. A informação
recebida, por meio do método read() de uma instância de DataInputStream, é um arranjo de
bytes e para se tornar um arquivo legível deve passar por um processo de transformação. O
processo de transformação ocorre de maneira simples, onde os bytes são lidos no canal de
comunicação e, ao final da leitura, utiliza-se o método write() de uma instância de
BufferedOutputStream para “escrever” as informações lidas em um arquivo lógico dentro de
um diretório informado.
A Listagem 6 apresenta uma implementação de socket de cliente que aguarda o recebimento
de um arquivo de imagem. O objetivo desta aplicação é, além de receber informações no
formato de bytes, transformar os dados recebidos e armazenar o arquivo final em um diretório
especificado no código-fonte.
package cliente;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.net.Socket;
public class SocketCliente {
private static final String HOST = "127.0.0.1";
private static final int PORTA = 1234;
private static final int FILE_SIZE = 6022386;
public static void main(String[] args) {
try {
Socket socket = new Socket(HOST, PORTA);
DataInputStream dis = new DataInputStream
(socket.getInputStream());
byte[] mybytearray = new byte[FILE_SIZE];
FileOutputStream fos = new FileOutputStream
("C:\\Users\\Guilherme\\Desktop\\android_copy.gif");
BufferedOutputStream bos = new BufferedOutputStream(fos);
int bytesRead = dis.read(mybytearray, 0, mybytearray.length);
int current = bytesRead;
do {
bytesRead = dis.read(mybytearray, current,
(mybytearray.length - current));
if (bytesRead >= 0) {
current += bytesRead;
}
} while (bytesRead > -1);
bos.write(mybytearray, 0, current);
System.out.println("Arquivo recebido com sucesso!");
bos.flush();
bos.close();
socket.close();
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
}
Listagem 6. SocketCliente.java: Implementação de socket de cliente para recebimento de
arquivos.
Com o desenvolvimento das aplicações cliente e servidor das Listagens 5 e 6 foi possível
perceber a facilidade e praticidade quando se deseja compartilhar arquivos entre processos
executados em máquinas diferentes por meio de sockets TCP. Esta ideia pode ser modificada
para aplicativos que gerenciam relatórios, gráficos, imagens e outros documentos onde o
processamento pode ou deve permanecer isolado apenas em uma máquina responsável por
prover serviços solicitados pelos clientes.
Conclusão
O conceito de computação distribuída não é recente e nem tão pouco utilizada, estando
presente em quase todos os locais e situações, porém o tema, por muitas vezes, não recebe a
importância devida, seja no ambiente acadêmico ou corporativo, cabendo aos
desenvolvedores e analistas realizar pesquisas em tecnologias antes ignoradas.
Graças ao aumento da necessidade e crescente uso da rede de computadores, principalmente
pela popularização da Internet, tornou-se necessário adaptar o processo de desenvolvimento
de software para que as aplicações trabalhem de maneira distribuída e descentralizada.
Este artigo apresentou os conceitos e mecanismos utilizados para a implementação de
aplicativos que fazem uso de redes de computadores para a troca de informações e
processamento de dados. A tecnologia Java nos provê classes e interfaces adequadas para a
implementação de aplicações que, por meio de um canal de comunicação, possam enviar e
receber dados de diversos tipos, facilitando o desenvolvimento de projetos baseados na
arquitetura distribuída.
Além dos conceitos e do embasamento teórico, o artigo apresentou, de forma didática,
alguns exemplos de aplicações que fazem uso de classes relacionadas ao desenvolvimento
com sockets, demonstrando que não é necessário possuir conhecimentos sobre detalhes da
implementação de algoritmos ou hardware de rede para produzir aplicações distribuídas em
Java.
O que foi abordado neste artigo é apenas uma breve amostra de como a tecnologia de
sockets pode auxiliar no desenvolvimento de aplicações Java, incorporando características da
computação distribuída no desenvolvendo de software.
Há muito ainda a ser explorado sobre essa tecnologia pouco comentada, porém muito
utilizada, criando a possibilidade de novos artigos e trabalhos relacionados.
LINKS
1. http://download.oracle.com/javase/tutorial/networking/sockets/
Página oficial da Oracle (Sun) sobre sockets em Java.
2. http://java.sun.com/developer/technicalArticles/ALT/sockets/
Página oficial da Oracle (Sun) sobre programação avançada de sockets.
3. http://www.oracle.com/technetwork/java/socket-140484.html
Tutorial oficial da Oracle (Sun) sobre desenvolvimento de aplicações distribuídas
baseadas em sockets.
4. http://www.javaworld.com/javaworld/jw-12-1996/jw-12-sockets.html
Artigo sobre desenvolvimento de sockets em Java da InfoWorld - Java World.
5. http://www.devarticles.com/c/a/Java/Socket-Programming-in-Java/
Artigo sobre desenvolvimento de sockets em Java da DevArticles.
Autor: GUILHERME DE CLEVA FARTO, Revista Easy Java Magazine, Edição 6, 2011.