C# -  Transferindo arquivos usando Sockets

A transferência de arquivos usando sockets na linguagem C# é um processo Cliente-Servidor com certo grau de complexidade. Podemos dividir esse processo em três fases:
  1. Estágio de preparação para o servidor e para o cliente.
  2. Estabelecer o canal de comunicação para enviar-receber dados entre o cliente e o servidor.
  3. Encerrar o canal de comunicação e liberar todos os recursos.

O que é um socket ?

Um socket pode ser entendido como uma porta de um canal de comunicação que permite a um processo executando em um computador enviar/receber mensagens para/de outro processo que pode estar sendo executado no mesmo computador ou num computador remoto.

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

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

Tipos de serviço de transporte:
  • Datagrama - transporte não orientado a conexão  e sem controle de erros ( protocolo UDP)
  • DataStream - transporte orientado a conexão com controle de erros ( protocolo TCP )

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

Para completar a transferência de dados entre o cliente e o servidor temos a seguinte sequência de ações realizadas no paradigma cliente/servidor:

Servidor Cliente
1- Cria um IP End Point(endereço IP e Porta) e um objeto Socket e então vincula o objeto socket com o IP End Point e envia-o para o modo listen para aguardar a conexão do cliente  
  2- Cria um IP End Point(endereço IP e Porta) e um objeto socket
  3- Prepara os dados do arquivo (em bytes) a ser enviado
  4- Tentar se conectar com o servidor usando socket cliente com ajuda do IP End Point
5. Recebe a solicitação do cliente e aceita. Uma vez que a conexão esteja estabelecida, o servidor cria um outro objeto soquete que irá lidar com este cliente até que todos os pedidos de conexão terminem. O Cliente será informado internamente pelo TCP sobre o sucesso de conexão.  
6. Inicia os preparativos para armazenar os bytes que chegam do cliente  
  7- Inicia o envia dos bytes dos dados sobre o socket conectado.
8- Recebe os bytes do arquivo de dados junto com o nome do arquivo e os armazena em um array de bytes  
9- Obtém o nome do arquivo a partir dos bytes de dados recebidos  
10- Abre um escritor stream binário com o nome do arquivo para armazenar os bytes de dados  
11- Após salvar o arquivo, fecha o escritor stream binário e os objetos sockets do servidor  
  12- Uma vez que a transferência esteja terminada e o servidor fechou a conexão via socket o cliente também fecha o objeto socket
13 - O programa do lado do servidor é encerrado  
  14 - O programa do lado cliente é encerrado.

Objetivo

Transferir arquivos de pequeno tamanho em um ambiente de rede usando sockets e o paradigma cliente/servidor.  Usar os recursos da classe Socket do namespace System.NET.

Recursos usados:

Criando o projeto Cliente

Abra o Visual Studio 2013 Express for Windows Desktop e clique em New Project;

Selecione a linguagem Visual C# e o template Windows Forms Application e informe o nome Socket_Cliente;

A seguir inclua no formulário form1.cs criado por padrão os seguintes controles:

A seguir disponha os controles no formulário conforme o leiaute da figura abaixo:

No menu PROJECT clique em Add Class e informe o nome FTCliente.cs e a seguir digite o código abaixo nesta classe:

using System;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;
namespace Socket_Cliente
{
    public class FTCliente
    {
        public static string mensagemCliente = "em espera";
        public static void EnviarArquivo(string nomeArquivo)
        {
            try
            {
                string strEnderecoIP = "192.168.1.11";
                IPEndPoint ipEnd_cliente = new IPEndPoint(IPAddress.Parse(strEnderecoIP), 5656);
                Socket clientSock_cliente = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
                string caminhoArquivo = "";
                nomeArquivo = nomeArquivo.Replace("\\", "/");
                while (nomeArquivo.IndexOf("/") > -1)
                {
                    caminhoArquivo += nomeArquivo.Substring(0, nomeArquivo.IndexOf("/") + 1);
                    nomeArquivo = nomeArquivo.Substring(nomeArquivo.IndexOf("/") + 1);
                }
                byte[] nomeArquivoByte = Encoding.UTF8.GetBytes(nomeArquivo);
                if (nomeArquivoByte.Length > 5000 * 1024)
                {
                    mensagemCliente = "O tamanho do arquivo é maior que 5Mb, tente um arquivo menor.";
                    return;
                }
                string caminhoCompleto = caminhoArquivo + nomeArquivo;
                byte[] fileData = File.ReadAllBytes(caminhoCompleto);
                byte[] clientData = new byte[4 + nomeArquivoByte.Length + fileData.Length];
                byte[] nomeArquivoLen = BitConverter.GetBytes(nomeArquivoByte.Length);
                nomeArquivoLen.CopyTo(clientData, 0);
                nomeArquivoByte.CopyTo(clientData, 4);
                fileData.CopyTo(clientData, 4 + nomeArquivoByte.Length);
                clientSock_cliente.Connect(ipEnd_cliente);
                clientSock_cliente.Send(clientData, 0, clientData.Length, 0);
                clientSock_cliente.Close();
                mensagemCliente = "Arquivo [" + caminhoCompleto + "] transferido.";
            }
            catch (Exception ex)
            {
               mensagemCliente = ex.Message + " " +"\nFalha, pois o Servidor não esta atendendo....";
            }
        }
    }
}

Entendendo o código:

-Definindo um endereço IP da máquina local

                string strEnderecoIP = "192.168.1.11";

- Criando um IPEndPoint com o endereço IP e uma porta

                IPEndPoint ipEnd_cliente = new IPEndPoint(IPAddress.Parse(strEnderecoIP), 5656);

- Criando um objeto Socket usando uma família de endereços e definindo o tipo de soquete como stream que oferece suporte a fluxos de bytes bidirecional

                Socket clientSock_cliente = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);

Nota: Antes de um Socket poder enviar e receber dados, ele primeiro deve ser criado usando um AddressFamily, um SocketTypee um ProtocolType. A enumeração SocketType fornece várias opções para definir o tipo de Socket que se pretende abrir.

Depois de ler o arquivo e criar um fluxo de bytes realizamos a conexão com o servidor usando o método Connect e a seguir enviamos os dados de forma assíncrona com o método Send.

                clientSock_cliente.Connect(ipEnd_cliente);
                clientSock_cliente.Send(clientData, 0, clientData.Length, 0);
                clientSock_cliente.Close();

O método Send usado envia o número de bytes ao socket conectado, a partir do deslocamento especificado e usando o SocketFlags definido.

Agora no formulário form1.cs inclua no evento Click do botão de comando btnEnviar o código abaixo:

 private void btnEnviar_Click(object sender, EventArgs e)
 {
        try
            {
                FTCliente.EnviarArquivo(txtArquivo.Text);
                lbmsgCliente.Items.Add(FTCliente.mensagemCliente);
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
 }

Este código utiliza o método EnviarArquivo() da classe FTCliente, que é um método estático, para enviar o arquivo informado na caixa de texto. Depois preenche o controle ListBox  com a mensagem retornada.

Criando o projeto Servidor

Abra o Visual Studio 2013 Express for Windows Desktop e clique em New Project;

Selecione a linguagem Visual C# e o template Windows Forms Application e informe o nome Socket_Servidor;

A seguir inclua no formulário form2.cs criado por padrão os seguintes controles:

A seguir disponha os controles no formulário conforme o leiaute da figura abaixo:

No menu PROJECT clique em Add Class e informe o nome FTServidor.cs e a seguir inclua o código abaixo nesta classe:

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;

namespace Socket_Servidor
{
    public class FTServidor
    {
        static IPEndPoint ipEnd_servidor;
        static Socket sock_Servidor;
        public static string caminhoRecepcaoArquivos = @"M:\";
        public static string mensagemServidor = "Serviço encerrado !!";

        public static void IniciarServidor()
        {
            try
            {
                string strEnderecoIP = "192.168.1.11";
                ipEnd_servidor = new IPEndPoint(IPAddress.Parse(strEnderecoIP), 5656);
                sock_Servidor = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
                sock_Servidor.Bind(ipEnd_servidor);

            }
            catch (Exception ex)
            {
                mensagemServidor = "Erro ao iniciar servidor : " + ex.Message;
                return;
            }

            try
            {
                sock_Servidor.Listen(100);
                Socket clienteSock = sock_Servidor.Accept();
                clienteSock.ReceiveBufferSize = 16384;

                byte[] dadosCliente = new byte[1024 * 50000];

                int tamanhoBytesRecebidos = clienteSock.Receive(dadosCliente, dadosCliente.Length, 0);
                int tamnhoNomeArquivo = BitConverter.ToInt32(dadosCliente, 0);
                string nomeArquivo = Encoding.UTF8.GetString(dadosCliente, 4, tamnhoNomeArquivo);

                BinaryWriter bWrite = new BinaryWriter(File.Open(caminhoRecepcaoArquivos + nomeArquivo, FileMode.Append));
                bWrite.Write(dadosCliente, 4 + tamnhoNomeArquivo, tamanhoBytesRecebidos - 4 - tamnhoNomeArquivo);

                while (tamanhoBytesRecebidos > 0)
                {
                    tamanhoBytesRecebidos = clienteSock.Receive(dadosCliente, dadosCliente.Length, 0);
                    if (tamanhoBytesRecebidos == 0)
                    {
                        bWrite.Close();
                    }
                    else
                    {
                        bWrite.Write(dadosCliente, 0, tamanhoBytesRecebidos);
                    }
                }

                bWrite.Close();

                clienteSock.Close();
                mensagemServidor = "Arquivo recebido e arquivado [" + nomeArquivo + "] (" + (tamanhoBytesRecebidos - 4 - tamnhoNomeArquivo) +
" bytes recebido); Servidor Parado";
            }
            catch (SocketException ex)
            {
                mensagemServidor = ex.Message +  " - Erro ao receber arquivo.";
            }
        }
    }
}

Entendendo o código:

- Define o local onde os arquivos serão recebidos e salvos

                 public static string caminhoRecepcaoArquivos = @"M:\";

- Define o IP do servidor

                string strEnderecoIP = "192.168.1.11";

-
Cria um IPEndPoint usando o IP , e a porta onde o servidor estará escutando:

                ipEnd_servidor = new IPEndPoint(IPAddress.Parse(strEnderecoIP), 5656);

- Cria um objeto Socket usando uma família de endereços e definindo o tipo de soquete como stream que oferece suporte a fluxos de bytes bidirecional

                 sock_Servidor = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);

- Associa o socket ao EndPoint criado. Devemos usar um Bind antes de chamar o Listen.

                sock_Servidor.Bind(ipEnd_servidor);

- Coloca o socket em estado de escuta a espera da requisição do cliente indicando o número de conexões de entrada que podem ser colocadas na fila como igual a 100

                sock_Servidor.Listen(100);

- Cria o novo socket para a conexão ativa a partir da conexões que estão na fila

                Socket clienteSock = sock_Servidor.Accept();

- Define o tamanho do buffer de recebimento do socket

                clienteSock.ReceiveBufferSize = 16384;

- Recebe o número de bytes especificado de dados de Socket vinculado em um buffer de recebimento, usando SocketFlags definido.

                  int tamanhoBytesRecebidos = clienteSock.Receive(dadosCliente, dadosCliente.Length, 0);

Nota: Lembre-se que os métodos Send e Receive da classe Sockect pela definição não são obrigados a realmente enviar/receber todos os dados que você forneceu.
O valores estão limitados ao buffer internos do sistema que estão relacionados com a placa de rede.

O tamanho limite do buffer interno ou o tamanho máximo de um pacote TCP também afetam os valores enviados e recebidos. (O limite é 8KB por processo)

Por isso para ter certeza que os dados foram totalmente enviados/recebidos é recomendado usar laço :

int bytesJaEnviados = arr.Length ;
int bytesEnviados = 0 ;
while ( bytesEnviados < bytesJaEnviados )
    bytesEnviados + = Socket.Send ( arr, bytesEnviados , bytesAEnviar - bytesJaEnviados , .... ); .

No formulário form1.cs vamos também declarar os namespaces usados no projeto:

using System;
using System.Windows.Forms;

A seguir no evento Click do botão de comando btnIniciarServidor inclua o código a seguir:

 private void btnIniciarServidor_Click(object sender, EventArgs e)
  {
            FTServidor.IniciarServidor();
            txtStatus.Text = FTServidor.mensagemServidor;
 }

O código acima usa o método estático IniciarServidor da classe FTServidor para colocar o servidor em atendimento e a seguir exibe a mensagem na caixa de texto.

1,2,3 Testando...

Para testar temos que fazer os seguintes procedimentos:

  1. Executar o projeto Socket_Servidor  e  clicar no botão para colocar o servidor em atendimento
  2. A seguir executar o projeto Socket_Cliente e clicar no botão para enviar o arquivo

Ao final desses procedimento devemos obter o seguinte resultado:

Atenção:

Os projetos mostrados neste artigo são bem simples e tem como objetivo mostrar como usar a classe Socket.

Para criar um projeto mais robusto você devera ajustar e melhorar os exemplos para permitir a transferência de vários arquivos e de arquivos com tamanho maior que 5 MB.

Pegue os projetos completos aqui :    Socket_Servidor.zip e Socket_Cliente.zip

Filipenses 2:5 Tende em vós aquele sentimento que houve também em Cristo Jesus,

Filipenses 2:6 o qual, subsistindo em forma de Deus, não considerou o ser igual a Deus coisa a que se devia aferrar,

Filipenses 2:7 mas esvaziou-se a si mesmo, tomando a forma de servo, tornando-se semelhante aos homens;

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

 

             Gostou ?   Compartilhe no Facebook   Compartilhe no Twitter
 

Referências:


José Carlos Macoratti