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