Java Socket: Transferência de Arquivos pela Rede

Veja neste artigo como realizar a transferência de arquivos pela rede usando a classe Socket do Java.

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.

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.

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; } }
Listagem 1. Arquivo.java

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.

Vamos ver alguns pontos importantes sobre a classe da Listagem 1:

  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.
  2. 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; } }
Listagem 2. Classe Arquivo.java no servidor

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; } }
Listagem 3. Classe Server.java

Vamos as explicações divididas por bloco de comentários:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

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.

Figura 1. Interface Cliente

O formulário da Figura 1 possui os seguintes campos:

  1. Arquivo Carregado: Mostra o nome do arquivo após selecionado no botão “Selecionar Arquivo”.
  2. IP: Número do IP do Servidor. Ex: 192.168.2.1.
  3. Porta: Número da Porta do Servidor. Ex: 5566 (porta usada no nosso exemplo).
  4. Diretório Dest.: Diretório onde o arquivo será salvo no servidor. Independente se for Linux ou Windows.
  5. 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.
  6. 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
Listagem 4. Formulário ClienteSocket.java parte 1

Na Listagem 4 temos:

  1. Os imports necessários para funcionamento do nosso ClienteSocket;
  2. 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;
  3. O construtor da classe ClienteSocket() chama um outro método: initComponents().
  4. 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(); } }
Listagem 5. Formulário ClienteSocket parte 2

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:

  1. 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.
  2. 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; }
Listagem 6. Formulário ClienteSocket parte 3

Na Listagem 6 temos alguns pontos importantes a serem notados:

  1. 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.
  2. O método serializarArquivo() usa o ByteArrayOutputStream em conjunto com o ObjectOutputStream para escrever o objeto e converter para byte[].
  3. 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.

Artigos relacionados