.NET

27 set, 2017

C# – Compartilhando arquivos – Cliente/Servidor (socket) – Client

Publicidade

Neste artigo, vamos criar uma aplicação para compartilhar arquivos usando a arquitetura cliente/servidor e os recursos dos sockets na linguagem C#.

A plataforma .NET fornece um conjunto de classes no namespace System.Net.sockets, que torna a nossa vida mais fácil, pois elas fornecem um conjunto de funcionalidades que abstraem muitas tarefas que antes exigiam a criação de um código extra.

Antes de entrar na aplicação propriamente dita, vamos recordar alguns conceitos básicos.

O que é um socket?

Um socket pode ser entendido como uma porta de um canal de comunicação que permite que um processo executando em um computador envie/receba mensagens para outro processo e até mesmo a partir dele, que pode estar sendo executado no mesmo computador ou em um computador remoto.

Os sockets permitem então a comunicação processo a processo da seguinte forma:

  • Comunicação local: processos locais usando sockets locais.
  • Comunicação remota: processos remotos usando sockets em rede (TCP/IP).

Abaixo temos uma figura que representa a comunicação de sockets e a pilha TCP/IP:

Paradigma cliente/servidor (modo orientado à conexão)

A seguir, a seqüência de ações realizadas no paradigma cliente/servidor:

Cliente Servidor
Cria um socket e atribui-lhe um endereço. Cria um socket e atribui-lhe um endereço. Este endereço deve ser conhecido pelo cliente.
Solicita a conexão do seu socket ao socket do servidor (conhece o endereço). Aguarda a conexão de um cliente.
Aguarda que a conexão seja estabelecida. Aceita a conexão e cria um novo socket para comunicação com o cliente em causa.
Envia uma mensagem (request). Recebe a mensagem no novo socket.
Recebe a mensage de resposta (reply). Envia mensagem de resposta (reply).
Fecha a conexão com o servidor. Fecha a conexão com o cliente.

Assim, um Socket é um objeto que representa um ponto de acesso de baixo nível para a pilha do protocolo internet (IP), onde ele é usado para enviar e receber dados, podendo ser aberto e fechado: os dados a serem enviados são sempre enviados em blocos conhecidos como pacotes.

Os pacotes devem conter o endereço IP da origem dos pacotes e do computador de destino onde os dados estão sendo enviados e, opcionalmente, ele pode conter um número de porta. Um número de porta está entre 1 e 65.535. Uma porta é um canal de comunicação ou nós de extremidade nos quais os computadores podem se comunicar. Recomenda-se sempre que os programas utilizem um número de porta superior a 1024 para evitar conflitos com outras aplicações em execução no sistema (uma vez que não existem duas aplicações que podem utilizar a mesma porta).

Os pacotes contendo números de porta, podem ser enviados usando UDP (User Datagram Protocol) ou TCP/IP (protocolo de controle de transmissão).

O UDP é mais fácil de usar do que o TCP, pois o TCP é mais complexo e tem latências mais longas. Porém, quando a integridade dos dados a serem transferidos é mais importante do que o desempenho, o TCP é preferível ao UDP e, portanto, no nosso aplicativo de compartilhamento de arquivos, iremos utilizar o TCP/IP, pois ele garante que nosso arquivo não se corrompa enquanto está sendo transferido, e se durante o processo de transmissão um pacote for perdido, ele será retransmitido, tendo dessa forma, a integridade do arquivo mantida.

Para colocar em prática a teoria, vou criar dois projetos: TCPServidor e TCPCliente. Como não vou usar threads, vamos precisar executar cada projeto separadamente. O projeto TCPServidor deverá ser executado primeiro e, a seguir, o projeto TCPCliente.

Lembrando que o exemplo funciona para arquivos menores que 1 GB e que para testar em máquinas remotas você deve verificar a comunicação entre as máquinas e as configurações do seu firewall.

Então, vamos ao trabalho.

Criando o projeto no VS Community 2015

Abra no VS community 2015 e no menu File clique em New Project;

A seguir selecione o template Visual C# -> Windows -> Windows Forms Application e informe o nome TCPCliente e clique em OK;

Agora abra o formulário Form1.cs e inclua os seguintes controles no formulário:

  • 4 Labels
  • 3 TextBox – txtEnderecoIP, txtPortaHost, txtArquivo
  • 2 Buttons – btnProcurar , btnEnviarArquivo

Disponha os controles conforme o layout da figura abaixo:

Definindo o código do formulário

Inclua os seguintes namespaces no formulário:

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

No início do formulário declare a seguinte variável:

private static string nomeAbreviadoArquivo = "";

No evento Click do botão de comando Procurar, inclua o código abaixo:

private void btnProcurar_Click(object sender, EventArgs e)
 {
            OpenFileDialog dlg = new OpenFileDialog();
            dlg.Title = "Envio de Arquivo - Cliente";
            dlg.ShowDialog();
            txtArquivo.Text = dlg.FileName;
            nomeAbreviadoArquivo = dlg.SafeFileName;
 }

No evento Click do botão de comando Enviar Arquivo, inclua o código abaixo:

private void btnEnviarArquivo_Click(object sender, EventArgs e)
        {
            if(string.IsNullOrEmpty(txtEnderecoIP.Text) && 
                string.IsNullOrEmpty(txtPortaHost.Text) && 
                string.IsNullOrEmpty(txtArquivo.Text))
            {
                MessageBox.Show("Dados Inválidos...");
                return;
            }
            //
            string enderecoIP = txtEnderecoIP.Text;
            int porta = int.Parse(txtPortaHost.Text);
            string nomeArquivo = txtArquivo.Text;
            //
            try
            {
                Task.Factory.StartNew(() => EnviarArquivo(enderecoIP, porta, nomeArquivo, nomeAbreviadoArquivo));
                MessageBox.Show("Arquivo Enviado com sucesso");
            }
            catch(Exception ex)
            {
                MessageBox.Show("Erro : " + ex.Message);
            }
     }

Neste código obtemos os valores para o IP, porta e arquivo a ser enviado e chama o método EnviarArquivo() passando essas informações para que o arquivo seja enviado.

O código do método EnviarArquivo() é o seguinte:

public void EnviarArquivo(string IPHostRemoto, int PortaHostRemoto, string nomeCaminhoArquivo, string nomeAbreviadoArquivo)
        {
            try
            {
                if (!string.IsNullOrEmpty(IPHostRemoto))
                {
                    byte[] fileNameByte = Encoding.ASCII.GetBytes(nomeAbreviadoArquivo);
                    byte[] fileData = File.ReadAllBytes(nomeCaminhoArquivo);
                    byte[] clientData = new byte[4 + fileNameByte.Length + fileData.Length];
                    byte[] fileNameLen = BitConverter.GetBytes(fileNameByte.Length);
                    //
                    fileNameLen.CopyTo(clientData, 0);
                    fileNameByte.CopyTo(clientData, 4);
                    fileData.CopyTo(clientData, 4 + fileNameByte.Length);
                    //
                    TcpClient clientSocket = new TcpClient(IPHostRemoto, PortaHostRemoto);
                    NetworkStream networkStream = clientSocket.GetStream();
                    //
                    networkStream.Write(clientData, 0, clientData.GetLength(0));
                    networkStream.Close();
                }
            }
            catch
            {
                throw;
            }
        }

No código acima temos o código que vai enviar o arquivo selecionado.

Quando o botão Procurar for clicado um objeto OpenFileDialog será criado para abrir uma caixa de diálogo e obter o nome do arquivo a ser enviado.

Quando o botão Enviar Arquivo for clicado, o método EnviarArquivo será chamado e manipulado em paralelo para que a interface de usuário do formulário principal não fique congelado enquanto o arquivo está sendo enviado e os processadores são totalmente utilizados em um ambiente multi-core.

O método Enviar Arquivo recebe o endereço IP e número de porta do computador de destino, bem como o caminho nome do arquivo e o nome do arquivo. Tanto o nome do arquivo como o arquivo são convertidos em bytes e enviados para o computador de destino utilizando TCPClient e objeto da classe NetworkStream criados.

Simples assim.

Dessa forma, temos a aplicação cliente pronta para ser usada, falta agora a aplicação servidor para receber o arquivo enviado.

Lembrando que esse exemplo é uma das muitas abordagens que podemos fazer para enviar arquivos.

No projeto, eu criei uma classe chamada Cliente com o código abaixo onde temos o método Enviar que mostra outra forma de enviar o arquivo (fique a vontade para usar ou não esse método):

using System;
using System.Net.Sockets;
using System.Threading;
namespace TCPCliente
{
    public class Arquivos
    {
        public static void Enviar(Socket socket, byte[] buffer, int offset, int tamanho, int timeout)
        {
            //Obtém o número de milissegundos decorridos desde a inicialização do sistema.
            int iniciaContagemTick = Environment.TickCount;
            //define o número de bytes enviados
            int enviados = 0;  // quantos bytes ja foram enviados

            do
            {
                //verifica se o timeout ocorreu
                if (Environment.TickCount > iniciaContagemTick + timeout)
                    throw new Exception("Tempo esgotado.");
                try
                {
                   //envia o arquivo e computa os bytes enviados
                    enviados += socket.Send(buffer, offset + enviados, tamanho - enviados, SocketFlags.None);
                }
                catch (SocketException ex)
                {
                    if (ex.SocketErrorCode == SocketError.WouldBlock ||
                        ex.SocketErrorCode == SocketError.IOPending ||
                        ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable)
                    {
                        // o buffer do socket buffer esta cheio , aguarde e tente novamente
                        Thread.Sleep(30);
                    }
                    else
                    {
                        throw ex;  // ocorreu um erro catastrófico
                    }
                }
            } while (enviados < tamanho);
        }
    }
}

A classe está comentada e a seguir temos um trecho de código de como usar o método Enviar:

Socket socket = tcpClient.Client;
  string arquivo = "exemplo de arquivo para envio!";
  try
  { // envia o texto com timeout de 10s
     Arquivos.Enviar(socket, Encoding.UTF8.GetBytes(arquivo), 0, str.Length, 10000);
  }
  catch (Exception ex) 
  { /* ... */ }

Na próxima parte do artigo vamos criar o projeto TCPServidor.

Pegue o projeto completo aqui: TCPCliente.zip