Neste artigo veremos como realizar transferência de arquivos pela rede usando a classe Socket em Java. Aprenderemos também um pouco sobre propriedades transientes e serialização de objeto para transferência.
Nosso intuito neste artigo é demonstrar uma solução para determinado problema, que é exatamente a transferência de arquivos entre dois hosts, então serão o mais práticos possíveis deixando um pouco a teoria de lado.
Saiba mais: Cursos de Java
A ideia do projeto
Antes de começarmos a desenvolver precisamos entender o que iremos fazer. A ideia aqui é ter dois hosts: um servidor e um cliente.
- O Servidor será iniciado e ficará aguardando o envio de algum arquivo vindo do cliente, quando este arquivo chegar ele irá salvá-lo no diretório que o cliente solicitou. A nossa classe Servidora não terá nenhuma interface gráfica, apenas será uma ferramenta de “escuta de envio de arquivos”.
- O Cliente, por outro lado, contará com uma interface gráfica simples onde o mesmo poderá escolher o arquivo que deseja enviar, o diretório de destino e as informações do servidor de destino (IP e Porta), dessa forma o cliente poderá enviar o arquivo para qualquer servidor que esteja executando a classe de escuta de arquivos.
A classe Arquivo.java
Criaremos uma classe Arquivo, como mostra a Listagem 1, que irá “carregar” o arquivo que foi escolhido no cliente e mais algumas informações importantes, como por exemplo: tamanho do arquivo, nome do arquivo e etc. Entenda que o arquivo é um aglomerado de bytes, independente do seu tipo (pdf, vídeo, texto, música, etc.), sendo assim para garantir a integridade do arquivo nós gravaremos seu conteúdo como byte[].
package br.com.transientfield.bean;
import java.io.Serializable;
import java.util.Date;
public class Arquivo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String nome;
private byte[] conteudo;
private transient long tamanhoKB;
private transient Date dataHoraUpload;
private transient String ipDestino;
private transient String portaDestino;
private String diretorioDestino;
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public byte[] getConteudo() {
return conteudo;
}
public void setConteudo(byte[] conteudo) {
this.conteudo = conteudo;
}
public long getTamanhoKB() {
return tamanhoKB;
}
public void setTamanhoKB(long tamanhoKB) {
this.tamanhoKB = tamanhoKB;
}
public Date getDataHoraUpload() {
return dataHoraUpload;
}
public void setDataHoraUpload(Date dataHoraUpload) {
this.dataHoraUpload = dataHoraUpload;
}
public String getIpDestino() {
return ipDestino;
}
public void setIpDestino(String ipDestino) {
this.ipDestino = ipDestino;
}
public String getPortaDestino() {
return portaDestino;
}
public void setPortaDestino(String portaDestino) {
this.portaDestino = portaDestino;
}
public String getDiretorioDestino() {
return diretorioDestino;
}
public void setDiretorioDestino(String diretorioDestino) {
this.diretorioDestino = diretorioDestino;
}
}
Você pode perguntar-se, porque optamos por byte[] ? Com o byte[] nós podemos gravar também o conteúdo do arquivo facilmente no banco de dados, e retorná-lo depois sem perder as informações. Lembre-se: Nós não sabemos que tipo de arquivo será enviado.
Saiba mais: Últimas Atualizações Java
Vamos ver alguns pontos importantes sobre a classe da Listagem 1:
- Ela é Serializable: É necessário que nossa classe implemente a interface Serializable para que possamos converter toda ela em um aglomerado de bytes (byte[]) e transferi-la via Socket.
- Os campos 'transient': Alguns dos nossos campos estão identificados com a palavra reservada 'transient' como, por exemplo, o campo “ipDestino”. Um campo transiente não é serializado, consequentemente aquele aglomerado de bytes definido no item acima não irá conter o campo “ipDestino”, perceba que este é útil apenas no lado do cliente, pois é o cliente que precisa saber para quem ele irá enviar o arquivo, não faz sentido o servidor possuir o campo “ipDestino” na classe Arquivo, enviar este campo seria desperdício de banda e processamento.
A classe Arquivo.java no servidor
Não precisamos de alguns campos no servidor, por isso eles foram anotados como transientes para evitar desperdício de banda e processamento. Então no lado do servidor nós iremos criar a mesma classe Arquivo.java, como mostra a Listagem 2, no mesmo pacote “br.com.transientfield.bean”, porém sem os campos transientes já que eles nunca serão requisitados.
package br.com.transientfield.bean;
import java.io.Serializable;
import java.util.Date;
public class Arquivo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String nome;
private byte[] conteudo;
private String diretorioDestino;
private Date dataHoraUpload;
public Date getDataHoraUpload() {
return dataHoraUpload;
}
public void setDataHoraUpload(Date dataHoraUpload) {
this.dataHoraUpload = dataHoraUpload;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public byte[] getConteudo() {
return conteudo;
}
public void setConteudo(byte[] conteudo) {
this.conteudo = conteudo;
}
public String getDiretorioDestino() {
return diretorioDestino;
}
public void setDiretorioDestino(String diretorioDestino) {
this.diretorioDestino = diretorioDestino;
}
}
Não iremos reexplicar esta classe, pois segue o mesmo princípio da classe Arquivo no cliente, porém sem alguns atributos que não são necessários.
Implementando o Servidor
Em linhas gerais a nossa classe servidor fica aguardando o envio de um arquivo pelo cliente, quando este arquivo é recebido então ele é desserilizado e salvo no diretório desejado. Dividimos a Listagem 3 em blocos de comentários para que a explicação fique de mais fácil entendimento.
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
import br.com.transientfield.bean.Arquivo;
public class Server {
public static void main(String args[]) {
try {
//1
ServerSocket srvSocket = new ServerSocket(5566);
System.out.println("Aguardando envio de arquivo ...");
Socket socket = srvSocket.accept();
//2
byte[] objectAsByte = new byte[socket.getReceiveBufferSize()];
BufferedInputStream bf = new BufferedInputStream(
socket.getInputStream());
bf.read(objectAsByte);
//3
Arquivo arquivo = (Arquivo) getObjectFromByte(objectAsByte);
//4
String dir = arquivo.getDiretorioDestino().endsWith("/") ? arquivo
.getDiretorioDestino() + arquivo.getNome() : arquivo
.getDiretorioDestino() + "/" + arquivo.getNome();
System.out.println("Escrevendo arquivo " + dir);
//5
FileOutputStream fos = new FileOutputStream(dir);
fos.write(arquivo.getConteudo());
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static Object getObjectFromByte(byte[] objectAsByte) {
Object obj = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
bis = new ByteArrayInputStream(objectAsByte);
ois = new ObjectInputStream(bis);
obj = ois.readObject();
bis.close();
ois.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return obj;
}
}
Vamos as explicações divididas por bloco de comentários:
- Logo no início nós criamos inicializamos o ServerSocket usando a porta 5566 e aguardando a conexão do cliente. Ao executar o método accept() o servidor ficará aguardando que alguém conecte-se a ele, até que tal conexão seja realizada ele ficará em “sleep” neste ponto.
- Neste ponto uma conexão já foi realizada e usamos o método getReceiveBufferSize() para saber o tamanho do objeto que foi enviado, em bytes. O BufferedInputStream irá realizar a tarefa de capturar o que foi lido do inputStream do socket e gravar na variável “objectAsByte” através do método read(), ou seja, o que está no canal de comunicação é gravado no objectAsByte.
- Já temos em mãos o que foi transferido, porém estes dados estão em bytes e não serão muito úteis se continuarem nesta forma. Usando um método para desserialização de dados, o getObjectFromByte(), nós transformamos os bytes em um objeto Arquivo. Obviamente que nós sabemos que o que será transferido é sempre um objeto Arquivo, pois se outro tipo de objeto fosse transferido iríamos ter um erro de cast.
- Com o objeto Arquivo podemos começar a manipular os dados para salvar o arquivo no diretório desejado. Formatamos este diretório certificando-se de colocar todas as informações necessárias.
- Por fim usamos o FileOutputStream passando o nome do diretório onde queremos gravar nosso arquivo. Através do método write() passamos um aglomerado de bytes que está no atributo conteudo do bean Arquivo, assim o FileOutputStream saberá o que gravar e onde gravar.
Saiba mais: Guias de Conteúdo sobre Java
Você já pode iniciar o cliente e deixar o mesmo monitorando o envio de arquivos, vamos agora construir nosso cliente.
Implementando o Cliente
Primeiro vamos ver a interface que idealizamos em nosso projeto, como mostra a Figura 1.
O formulário da Figura 1 possui os seguintes campos:
- Arquivo Carregado: Mostra o nome do arquivo após selecionado no botão “Selecionar Arquivo”.
- IP: Número do IP do Servidor. Ex: 192.168.2.1.
- Porta: Número da Porta do Servidor. Ex: 5566 (porta usada no nosso exemplo).
- Diretório Dest.: Diretório onde o arquivo será salvo no servidor. Independente se for Linux ou Windows.
- Selecionar Arquivo: Abre a janela para seleção do arquivo que será enviado e depois preenche o label “tamanho” com o tamanho do arquivo em KB.
- Enviar: Inicia o processo de envio do arquivo do Cliente para o Servidor. Neste ponto o servidor é notificado que uma conexão está sendo estabelecida.
Por fim, temos a classe ClienteSocket.java, como mostra a Listagem 4, que representa o formulário demonstrado na Figura 1. Mostraremos a mesma dividida em blocos para facilitar a compreensão.
package br.com.transientfield.gui;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Date;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import br.com.transientfield.bean.Arquivo;
public class ClienteSocket extends javax.swing.JFrame {
private long tamanhoPermitidoKB = 5120; //Igual a 5MB
private Arquivo arquivo;
private static final long serialVersionUID = 1L;
public ClienteSocket() {
initComponents();
}
private void initComponents() {
jLabel1 = new javax.swing.JLabel();
jTextFieldNome = new javax.swing.JTextField();
jButtonArquivo = new javax.swing.JButton();
jLabelTamanho = new javax.swing.JLabel();
jButtonEnviar = new javax.swing.JButton();
jLabel2 = new javax.swing.JLabel();
jTextFieldIP = new javax.swing.JTextField();
jLabel3 = new javax.swing.JLabel();
jTextFieldDiretorio = new javax.swing.JTextField();
jLabel4 = new javax.swing.JLabel();
jTextFieldPorta = new javax.swing.JTextField();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
jLabel1.setText("Arquivo Carregado");
jTextFieldNome.setEnabled(false);
jButtonArquivo.setText("Selecionar Arquivo");
jButtonArquivo.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButtonArquivoActionPerformed(evt);
}
});
jLabelTamanho.setFont(new java.awt.Font("Dialog", 0, 12));
jLabelTamanho.setText("Tamanho:");
jButtonEnviar.setText("Enviar");
jButtonEnviar.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jButtonEnviarActionPerformed(evt);
}
});
jLabel2.setText("IP");
jLabel3.setText("Diret\u00f3rio Dest.");
jLabel4.setText("Porta");
javax.swing.GroupLayout layout = new javax.swing.GroupLayout
(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout
.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.
GroupLayout.Alignment.LEADING)
.addComponent(jTextFieldNome, javax.swing.
GroupLayout.DEFAULT_SIZE, 279, Short.MAX_VALUE)
.addComponent(jLabel1)
.addComponent(jButtonEnviar)
.addComponent(jButtonArquivo)
.addComponent(jLabelTamanho)
.addGroup(layout.createSequentialGroup()
.addComponent(jLabel2)
.addPreferredGap(javax.swing.LayoutStyle.
ComponentPlacement.RELATED, 104, Short.MAX_VALUE)
.addComponent(jTextFieldIP, javax.swing.
GroupLayout.PREFERRED_SIZE, 162, javax.swing.
GroupLayout.PREFERRED_SIZE))
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.
GroupLayout.Alignment.TRAILING, false)
.addGroup(javax.swing.GroupLayout.Alignment.
LEADING, layout.createSequentialGroup()
.addComponent(jLabel4)
.addPreferredGap(javax.swing.LayoutStyle.
ComponentPlacement.RELATED, javax.swing.
GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(jTextFieldPorta, javax.swing.
GroupLayout.PREFERRED_SIZE, 162, javax.swing.
GroupLayout.PREFERRED_SIZE))
.addGroup(javax.swing.GroupLayout.Alignment.
EADING, layout.createSequentialGroup()
.addComponent(jLabel3)
.addPreferredGap(javax.swing.LayoutStyle.
ComponentPlacement.RELATED)
.addComponent(jTextFieldDiretorio, javax.
swing.GroupLayout.PREFERRED_SIZE, 160,
javax.swing.GroupLayout.PREFERRED_SIZE)))
.addPreferredGap(javax.swing.LayoutStyle.
ComponentPlacement.RELATED, 2, javax.swing.
GroupLayout.PREFERRED_SIZE)))
.addGap(37, 37, 37))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.
Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jLabel1)
.addPreferredGap(javax.swing.LayoutStyle.
ComponentPlacement.RELATED)
.addComponent(jTextFieldNome, javax.swing.GroupLayout.
PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.
ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.
GroupLayout.Alignment.BASELINE)
.addComponent(jLabel2)
.addComponent(jTextFieldIP, javax.swing.GroupLayout.
PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.
RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.
Alignment.BASELINE)
.addComponent(jLabel4)
.addComponent(jTextFieldPorta, javax.swing.GroupLayout.
PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.
RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.
Alignment.BASELINE)
.addComponent(jLabel3)
.addComponent(jTextFieldDiretorio, javax.swing.GroupLayout.
PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addGap(16, 16, 16)
.addComponent(jButtonArquivo)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jLabelTamanho)
.addGap(25, 25, 25)
.addComponent(jButtonEnviar)
.addGap(139, 139, 139))
);
pack();
}// </editor-fold>//GEN-END:initComponents
Na Listagem 4 temos:
- Os imports necessários para funcionamento do nosso ClienteSocket;
- Dois atributos: tamanhoPermitidoKB para armazenar o máximo em KB que um arquivo poderá ter para ser enviado, e 'arquivo' que irá guardar a instância do objeto Arquivo que foi configurado durante a seleção do arquivo;
- O construtor da classe ClienteSocket() chama um outro método: initComponents().
- O initComponents() é responsável por configurar todos os componentes do nosso formulários: botões, labels, caixas de texto e etc.
private void jButtonEnviarActionPerformed(java.awt.event.ActionEvent evt) {
enviarArquivoServidor();
}
private void jButtonArquivoActionPerformed(java.awt.event.ActionEvent evt) {
FileInputStream fis;
try {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
chooser.setDialogTitle("Escolha o arquivo");
if (chooser.showOpenDialog(this) == JFileChooser.OPEN_DIALOG) {
File fileSelected = chooser.getSelectedFile();
byte[] bFile = new byte[(int) fileSelected.length()];
fis = new FileInputStream(fileSelected);
fis.read(bFile);
fis.close();
long kbSize = fileSelected.length() / 1024;
jTextFieldNome.setText(fileSelected.getName());
jLabelTamanho.setText(kbSize + " KB");
arquivo = new Arquivo();
arquivo.setConteudo(bFile);
arquivo.setDataHoraUpload(new Date());
arquivo.setNome(fileSelected.getName());
arquivo.setTamanhoKB(kbSize);
arquivo.setIpDestino(jTextFieldIP.getText());
arquivo.setPortaDestino(jTextFieldPorta.getText());
arquivo.setDiretorioDestino(jTextFieldDiretorio.getText().trim());
}
} catch (Exception e) {
e.printStackTrace();
}
}
Na Listagem 5 temos o uso de dois métodos que complementam a Listagem 4, ambos são disparados no clique do botão, vejamos:
- O método jButtonEnviarActionPerformed() é disparado no clique do botão 'Enviar' e a sua ação é chamar o método enviarArquivoServidor() que será mostrado mais a frente.
- O método jButtonArquivoActionPerformed() é disparado no clique do botão 'Selecionar Arquivo' e o mesmo possui mais detalhes:
- Usamos a classe JFileChooser para permitir ao usuário a escolha do arquivo que será enviado.
- Criamos uma variável byte[] bFile através do tamanho do arquivo já selecionado, esta variável irá armazenar o conteúdo do arquivo. Conseguimos extrair o seu conteúdo através do método read() da classe FileInputStream que recebe o objeto File selecionado.
- Calculamos o tamanho do arquivo em KB e configuramos o nome do arquivo no jTextFieldNome.
2.4 – Instanciamos a variável arquivo setando todos os seus atributos.
private void enviarArquivoServidor(){
if (validaArquivo()){
try {
Socket socket = new Socket(jTextFieldIP.getText().trim(),
Integer.parseInt(jTextFieldPorta.getText().trim()));
BufferedOutputStream bf = new BufferedOutputStream
(socket.getOutputStream());
byte[] bytea = serializarArquivo();
bf.write(bytea);
bf.flush();
bf.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private byte[] serializarArquivo(){
try {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream ous;
ous = new ObjectOutputStream(bao);
ous.writeObject(arquivo);
return bao.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private boolean validaArquivo(){
if (arquivo.getTamanhoKB() > tamanhoPermitidoKB){
JOptionPane.showMessageDialog(this,
"Tamanho máximo permitido atingido ("+(tamanhoPermitidoKB/1024)+")");
return false;
}else{
return true;
}
}
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new ClienteSocket().setVisible(true);
}
});
}
private javax.swing.JButton jButtonArquivo;
private javax.swing.JButton jButtonEnviar;
private javax.swing.JLabel jLabel1;
private javax.swing.JLabel jLabel2;
private javax.swing.JLabel jLabel3;
private javax.swing.JLabel jLabel4;
private javax.swing.JLabel jLabelTamanho;
private javax.swing.JTextField jTextFieldDiretorio;
private javax.swing.JTextField jTextFieldIP;
private javax.swing.JTextField jTextFieldNome;
private javax.swing.JTextField jTextFieldPorta;
}
Na Listagem 6 temos alguns pontos importantes a serem notados:
- O método enviarArquivoServidor():
- Faz a validação do arquivo, checando se o mesmo possui o tamanho máximo permitido para envio;
- Realiza a conexão com o servidor através do IP e Porta digitados;
- Prepara o objeto BufferedOutputStream para escrita dos dados e envio ao servidor;
- Serializa o Arquivo que foi instanciado no passo 4, transformando o mesmo em um aglomerado de bytes, ou seja, byte[]. Com o método write() nós escrevemos os bytes para serem transferidos ao servidor e com o flush() nós forçamos essa transferência imediatamente.
- O método serializarArquivo() usa o ByteArrayOutputStream em conjunto com o ObjectOutputStream para escrever o objeto e converter para byte[].
- O método validarArquivo() apenas checa se o tamanho do arquivo é menor ou igual ao máximo permitido.
Para você leitor há ainda alguns desafios a serem enfrentados que deixamos para que você estude-os e resolva-os. Você perceberá que só conseguirá enviar arquivos com até 8129 bytes, e que sempre no envio de um arquivo ao término do processo o servidor será encerrado.
Então o seu desafio é fazer com que arquivos maiores possam ser enviados e que o servidor possibilite o envio de mais de um arquivo sem ser encerrado.
Focamos neste artigo a implementação de um projeto de transferência de arquivos via Socket, dando maior ênfase à parte prática. Os desafios irão lhe ajudar a assimilar melhor o conteúdo indo atrás de soluções para os problemas encontrados.
Links Úteis
- Curso de Yarn - Gerenciando de dependências com Yarn: Neste curso aprenderemos o que é o Yarn: um gerenciador de dependências concorrente do NPM que promete ser mais rápido e eficiente que este.
- Web services RESTful com Spring framework e JPA: Neste curso você vai aprender a criar sua primeira API REST baseada nos recursos do Spring Framework. Veremos como declarar corretamente os verbos HTTP em cada recurso consumido e também como definir, de forma apropriada, o status de cada resposta fornecida pela API.
- Gestão de projetos com MS Project: Este artigo apresenta uma das ferramentas mais usadas pelo mercado, o MS Project, demonstrando como preparar a ferramenta para seu uso adequado, configurar calendários e principalmente compartilhar essas configurações entre projetos.
Saiba mais sobre Java ;)
- Curso de Java EE: Construa uma aplicação completa Java EE: Neste curso de Java usaremos a plataforma WEB. Você irá encontrar o mais rico material sobre construções de aplicação utilizando as tecnologias e recursos do Java Enterprise Edition 7.
- Java Enterprise Edition - Java EE: Neste Guia de Referência você encontrará todo o conteúdo que precisa para conhecer o Java EE, Java Enterprise Edition, a plataforma Java voltada para o desenvolvimento de aplicações web/corporativas.
- Curso de Android: Neste curso iremos aprender a fazer um aplicativo de signos no Android Studio. Criamos um exemplo de uma calculadora de signos utilizando orientação a objetos na linguagem Java contida na IDE e arquivos XML que representam a parte visual de nosso aplicativo.