Prévia do material em texto
TRANSFERÊNCIA E SINCRONIA DE PASTAS E ARQUIVOS EM REDE LOCAL SEM NÓ CENTRALIZADOR Gustavo de Oliveira Silva Projeto de Graduação apresentado ao Curso de Engenharia Eletrônica e de Computação da Escola Politécnica, Universidade Federal do Rio de Janeiro, como parte dos requisitos necessários à obtenção do título de Engenheiro. Orientador: Aloysio de Castro Pinto Pedroza, D.Sc. Rio de Janeiro Fevereiro de 2017 iv Silva, Gustavo de Oliveira Transferência e Sincronia de Pastas e Arquivos em Rede Local sem Nó Centralizador / Gustavo de Oliveira Silva – Rio de Janeiro: UFRJ / Escola Politécnica, 2017. xi, 80 p.: il.; 29,7 cm. Orientador: Aloysio de Castro Pinto Pedroza, D.Sc. Projeto de Graduação – UFRJ/Escola Politécnica/ Curso de Engenharia de Eletrônica e de Computação, 2017. Referências Bibliográficas: p. 75-76. 1. Transferência de Arquivos. 2. Rede Local. 3. Sincronia de Pastas e Arquivos. 4. Identificação Única. I. Pedroza, Aloysio de Castro Pinto. II. Universidade Federal do Rio de Janeiro, Escola Politécnica, Curso de Engenharia de Eletrônica e de Computação. III. Transferência e Sincronia de Pastas e Arquivos em Rede Local sem Nó Centralizador. v UNIVERSIDADE FEDERAL DO RIO DE JANEIRO Escola Politécnica – Departamento de Eletrônica e de Computação Centro de Tecnologia, bloco H, sala H-217, Cidade Universitária Rio de Janeiro – RJ CEP 21949-900 Este exemplar é de propriedade da Universidade Federal do Rio de Janeiro, que poderá incluí-lo em base de dados, armazenar em computador, microfilmar ou adotar qualquer forma de arquivamento. É permitida a menção, reprodução parcial ou integral e a transmissão entre bibliotecas deste trabalho, sem modificação de seu texto, em qualquer meio que esteja ou venha a ser fixado, para pesquisa acadêmica, comentários e citações, desde que sem finalidade comercial e que seja feita a referência bibliográfica completa. Os conceitos expressos neste trabalho são de responsabilidade do(s) autor(es). vi AGRADECIMENTOS À Microsoft, pois sem ela a essência desse projeto não existiria. A meus familiares que sempre me apoiaram e me deram forças para seguir em frente. A meus amigos que me apoiaram e ajudaram com dicas e ideias em alguns momentos. A todas as pessoas que aceitaram o papel de cobaia para realizar alguns testes. E a mim mesmo, por encarado o problema e realizado a solução. vii Resumo do Projeto de Graduação apresentado à Escola Politécnica/UFRJ como parte dos requisitos necessários para a obtenção do grau de Engenheiro de Eletrônica e de Computação. Transferência e Sincronia de Pastas e Arquivos em Rede Local sem Nó Centralizador Gustavo de Oliveira Silva Fevereiro/2017 Orientador: Aloysio de Castro Pinto Pedroza, D.Sc. Curso: Engenharia Eletrônica e de Computação Este trabalho corresponde a um programa desenvolvido para transferir e/ou sincronizar pastas e arquivos entre dois computadores conectados em rede local, incluindo um método para identificação única dos computadores para remover a necessidade de um nó central, que contenha a informação de cada computador, para isso, um computador deve formar um vínculo com outro para que possa realizar o processo de sincronia. Palavras Chaves: Transferência, Sincronia, Rede Local, Identificação Única. viii Abstract of Undergraduate Project presented to POLI/UFRJ as a partial fulfillment of the requirements for the degree of Engineer. Transfer and Synchronization of Files and Folders in Local Area Network without Central Node Gustavo de Oliveira Silva February/2017 Advisor: Aloysio de Castro Pinto Pedroza, D.Sc. Course: Electronics and Computing Engineering This work corresponds to a program developed to transfer and/or synchronize folders and files between two computers connected in local area network, including a method for unique identification of the computers to remove the need of a central node, that contains the information of each computer, therefore, one computer must form a link with another so that it can perform the synchronization process. Keywords: Transfer, Synchronization, Local Area Network, Unique Identification. ix SUMÁRIO 1. Introdução ................................................................................... 1 1.1. Motivação e Justificativa ....................................................................... 1 1.2. Objetivo ................................................................................................. 2 1.3. Organização e Abordagem ..................................................................... 2 2. Conceitos ..................................................................................... 4 2.1. Comunicação entre Computadores ........................................................ 4 2.1.1. TCP – Transmission Control Protocol ........................................ 4 2.1.2. UDP – User Datagram Protocol .................................................. 5 2.2. Serialização de Estruturas ..................................................................... 6 2.2.1. Binário ......................................................................................... 6 2.2.2. Texto ........................................................................................... 7 2.3. Criptografia ........................................................................................... 7 2.4. Reconhecimento na Rede ....................................................................... 8 2.5. Bibliotecas ............................................................................................. 9 2.6. Comentários ......................................................................................... 10 3. Cross-Compile ............................................................................ 11 3.1. Socket ................................................................................................... 11 3.1.1. Interface simples ......................................................................... 12 3.1.2. Interface completa ...................................................................... 12 3.2. Interface de Rede .................................................................................. 13 3.3. Operações com Arquivos ...................................................................... 13 3.4. Observações .......................................................................................... 14 4. Módulos Essenciais ..................................................................... 15 4.1. ByteStream ........................................................................................... 15 4.1.1. Em memória ............................................................................... 16 4.1.2. Em arquivo ................................................................................. 16 4.1.3. Em arquivo com escrita assíncrona ............................................. 17 4.2. DataStreamer ....................................................................................... 17 4.3. DirTree ................................................................................................ 19 4.3.1. Serialização em JSON ................................................................. 20 4.3.2. Adição de Nó .............................................................................. 21 4.3.3. Remoção de Nó ........................................................................... 22 4.3.4. Atualização de Árvore ................................................................ 22 4.3.5. Diferença entre Árvores .............................................................. 23 4.3.6. União de Árvores ........................................................................24 x 4.4. Tarefas em Segundo Plano ................................................................... 26 4.5. Conclusão ............................................................................................. 28 5. Conectividade ............................................................................. 29 5.1. Mensagens de Conectividade ................................................................ 29 5.2. Módulo do Núcleo de Conectividade .................................................... 32 5.3. Módulo de Notificação de Conectividade .............................................. 34 5.4. Considerações ....................................................................................... 36 6. Relacionamentos......................................................................... 37 6.1. Amigo ................................................................................................... 37 6.2. Módulo do Núcleo de Relacionamentos ................................................ 38 6.3. Módulo de Formação de Amizade ........................................................ 39 6.4. Módulo de Procura de Peer .................................................................. 42 6.5. Módulo de Validação de Amizade ........................................................ 43 6.6. Observações .......................................................................................... 44 7. Transferência de Arquivos ......................................................... 45 7.1. O Canal e suas Características ............................................................. 45 7.2. Estabelecendo a Conexão entre os peers ............................................... 47 7.3. Transferindo Arquivos .......................................................................... 49 7.4. Finalizando o Processo e Conclusões .................................................... 50 8. Sincronia de Pastas e Arquivos .................................................. 51 8.1. O Canal e suas Características ............................................................. 51 8.2. A Detecção de Diferenças ..................................................................... 52 8.3. Estabelecendo Conexão entre os Peers ................................................. 53 8.4. Operações do modo Cliente .................................................................. 55 8.5. Operações do modo Servidor ................................................................ 56 9. Experiências e Resultados .......................................................... 59 9.1. Reconhecimento de amizade ................................................................. 59 9.1.1. Objetivo ...................................................................................... 59 9.1.2. Equipamentos ............................................................................. 59 9.1.3. Preparações................................................................................. 59 9.1.4. Procedimentos ............................................................................ 60 9.1.5. Resultado .................................................................................... 60 9.1.6. Análise dos Resultados ............................................................... 61 9.1.7. Conclusão ................................................................................... 62 9.2. Sincronia de Pastas e Arquivos ............................................................ 62 9.2.1. Objetivo ...................................................................................... 62 xi 9.2.2. Equipamentos ............................................................................. 62 9.2.3. Preparações................................................................................. 62 9.2.4. Procedimentos ............................................................................ 62 9.2.5. Resultados .................................................................................. 63 9.2.6. Análise dos Resultados ............................................................... 65 9.2.7. Conclusão ................................................................................... 66 9.3. Taxas de transferência .......................................................................... 67 9.3.1. Objetivo ...................................................................................... 67 9.3.2. Equipamentos ............................................................................. 67 9.3.3. Preparações................................................................................. 67 9.3.4. Procedimentos ............................................................................ 67 9.3.5. Resultados .................................................................................. 68 9.3.6. Análise dos Resultados ............................................................... 68 9.3.7. Conclusões .................................................................................. 71 10. Conclusão ................................................................................... 72 10.1. Projetos Futuros ............................................................................... 73 Referências bibliográficas ................................................................... 75 Apêndice A ........................................................................................ 77 Apêndice B ........................................................................................ 80 1 CAPÍTULO 1 1. INTRODUÇÃO No Windows 7, o mesmo programa que permite visualizar arquivos e pastas, tanto locais quanto compartilhados na rede local, também permite copiá-los. Entretanto, essas cópias, quando realizadas entre computadores na rede, não apresentam taxas de transferência satisfatórias, pois outras aplicações conseguem obter taxas maiores. Existem empresas que oferecem serviço de armazenamento na nuvem. Tais empresas costumam fornecer um programa que permite sincronizar uma determinada pasta (configurável), em seu computador, com uma determinada pasta (também configurável) no armazenamento na nuvem. Diferente desses serviços, este projeto não possui um servidor central, que contém todos os arquivos, informações e configurações de seus clientes, e também não acessa a Internet para nada, tudo é realizado dentro da rede local do usuário, permitindo taxas de transferência altas e comumente maiores que a taxa de transferência que esse mesmo usuário possui na Internet. Este projeto não apenas realiza uma simples transferência de arquivos entre dois computadores, mas também é capaz de realizar sincronia de pastas entre dois computadores. Outra característica do projeto é que o mesmo não é exclusivo para Windows, ele também foi feito para ser executado em um sistema operacional baseado em Linux. O Ubuntu 14.06 e suas versões seguintes foram utilizados no desenvolvimento e testes do projeto. 1.1. MOTIVAÇÃO E JUSTIFICATIVA A cópia de pastas e arquivos no Windows usando a ferramenta de compartilhamento de pastas e arquivos nativa é realizada através do Windows Explorer, outra ferramenta nativa do sistema operacional para explorar as pastas do disco. Ou seja, da mesma forma que o usuário realiza operações nas 2 pastas e arquivos em seu computador, ele também pode realizar tais operações envolvendo as pastas e arquivos de outro computador na rede. Infelizmente é comum a obtenção de baixa taxa de transferência durante transferências realizadas entre dois computadores com Windows 7 dentro na mesma rede local, tal taxa é, inclusive, inferior à taxa de transferência máxima (e comum) que era obtida, na mesma rede, ao realizar o download de um arquivo da Internet, e com isso surgiu uma desconfiança de que esse problema pudesse ser exclusivoda forma como a ferramenta de compartilhamento de pastas e arquivos do Windows realiza essas operações. 1.2. OBJETIVO De um modo geral, realizar transferência de arquivos entre dois computadores com Windows em uma rede local, obtendo taxas de transferências altas (em alguns casos atingindo a velocidade máxima da rede), ou seja, no mínimo igual à taxa de transferência obtida ao se realizar o download de um arquivo da Internet (considerando que a velocidade da conexão com a Internet é inferior à velocidade máxima da rede local), é o principal objetivo deste projeto. O projeto também é capaz de realizar sincronia de pastas e arquivos entre dois computadores em rede local. Outra característica deste projeto é a não necessidade de acesso à Internet e a não necessidade de um servidor para centralizar as informações e configurações dos clientes. 1.3. ORGANIZAÇÃO E ABORDAGEM Este trabalho possui 8 capítulos, além desta introdução e da conclusão ao final. No capítulo 2 serão apresentados os conceitos nos quais o projeto se baseia. No capítulo 3 serão apresentadas algumas particularidades existentes entre as APIs (Application Programming Interface) do Windows e Linux. No capítulo 4 serão abordados os módulos essenciais do projeto, que realizam algumas das primordiais tarefas do mesmo. No capítulo 5 serão apresentadas as formas como o projeto realiza a comunicação entre computadores na rede. 3 No capítulo 6 será apresentada a forma como o projeto realiza o vínculo entre os computadores. No capítulo 7 está descrito o funcionamento da transferência de arquivos. No capítulo 8 o funcionamento da sincronia de pastas e arquivo é descrito. O capítulo 9 contém as experiências e seus resultados. 4 CAPÍTULO 2 2. CONCEITOS O projeto foi criado usando a linguagem de programação C++ seguindo o padrão C++11 [1] [2]. Para algumas funções, algumas bibliotecas foram utilizadas, porém nenhuma dessas funções realiza o objetivo deste projeto, mas elas são essenciais em determinadas partes. Adiante essas bibliotecas serão apresentadas e quais recursos delas foram utilizados. O projeto foi compilado usando o GNU Compiler Collection (GCC) [3] nas versões 5.1, 5.3 e 6.1. Para compilar para o Windows foi utilizado o Mingw- w64 [4] que é o GCC para Windows, com suporte ao Windows de 64 bits. 2.1. COMUNICAÇÃO ENTRE COMPUTADORES Este projeto foi planejado e concebido na camada de aplicação do modelo TCP/IP (Transmission Control Protocol/Internet Protocol) [5] [6]. Considerado como pontos-finais (endpoint) de uma comunicação dentro de uma rede, o socket é uma API que oferece acesso a funções, implementadas pelo sistema operacional, que operam nas camadas mais baixas do modelo TCP/IP. É necessário configurar o socket sobre a camada de transporte (Transmission Control Protocol ou User Datagram Protocol) e a camada de Internet (Internet Protocol versão 4, que é usado nesse projeto), antes de realmente usar o socket. Cada protocolo possui suas próprias características. A seguir serão destacadas as principais características que foram consideradas para a escolha do protocolo. 2.1.1. TCP – Transmission Control Protocol É uma conexão de dois pontos apenas e é uma conexão garantida, ou seja, uma vez estabelecida, há certeza de que está conectado ou não. A Figura 1 demonstra o diagrama de fluxo de operações que devem ser realizadas para 5 estabelecer uma conexão entre dois sockets configurados para usar o protocolo TCP. Há garantia de entrega e mantém a ordem dos pacotes, o que é essencial para transmissão de dados maiores que o tamanho do pacote TCP. Figura 1 – Diagrama de conexão TCP 2.1.2. UDP – User Datagram Protocol Este protocolo não realiza uma conexão entre dois pontos. Nesse projeto esse protocolo é usado exatamente para comunicações que não necessitam conectar diretamente ao outro ponto, que é o caso do momento de descoberta de outros computadores na rede através de broadcast. Os pacotes transmitido e recebido são exatamente iguais. Isso faz com que ao enviar uma mensagem por UDP, a mensagem estará inteira dentro do pacote transmitido. Ao contrário do TCP, onde o envio de uma mensagem pode ser fragmentado em mais de um pacote. O UDP não garante a entrega do pacote e não garante a ordem de entrega dos mesmos, por isso foi implementado a confirmação de entrega para as mensagens que necessitam dessa confirmação. Socket() bind() listen() accept() accept() Socket() Aguarda até haver conexão Servidor Cliente Conexão estabelecida 6 2.2. SERIALIZAÇÃO DE ESTRUTURAS A fim de armazenar ou transmitir uma estrutura que se encontra em memória, torna-se necessário a serialização da estrutura. Este processo consiste em armazenar todos os tipos e valores de uma estrutura, em uma sequência de bytes, de forma que seja possível recuperar a mesma estrutura a partir da mesma sequência de bytes, ou seja, é uma operação reversível. Embora possa parecer simples, deve-se tomar cuidado ao serializar uma estrutura. Referência de memória não pode ser serializada, pois a posição de memória pode variar (e geralmente varia) entre dois processos diferentes, mesmo sendo o mesmo programa. Para esse caso, todos os dados da referência também devem ser serializados. O resultado da serialização pode conter apenas caracteres imprimíveis, chamado de serialização em forma de texto, ou ser um stream binário. 2.2.1. Binário A mensagem serializada é feita de forma binária, como uma cópia de memória. Para strings não há muita diferença, mas para números (inteiro e ponto flutuante) o resultado não é humanamente legível. Neste projeto a serialização de strings é precedida de um número inteiro sem sinal indicando o tamanho da string, isso remove o caractere nulo do final da string do padrão C. O número inteiro que representa o tamanho da string, ao ser serializado, pode ocupar 1, 2, 4 ou 8 bytes. Essa quantidade de bytes deve ser indicada tanto ao serializar a string quanto ao desserializar. A Figura 2 mostra um exemplo de mensagem de identificação de host usado pelo projeto. Figura 2 – Mensagem de MyCredential Outra observação a se fazer com a serialização de números é devido ao endianess, que é a ordem com que os bytes estão arranjados em memória, para tipos de dados com mais de um byte. É chamado de big-endian quando o primeiro byte de memória corresponde ao byte de maior peso. É chamado de 7 little-endian quando o primeiro byte de memória corresponde ao byte de menor peso. A Figura 3 ilustra um determinado valor armazenado em memória em big- endian e little-endian. Neste projeto, todos os números são serializados em little-endian, existem funções que garantem isso. Figura 3 – Ilustração de um valor armazenado em little-endian e big-endian 2.2.2. Texto Nesse modo a mensagem serializada possui maior facilidade para compreensão humana. Existem diferentes métodos de serialização em modo texto. Para este projeto foi escolhido o JSON (JavaScript Object Notation) [7], devido à facilidade em depurar, pois pode-se usar depuradores de JavaScript, e por ser compacto, pois em alguns casos a estrutura serializada em JSON será armazenada em banco de dados ou transmitida pela rede. Foi utilizado o ASCII85, mais precisamente o ZeroMQ85, como forma de codificar os números inteiros, mesmo utilizando JSON. Isso se deve ao caso, ilustrado na Tabela 1, onde números muito grandes ocupam muito espaço ao serem escritos por extenso, enquanto que ao codificar sempre ocuparão 5 bytes (quando codifica um inteiro de 32 bits) ou 10 bytes (quando codifica um inteiro de 64 bits). Número: 578 1298230816 Hexadecimal: 00 00 02 42 4D 61 6E 20 ZeroMQ85: 0006! o<}]Z Base 64: AAACQg== TWFuIA== Tabela 1 – Comparação de codificação 2.3. CRIPTOGRAFIA Este projeto utilizaAES (Advanced Encryption Standard) para realizar o handshake necessário para realizar sincronia e algumas transferências. Esse little-endian big-endian Número representado: 104105364 8 handshake é uma forma de autenticar o vínculo formado entre dois computadores, conforme será explicado no capítulo 6. 2.4. RECONHECIMENTO NA REDE Neste projeto existem duas formas de identificar um computador, ou seja, pelo nome do computador (que pode ser modificado) e por uma identificação única, que permanece transparente ao usuário. Uma das dificuldades foi definir essa identificação única, pois deveria ter que garantir a unicidade do computador perante qualquer rede. A melhor opção de identificação única é o número de série de algum componente de hardware do computador. Por exemplo, da CPU, mas essa operação não existe nos processadores modernos devido a preocupações de privacidade na época do Pentium III. Devido à complexidade em obter o número de série de outros componentes de hardware em cada sistema operacional, foi adotado o MAC Address (Media Access Control Address) de alguma interface de rede do computador por apresentar uma fácil forma de obtenção. Embora não haja garantia de unicidade de MAC Address nas interfaces de rede (além da possibilidade de modificar o MAC Address de algumas placas), não há como existir dois ou mais computadores com o mesmo MAC Address em uma rede local. Na primeira execução do projeto, o MAC Address é obtido e armazenado como sendo a identificação única. Nas demais execuções o projeto não inicia caso a identificação única não seja o MAC Address de uma das placas de rede do computador. É definido como “amizade” o vínculo estabelecido entre dois computadores, os quais são chamados de “amigo”. Esse vínculo é criado por meio da execução da operação de formação de amizade do projeto e, durante essa operação, duas chaves são negociadas. Essas chaves são utilizadas para o handshake e para o possível futuro uso de criptografia em outras operações. Os computadores se identificam na rede por meio de broadcast. Um computador envia um broadcast informando quem ele é, e apenas os “amigos” 9 dele que estiverem na rede nesse momento é que respondem, em unicast, quem são. 2.5. BIBLIOTECAS Segue a lista de bibliotecas usadas no projeto. • DIG-Logger [8] Copyright © 2015-2016 Gustavo de Oliveira Silva. All rights reserved. Biblioteca de mesma autoria que a deste projeto, para facilitar a exibição e/ou armazenamento do log gerado no projeto. • LibJSON [9] Copyright © 2010 Jonathan Wallace. All rights reserved. Biblioteca que realiza a codificação e decodificação em JSON. • PolarSSL (mbed TLS) [10] Copyright © 2006-2015, ARM Limited, All Rights Reserved. Biblioteca usada para criptografia, apenas as funções para operação do AES e cálculo de MD5 (Message-Digest algorithm 5) foram usadas. • SQLite [11] SQLite is in the Public Domain. Biblioteca que implementa o banco de dados usado no projeto para armazenar as informações e configurações do computador. • Z85 [12] Copyright © 2013 Stanislav Artemkin. All rights reserved. Biblioteca que realiza a codificação e decodificação em ZeroMQ Base-85. O código dessa biblioteca foi anexado ao código do projeto e modificado, pois acrescentava um byte desnecessário ao resultado da codificação. 10 2.6. COMENTÁRIOS Neste capítulo foi feito uma breve apresentação de alguns conceitos que foram levados em consideração no desenvolvimento do projeto. Conforme esses conceitos forem sendo usados na implementação, argumentado nas seções seguintes, uma melhor explicação será apresentada. Como foi de interesse do autor que este projeto funcionasse também no sistema operacional Linux, algumas funções e estruturas foram criados para manter o código independente da plataforma, e elas serão melhores abordadas no próximo capítulo. 11 CAPÍTULO 3 3. CROSS-COMPILE A essência deste projeto é independente do sistema operacional, porém a implementação de alguns recursos usados pelo projeto são diferentes entre alguns sistemas operacionais, como é o caso da manipulação de arquivos e o acesso às funções nativas do sistema operacional. O compilador Mingw-w64 possui a implementação de algumas funções da API POSIX para o Windows, entretanto, algumas funções e macros variam nos dois sistemas operacionais. Portanto, as seguintes implementações foram feitas de forma a criar uma abstração maior para o código principal do projeto. 3.1. SOCKET As funções e algumas macros usadas pela API do Windows são idênticas às macros usadas na API POSIX, mas, devido à existência de macros diferentes, foi necessária a criação de uma interface que isolasse, do código principal, o código da implementação dependente do sistema operacional. Além disso, na API do Windows é necessário executar uma função para inicializar o uso de sockets pelo programa e, também na API do Windows, a função para obter o código de erro do socket é diferente da usada na API POSIX. Essa interface possuiu duas finalidades, oferecer compatibilidade entre sistemas operacionais, ao mesmo tempo em que isola as dependências do sistema operacional do resto do código, e oferecer simplicidade de uso por parte da programação. A implementação dessa última finalidade possui um efeito prejudicial, conforme será explicado adiante, e por isso outra implementação foi feita. 12 3.1.1. Interface simples Possui apenas cinco funções. Uma para definir as configurações do socket, uma para inicializar o socket, uma para finalizar o socket, uma para enviar um pacote e uma para receber um pacote. É completamente funcional, mas tornou-se bastante complexa por ter que oferecer suporte ao TCP e ao UDP, ao mesmo tempo em que oferece controles simples. A função de receber pacote possui buffer interno, o que reduz a eficiência do código, uma vez que o mesmo dado é copiado duas vezes, e esse buffer possui tamanho fixo, que facilmente é contornado ao colocar um buffer maior que o tamanho máximo de um pacote (64KB). Mesmo com esses problemas, esse socket foi mantido para manter compatibilidade com a parte do código até então implementada e que não sofria perda significativa, que é o caso da comunicação via UDP realizada pelo núcleo de conexão do projeto. 3.1.2. Interface completa É assim chamada por ter as mesmas funções que a API POSIX para operações com socket: create(), destroy(), bind(), setsockopt(), listen(), connect(), accept(), close(), shutdown(), send(), sendto(), select(), recv() e recvfrom(). Essas funções e seus argumentos são iguais às da API POSIX que foram obtidas através do manual do Linux, com exceção de um grande fator, ao implementar essa interface em C++, o socket passou a ser uma classe, portanto, enquanto as funções da API POSIX requerem o socket em um de seus argumentos, já na interface isso é omitido devido à orientação a objeto. As funções de shutdown e select também possuem argumentos diferentes, pelo mesmo motivo citado acima. Essa implementação de interface de socket oferece completo controle sobre o socket, que poderia ser realizado usando as funções nativas de socket. Com isso, o problema do buffer que ocorria no modelo anterior não ocorre mais, o que é essencial para um grande fluxo de dados. Outra característica é que esta interface permite receber uma quantidade específica de bytes, e isso resolveu outro problema que ocorre ao utilizar a outra interface com o protocolo TCP. 13 No TCP, ao executar a função send duas vezes, cada vez com dois bytes, é possível que o socket que receba as mensagens, receba os quatro bytes em uma única execução da função de recebimento. E isso trazia problemas para as mensagens de controle que são transmitidas pelo socket, pois acabavam sendo recebidas duas mensagens e eram tratadas como uma. Para evitar o usode mais um buffer, e ao mesmo tempo aproveitar do buffer do próprio socket, as comunicações em TCP são feitas todas usando essa nova interface. 3.2. INTERFACE DE REDE Um módulo foi criado para obter todas as interfaces de rede do computador que, por estar nesse capítulo, possui implementação diferente para cada sistema operacional, porém, em ambas as implementações, é obtido o nome da interface de rede, a máscara de rede, o IP atribuído, o IP de broadcast e o MAC Address da interface. Este módulo inicia uma tarefa assíncrona dentro do próprio escopo do programa principal que, de tempos em tempos, verifica as interfaces de rede disponíveis, cria e disponibiliza uma lista com essas interfaces, além de uma lista exclusiva contendo apenas as interfaces que estão conectadas em alguma rede. Ao contrário da primeira lista, que normalmente permanece intacta desde o início do programa, afinal, ela só é atualizada quando alguma interface de rede é adicionada ou removida do computador, o que não é muito comum, a segunda lista é modificada toda vez que alguma interface de rede se conecta ou desconecta de alguma rede. Isso é de extrema importância para o envido de broadcast na rede, pois ao enviar um broadcast no endereço 255.255.255.255 com duas interfaces de rede conectadas, o sistema operacional escolhe uma das interfaces para enviar o broadcast. Ao usar o IP de broadcast associado a uma das interfaces, é assegurado que é essa interface de rede que será usada para enviar o broadcast. 3.3. OPERAÇÕES COM ARQUIVOS A linguagem de programação C++ em seu padrão C++11 possui implementações para leitura e escrita de arquivos independente do sistema operacional. Entretanto o sistema operacional Windows apresenta uma particularidade na forma como trata a nomenclatura de pastas e arquivos. 14 A começar pelo uso da barra invertida (\), ao invés da barra (/), como nos sistemas Linux. Outro fator é que a API do Windows trata os caracteres estendidos de forma particular, usando o conceito de caractere largo (wide char). Infelizmente, no padrão C++11, a classe que opera com arquivos com suporte ao wide char requer que o conteúdo do arquivo também seja manipulado em wide char, o que é inviável pois as operações de leitura e escrita nesse formato só ocorrem em múltiplos de 2 (wide char corresponde a 2 bytes), logo arquivos binários com tamanho ímpar não poderiam ser lidos ou gravados completamente. Uma solução encontrada inicialmente foi o uso de uma biblioteca chamada nowide, porém foi constatado uma taxa de escrita muito pequena em relação ao uso da biblioteca padrão do C++11, o que tornou inviável o uso da biblioteca devido à necessidade de alta taxa de leitura e gravação de arquivos. Por isso, este projeto possui atual restrição de não realizar operações com arquivos e pastas com caracteres estendidos em seus nomes. 3.4. OBSERVAÇÕES Todas as strings dentro do programa são tratadas com codificação UTF-8 (8-bit Unicode Transformation Format), evitando, assim, problemas com o wide char presente na API do Windows e, na maioria das vezes, o ambiente Linux usa UTF-8 para as nomenclatura dos arquivos. O mesmo ocorre com o uso da barra (/) ao invés da barra invertida (\) dentro do programa, mas para as operações de escrita e leitura pela API do Windows é feito a conversão para a barra invertida. Outra particularidade envolvendo arquivos e o Windows será abordada no capítulo seguinte, onde alguns módulos essenciais serão apresentados e explicados. 15 CAPÍTULO 4 4. MÓDULOS ESSENCIAIS Dentre os módulos criados, aqueles aqui apresentados são necessários e úteis a cumprir o objetivo no qual foram baseados suas funcionalidades. Eles podem ser entendidos como módulos primitivos, que exercem funções básicas para as demais operações. Tais módulos não podem ser removidos e também receberam maior atenção em sua implementação para serem confiáveis. 4.1. BYTESTREAM Módulo que realiza operações com um array de bytes, similar à implementação da string do padrão C++ (std::string), porém essa segunda não foi adotada pois uma string em C é caracterizada como uma sequência de caracteres até o primeiro caractere nulo (\0) e, como o módulo deveria operar com arquivos binários também, essa restrição não deve existir. Este módulo possui esse nome, pois também se assemelha à classe de arquivos do C++ (std::fstream), possuindo funções de escrita, leitura e cursor. Há apenas uma função de escrita (append) e ela pode receber quatro tipos diferentes de estruturas: um caractere, um array de caracteres, junto com o tamanho do mesmo, um std::string ou um ByteStream. Essa função copia o conteúdo da estrutura, passada no argumento, no objeto atual com início na posição indicada pelo cursor e move o cursor para até onde o conteúdo foi copiado. Caso o cursor seja movido para depois do valor que marca o tamanho do array, esse valor é atualizado para o valor do cursor. A função de leitura (getStream) recebe, como argumento, a posição de início da leitura e a quantidade de bytes que se deseja obter. E retorna um ponteiro que aponta, em memória, para o início da sequência de bytes e contém, no mínimo, a quantidade de bytes informada no argumento. É garantido que o 16 ponteiro retornado por essa função seja válido até que outra função seja chamada nesse mesmo objeto. Há também a função read que recebe, como argumento, um ponteiro para uma sequência de bytes, que já deve estar alocado, e a quantidade de bytes que se deseja ler. Nessa função o início da leitura de dados é passado pelo cursor do módulo, e incrementado pela quantidade de bytes lidos, e o conteúdo é copiado para dentro da região do ponteiro passado pelo argumento. Em caso de erro de leitura a função retorna falso. Conforme pode ser concluído das explicações anteriores, o cursor é usado para posicionar o início da leitura e/ou escrita de dados e pode ser movido livremente ao longo do array de bytes. O cursor não é usado na função de leitura getStream pois esta é otimizada para operações com alta taxa de leitura, pois reduz a quantidade de cópia de memória. Esse módulo foi implementado de três formas diferentes para operar com áreas diferentes e também fornecer otimização em uma das implementações. 4.1.1. Em memória Essa implementação armazena todos os bytes apenas na memória do computador. A função append possui recurso de automaticamente ajustar o tamanho do buffer interno conforme for necessário mais espaço. É uma funcionalidade de grande utilidade, mas que deve ser evitada em larga escala, por isso a função abaixo deve ser usada na maioria dos casos. A função realloc tem como finalidade reservar um espaço em memória maior ou igual ao tamanho informado no argumento da função. Quando o tamanho do buffer interno é maior ou igual ao tamanho desejado na execução da função, o buffer não é modificado. Caso o cursor esteja posicionado acima do tamanho desejado, ele é movido para a nova posição final. 4.1.2. Em arquivo Essa implementação realiza operações diretamente sobre a classe std::fstream, inclusive as operações com o cursor. Existe um buffer interno que é compartilhado para a função de leitura getStream e para a função de escrita append, portanto, não é recomendável usar a mesma instância para leitura e escrita. 17 O buffer é similar à implementação em memória, porém apenas a função de leitura é que possui a capacidade de expandir automaticamente o buffer. Em contrapartida, existe uma função exclusiva que pode ser usada para definir o tamanho do buffer. É utilizada no DataStreamer (que será explicado mais a frente) pois ler um trecho de um arquivo em disco com várias pequenas leituras consome mais tempo do que ler o mesmo trecho do mesmo arquivo em disco de uma única vez. 4.1.3. Em arquivo com escrita assíncrona Essa é uma especializaçãodo módulo de arquivo para realizar escrita em arquivo de forma assíncrona. Usando o módulo de arquivo básico, após o buffer ser preenchido, durante a chamada da função de escrita, todo o buffer é escrito no arquivo. Isso tinha um efeito negativo na transferência de arquivos, pois toda vez que o buffer ficava cheio, a transferência parava até que o buffer voltasse a ficar vazio. Essa especialização inicia um processamento paralelo (uma thread) que realiza o processo de gravação do buffer em disco sempre que um buffer fica cheio. A implementação também conta com uma fila que permite que exista mais de um buffer, permitindo que enquanto existe um buffer sendo preenchido, existe outro sendo gravado em disco. Quando a fila atinge sua capacidade máxima, que por padrão é 256, mas pode ser modificada, a função de escrita do ByteStream é bloqueada até que haja ao menos um espaço na fila. 4.2. DATASTREAMER Módulo que realiza transferência de um ByteStream entre dois computadores através da rede, conforme ilustrado na Figura 4, garantindo a entrega, a ordem dos dados contidos no ByteStream e suporta transferência de até (264 – 1) bytes. Figura 4 – Diagrama de relacionamento entre o DataStreamer e o ByteStream Computador A Computador B ByteStream DataStreamer ByteStream DataStreamer 18 No capítulo 2 desta monografia foram apresentadas algumas características do TCP, dentre elas, a garantia de entrega e a ordem dos pacotes, foram os fatores decisivos para a escolha do protocolo TCP para a realização da transferência de dados na rede. Essa escolha reduziu a complexidade do algoritmo do projeto, pois não foi necessária a criação de um algoritmo para garantir a ordem dos bytes enviados, nem a criação de um algoritmo para garantir a entrega. A conexão realizada entre dois computadores através deste módulo segue o mesmo conceito de uma conexão TCP, porém as operações de conexão de socket são realizadas internamente no módulo. Existe uma única função que estabelece a conexão, na qual informa-se o IP de destino, a porta que será usada e o modo de conexão. Este último consiste em dizer se o módulo irá utilizar a função accept ou a função connect do socket para estabelecer a conexão. Além da função que encerra a conexão que esteja aberta no módulo, existe uma função que envia os dados e outra função que recebe os dados. Essas duas funções são complementares e precisam ser executadas uma em cada computador para a transferência ser realizada. Tal transferência segue uma sequência de operações, conforme pode ser observado no diagrama da Figura 5, que correspondem ao controle do módulo. A fim de facilitar a compreensão do módulo, servidor é o computador que envia os dados e cliente é o computador que recebe os dados. Ao iniciar o processo de transferência do módulo, o servidor envia para o cliente o tamanho dos dados (em bytes) que serão transmitidos. O cliente responde se aceita ou não a transferência. Para que o cliente aceite a transferência, deve-se informar o tamanho dos dados ao executar a função de recebimento do módulo. Após o servidor receber a resposta de aceitação do cliente, inicia-se o processo de transmissão dos dados. Os dados são fragmentados em segmentos, de tamanho inicial de 63KB, e são transmitidos, dessa forma, direto pelo socket. Caso ocorra mais de três erros de timeout na função de envio do socket ao enviar um mesmo segmento, o tamanho do segmento é reduzido em 10%. Caso não ocorra erro na função de envio do socket, o tamanho do segmento é aumentado em 10% até o limite de 63KB. 19 Figura 5 – Diagrama de transmissão do DataStreamer Os segmentos sempre são enviados em ordem. Caso a função de envio do socket falhe mais de cinco vezes, a operação é encerrada com erro. Caso a função de recebimento do socket falhe mais de dez vezes, a operação é encerrada com erro. Em ambos os casos de erro, o socket é fechado e o módulo é desconectado. Após a transmissão de todos os segmentos, o servidor envia um byte informando que a transmissão foi finalizada. Caso o cliente não receba esse byte, um erro é retornado no servidor e no cliente. 4.3. DIRTREE Este módulo é responsável por criar e manipular estruturas em árvore que corresponde à estrutura de pastas e arquivos contidos no computador, ao mesmo tempo em que fornece facilidade para realizar operações com essas estruturas. Cada ramo da árvore sempre será uma pasta. Todo arquivo sempre será uma folha na árvore. A estrutura permite armazenar a data da última Envia Solicitação Recebe Resposta Envia o ByteStream Envia Fim Recebe Solicitação Envia Resposta Recebe o ByteStream Recebe Fim Aceitou? Não Sim Aceitou? Não Sim Fim Fim 20 modificação, o tamanho e o MD5 de arquivos, sendo que os dois primeiros são sempre obtidos ao se montar a estrutura usando as funções internas do módulo. A data é armazenada como inteiro de 32 bits e é obtida por meio da função stat. O cálculo do MD5 é opcional, pois é apenas utilizado para verificar se dois arquivos são iguais e o tempo gasto para calcular esse valor não é algo que se possa desconsiderar. Há também um atributo cuja finalidade é marcar a operação realizada com aquela pasta ou arquivo (criação, edição ou remoção), necessário no processo de sincronia de pastas e arquivos. Todos os ramos e folhas da árvore estão organizados em ordem alfabética de nome (da pasta ou arquivo), isso reduz a complexidade dos demais algoritmos, além de fornecer otimização para algumas operações, conforme serão explicadas adiante. A estrutura é case-sensitive em relação à nomenclatura das pastas e arquivos. Mesmo que uma estrutura física de pastas e arquivos não permita que exista uma pasta e um arquivo com o mesmo nome, essa estrutura permite isso para poder realizar as operações de comparação, muito utilizadas para realizar a sincronia de pastas e arquivos. A estrutura pode ser criada “manualmente”, mas o módulo possui uma função que monta toda a estrutura para uma desejada pasta, garantindo um padrão para o preenchimento das estruturas da árvore. É importante observar que essa função suporta path tanto no formato Windows quanto no formato UNIX, embora internamente opere tudo no formato UNIX. Outra característica desta função é que todos os ramos e folhas correspondem a apenas uma pasta ou arquivo, mas apenas a raiz da árvore corresponde ao path absoluto, que é informado no argumento da função. É importante ressaltar que o nó de raiz da árvore não é usado em nenhuma outra operação senão a de criação da árvore e a de leitura dos demais nós com o path, sendo que neste último caso o nó de raiz pode ser ignorado por meio de um argumento da própria função. As demais operações serão descritas a seguir, tendo como premissa a estrutura e o preenchimento da mesma através da função do próprio módulo para esta finalidade, descrita acima. 4.3.1. Serialização em JSON A fim de poder salvar a estrutura no banco de dados (sem problemas com informação em binário), transmitir a estrutura pela rede e por possuir ferramentas acessíveis para a depuração, a serialização por meio de JSON foi 21 escolhida. Navegadores modernos, como Google Chrome e Mozilla Firefox possuem um console JavaScript que permite executar comandos JavaScript diretamente, onde um desses comandos é capaz de serializar e desserializar uma estrutura em JSON. Todo arquivo serializado em JSON possui a data de criação, data de modificação e o tamanho do arquivo. Opcionalmente o MD5 pode ser adicionado à estrutura serializada, visto que em algumas etapas não é necessário o envio do MD5. O atributo de modificação também é opcional e é usado apenas no processo de sincronia, a fim de informar que tipo de modificação o arquivo/pasta sofreu. Com exceção do atributo de modificação,os demais números são codificados usando Z85 a fim de comprimir o resultado final, visto que, em JSON, o número é escrito de forma literal, logo, números muito grandes ocupam muito espaço, conforme Tabela 1 (página 7). Todo nó é serializado como um array com seus elementos dispostos seguindo os seguintes critérios: O primeiro elemento sempre será o nome do arquivo ou pasta, o segundo elemento sempre será um array que armazena os nós que são filhos do nó atual, ou seja, quando o nó atual é um arquivo, esse array fica vazio, caso contrário, contém os arquivos e pastas do nó atual. Caso o nó seja uma pasta, o terceiro elemento corresponde ao atributo de modificação e é um elemento opcional. Caso o nó seja um arquivo, o terceiro elemento é o tamanho do arquivo, o quarto elemento é a data de modificação, o quinto elemento é o MD5 (que é opcional, mas se torna obrigatório quando o atributo de modificação for serializado) e o sexto elemento é o atributo de modificação, que é opcional. 4.3.2. Adição de Nó O módulo possui duas funções que permitem adicionar um nó à estrutura em árvore na posição desejada. Enquanto uma das funções recebe a posição do nó como string no padrão UNIX para nomenclatura de pastas e arquivos, a outra função recebe a própria estrutura de nó, que será copiada internamente, porém não copia os filhos desse nó. Ao contrário da função que monta a estrutura, essa função, que adiciona um nó, recebe, como argumento, as informações do arquivo já preenchidas (caso seja arquivo). 22 A função ignora o path da raiz da árvore e cria a estrutura de pastas caso ela não exista. Ou seja, ao adicionar um nó do tipo arquivo que esteja em uma pasta que não pertença a atual estrutura, essa pasta será adicionada à estrutura, mantendo o padrão da função de criação da árvore. Por conveniência, na função de adição com base em outro nó, caso o nó que se deseja adicionar já se encontra na árvore, a função pode atualizar as informações do nó, para isso deve-se passar o argumento para a função de forma a permitir a atualização, caso contrário, um erro é emitido. Essa característica é usada na recepção de arquivos durante a sincronia de pastas e arquivos, pois é possível que o arquivo recebido seja uma atualização do arquivo existente. 4.3.3. Remoção de Nó Diferente da função de adição de nó, a função de remoção de nó recebe, como argumento, um nó de qualquer árvore e realiza a remoção do nó que possui os mesmos pais (comparando o nome dos nós superiores) e o mesmo nome e tipo. Ou seja, para remover um nó de uma árvore pode-se usar o próprio nó na função, mas também se pode criar duas árvores com a mesma estrutura e remover o nó de uma das estruturas usando o nó da outra estrutura equivalente. O nó removido e os filhos desse nó são todos apagados da memória ao se utilizar essa função. Portanto referências a esses nós se tornarão inválidas logo após a operação de remoção. 4.3.4. Atualização de Árvore Há uma função que atualiza todos os filhos de um nó de acordo com os filhos do nó informado como referência. Note que a atualização só é realizada com os filhos dos nós informados, além disso, devido a própria característica das estruturas em árvore, os nós usados como principal e referência serão entendidos como raiz de uma árvore. Dois nós são considerados iguais se seus nomes, seus tipos (arquivo ou pasta) e seus pais forem iguais. Caso a condição de igualdade seja verdadeira, o nó da árvore principal será atualizado conforme os dados da árvore de referência. Observe que dessa forma apenas os nós que são do tipo arquivo é que são modificados, visto que pastas não possuem outros atributos além do nome e do tipo, mas isso não é uma verdade constante, há um flag na função para atualizar 23 o atributo de modificação. Uma vez que tal flag esteja definido, o atributo de modificação de todos os nós da árvore principal, que estiverem presentes na árvore de referência, será atualizado para o valor contido na árvore de referência. Adicionalmente, devido ao modo como o algoritmo percorre a estrutura, uma flag foi acrescentada à função que permite que um nó de referência seja inserido na árvore principal (junto com seus filhos) caso o mesmo não exista na árvore principal. Pode parecer estranho, pois é uma operação de atualização, mas o algoritmo de sincronia, em alguns momentos, necessita atualizar alguns nós de uma árvore e inserir os nós que faltam. 4.3.5. Diferença entre Árvores Este é um dos pontos principais desse módulo e faz parte da base do módulo de sincronia de pastas e arquivos. Esta operação é realizada entre duas árvores e resulta em listas contendo a diferença entre as árvores. Figura 6 – Duas árvores para ilustrar o algoritmo de diferença de árvores esquerda ambas direita ( , ) Tabela 2 – Resultado do algoritmo de diferença das árvores de exemplo Considere duas árvores posicionadas lado a lado, conforme ilustrado na Figura 6. O algoritmo retorna três listas, conforme pode ser observado na Tabela 2. Uma das listas corresponde aos nós que estão na árvore da direita e não estão na árvore da esquerda, identificada como lista da direita. Outra lista corresponde aos nós que estão na árvore da esquerda e não estão na árvore da direita, identificada como lista da esquerda. E a última lista corresponde aos 0 1 3 4 5 6 7 8 A 9 B 0 1 2 4 5 6 7 8 A 9 B C 2 3 B B C 24 pares de nós que existem em ambas as árvores, mas apresentam informações diferentes, identificada como lista central. Considerando essa característica, conclui-se que a lista central somente pode conter arquivos. Observe que para um nó pertencer a ambas as árvores é necessário que eles possuam o mesmo nome, o mesmo tipo e os mesmos pais, idem ao modo de comparação realizado na operação de atualização de nós. Atendendo a essa condição, o algoritmo compara o tamanho do arquivo, a data de modificação e, caso esteja disponível, o MD5. O algoritmo julga essas três informações e insere ambos os nós na lista central se ao menos uma dessas informações for diferentes. O algoritmo percorre a árvore da direita ao mesmo tempo em que percorre a árvore da esquerda e aproveita o fato de que os filhos de cada nó estão em ordem alfabética. O algoritmo será explicado de forma recursiva, mas a sua implementação foi não recursiva, com o uso de pilhas para retornar ao ponto anterior. Idem ao algoritmo de atualização de árvore, o algoritmo de diferença de árvore ignora o nó de raiz da árvore. Ao receber o ponteiro de um dos nós da árvore da esquerda e similar ao da árvore da direita, o algoritmo percorre os filhos de ambos os nós, porém baseia-se no nó da esquerda. Quando o filho do nó da esquerda possui nome que antecede o nome do filho do nó da direita, o algoritmo assume que esse filho da esquerda não existe na direita e então adiciona ele e os filhos dele à lista da esquerda e segue com o próximo filho da esquerda. Quando os nomes de ambos os filhos são idênticos, usa-se o método de comparação supracitado quando o filho é um arquivo, caso o filho seja uma pasta, adentra-se ela em ambos os nós, ou seja, chama-se a mesma função usando, agora, esses dois filhos. Caso ainda existam filhos na direita e não exista mais filhos na esquerda, o algoritmo adiciona todos os demais filhos da direita (e os filhos deles também) à lista da direita. 4.3.6. União de Árvores Compara duas árvores e resulta em uma terceira árvore que contém todos os nós de ambas as árvores comparadas. Caso haja algum conflito, o par de nós é adicionado a uma lista, identificada como lista de conflitos, e nenhum dos dois nós é copiado para a árvore resultante. Essa operação é realizada no processo de sincronia de arquivos, que será explicado no capítulo 8, e tem como finalidade gerar a árvore com as operações 25 a seremrealizadas. Tem comportamento similar à operação de diferença de árvore, porém não utiliza as informações dos arquivos em todos os casos, mas utiliza o atributo de modificação em todas as comparações, a Figura 7 ilustra um exemplo de árvore sendo unida, com base apenas no atributo de modificação. Figura 7 – Ilustração do processo de união de árvores Quando um dos nós do par de nós analisado possui nome anterior ao outro, este nó (e todos os seus filhos) é adicionado à árvore resultante, na posição equivalente, e segue-se assim como no algoritmo de diferença. Similar ao processo de diferença de árvores, quando acaba os filhos de um dos nós, todos os demais filhos do outro nó são adicionados à árvore resultante. Quando os dois nós do par possuem o mesmo nome, verifica-se o tipo deles. Caso sejam diferentes, verifica-se se um dos dois nós foi marcado como deletado, caso nenhum dos dois foi marcado como deletado, esse par de nós é adicionado à lista de conflitos. Caso ambos os nós sejam pasta, copia o nó para a árvore resultante e define-se o atributo de modificação como deletado se ao menos um dos nós estiver marcado como deletado. Caso ambos os nó sejam arquivos, toma-se a decisão conforme apresentado na Tabela 3. Devido à forma que as árvores são geradas antes de se executar a operação de união no processo de sincronia de pastas e arquivos, algumas situações são impossíveis de ocorrer, mais detalhes podem ser encontrados no capítulo 8, e por isso nada é feito nessas situações. Quando o arquivo é criado ou editado em ambas as árvores, o algoritmo realiza a comparação das informações dos arquivos da mesma forma que no processo de diferença de árvore, exceto pelo fato de não ignorar o MD5 e, caso o mesmo não tenha sido calculado, considera-se que é um conflito. 0 1 5 6 8 B C 3 r n n n c n e 0 1 5 6 3 r r n r 0 1 5 6 8 B C 3 r r n c c n 4 4 r r e 26 Nó B criado editado deletado N ó A criado comparar impossível* impossível* editado impossível* comparar Nó A deletado impossível* Nó B deletado Tabela 3 – Decisão entre dois arquivos de mesmo nome Adicionalmente há uma lista de par de nós para todos os nós que foram comparados e marcados como iguais durante o processo de união de árvores. Essa lista é opcional e é usada no processo de sincronia de pastas e arquivos. 4.4. TAREFAS EM SEGUNDO PLANO Este projeto possui três módulos que necessitam estar operando de forma contínua e sem interrupção para realizar outras tarefas, são eles: • Módulo de Interface de Usuário Módulo responsável por toda a interação do usuário com os demais módulos que são controláveis. Este módulo não pode ser interrompido por qualquer tarefa, pois deve dar ao usuário a capacidade de executar ou parar outras tarefas. • Módulo do Núcleo de Conectividade Módulo responsável por receber os pacotes, pela rede, de informação e de operação. Tais pacotes devem ser tratados assim que chegam e despachar a tarefa equivalente. Este módulo não pode ser interrompido, pois durante uma transferência é possível que outros computadores realizem trocas de informações com este computador que, portanto, deve estar disponível para responder a essas requisições. Esse módulo será abordado no capítulo 5. • Módulo de Notificação de Conectividade Deste módulo, duas operações são desempenhadas, uma verifica as interfaces de rede e a outra é responsável pelo broadcast que é usado para notificar que o computador está na rede. Esse módulo também será abordado no capítulo 5. 27 Como nenhuma das tarefas acima podem ser paradas, foi necessário criar um módulo para agendar as demais tarefas, como, por exemplo, a tarefa de transferência de arquivos. Toda solicitação recebida pelo núcleo de conectividade é previamente analisada e então dispara a tarefa necessária para processar a solicitação recebida. Há tarefas que requerem interação com o usuário, não usar tarefa em segundo plano faria com que dois módulos principais ficassem parados em um determinado momento. Toda tarefa é passada ao sistema como tarefa que requer interação com o usuário ou tarefa automática. A tarefa que requer interação do usuário é executada pelo módulo de interface do usuário e, por tanto, não deve ter muito processamento além de simplesmente requisitar alguma informação. Já a tarefa automática é colocada em uma fila e serão processadas assim que uma das threads da thread pool estiver disponível. Essa thread pool é a responsável por manter as tarefas em segundo plano. Nesse projeto essa thread pool foi limitada a ter apenas duas threads executando em segundo plano. Como exemplo, considere o processo de aceitação de um pedido de criação de canal de sincronia. Ao receber a mensagem de criação de canal de sincronia, o núcleo de conectividade cria uma tarefa para processar essa solicitação e coloca essa tarefa como automática. Essa tarefa conecta um socket com o computador que criou a solicitação, faz um processamento inicial e cria uma tarefa para perguntar ao usuário se aceita a criação do canal. Essa segunda tarefa é executada pelo módulo de interface do usuário que deve responder a solicitação. Ao responder, essa segunda tarefa é finalizada e a primeira tarefa continua seu processamento. Maiores detalhes sobre esse processo serão apresentados nos capítulos 7 e 8. Conforme pode ser visto no exemplo acima, os módulos principais só interagem com o outro módulo em determinados pontos e com o mínimo de processamento necessário. Isso permite maior controle para o usuário e melhor resposta do sistema às requisições realizadas ao computador. 28 4.5. CONCLUSÃO Os módulos aqui apresentados são de extrema importância para o projeto, e sem eles o projeto não seria possível da forma que é. Em especial o ByteStream, que é a base de todas as informações manuseadas no projeto. Os módulos das próximas seções utilizarão esses módulos e nem sempre ficará explícito o uso dos mesmos. No próximo capítulo será apresentado o módulo de conectividade e seus derivados. 29 CAPÍTULO 5 5. CONECTIVIDADE Neste projeto toda a comunicação entre processos, e consequentemente entre computadores, é realizada através de mensagens binárias transmitidas por meio de sockets TCP/IP. Este capítulo abordará a forma como as mensagens binárias são geradas, o modo como essas mensagens são transmitidas e como essas mensagens são interpretadas. A Figura 8 contém os módulos de conectividade e demonstra o relacionamento entre eles e outros módulos. A seta com linha tracejada indica que a comunicação é feita por meio da rede. Figura 8 – Diagrama de relacionamento dos módulos de conectividade 5.1. MENSAGENS DE CONECTIVIDADE Os módulos de conectividade possuem um único grupo de tipos de mensagens que podem ser usadas. Essas mensagens são estruturas básicas em C (struct) que são serializadas em forma binária para serem transmitidas na rede. Para identificar o tipo de mensagem que foi serializada, o primeiro byte da mensagem corresponde à identificação da mesma no grupo exclusivo de mensagens de conectividade. Por usar apenas um byte, a quantidade máxima de mensagens diferentes é de 255 (uma vez que o byte nulo é descartado). Entretanto existe um limite Computador A Conectividade Computador B Conectividade Núcleo Notificação Núcleo Notificação Outros módulos 30 imposto de no máximo 127 mensagens diferentes, forçando o bit mais significativo desse byte em zero. Isso é para permitir que futuramente existam mais de 255 combinações, usando algum método similar ao adotado no UTF-8. Abaixo segue a lista de mensagens usadas no projeto, junto com uma breve explicação de uso e de seus conteúdos. As mensagens estão ordenadas de acordo com seu byte de identificação. Todas as mensagens que possuem oprefixo Broadcast podem, e devem, ser enviadas em broadcast. Já as mensagens que não possuem tal prefixo, não podem ser enviadas em broadcast. 1 - BroadcastHello Esta mensagem é usada apenas quando o computador entra em uma rede. Contém o hostname e a identificação única do computador que envia esta mensagem. 2 - BroadcastHelloExtended Esta mensagem não é usada, mas já está reservada a sua identificação. 3 - BroadcastIAmHere Esta mensagem é usada para informar aos demais computadores da rede que o computador ainda está presente na rede. Contém apenas a identificação única do computador que envia esta mensagem. 4 - MyCredential Esta mensagem é usado para informar a identificação do computador. Contém o hostname e a identificação única do computador que envia esta mensagem. 5 - MyCredentialExtended Esta mensagem não é usada, mas já está reservada a sua identificação. 6 - BroadcastConnectionDrop Esta mensagem é usada para informar que o computador está se retirando da rede e não contém outra informação adicional, restando aos que receberem a mensagem, identificarem o peer através do IP de origem da mensagem. 31 7 - BroadcastCheckHostname Esta mensagem é usada para pesquisar na rede se existe algum computador com um determinado hostname, que deve estar contido na mensagem. 8 - BroadcastCheckHost Esta mensagem é usada para pesquisar na rede se existe algum computador com uma determinada identificação única, que deve estar contida na mensagem. 9 - RequestAddFriend Esta mensagem é usada para enviar um pedido de solicitação de amizade a outro computador. Deve conter o hostname e a identificação única do computador que envia essa mensagem e uma palavra-chave, que será explicada no capítulo 6. 10 - RequestAddFriendAnswer Esta mensagem é usada para responder ao pedido de solicitação de amizade. Deve conter o hostname e a identificação única do computador que envia essa mensagem e uma palavra-chave, além da devida resposta (sim ou não). 11 - UpdateFriendCredential Esta mensagem não é usada, mas já está reservada a sua identificação. É planejada para atualizar o hostname nos amigos, quando o hostname for modificado. 12 - RequestAddFriendAck Esta mensagem é usada para confirmar que a mensagem de pedido de solicitação de amizade foi recebida pelo computador. Como essas mensagens são transmitidas por meio de uma conexão UDP, não há garantia de entrega dos pacotes, portanto a confirmação de entrega deve ser feita “manualmente”. Não contém nada. 32 13 - RequestAddFriendAnswerAck Idem à mensagem acima, porém confirma o recebimento da mensagem de resposta à solicitação de amizade. 14 - RequestChannelTransfer Esta mensagem é usada para solicitar um canal do tipo transferência de arquivos entre computadores. Deve conter a porta que será utilizada no canal e a identificação do canal. Mais informações sobre esse canal serão apresentadas no capítulo 7. 15 - RequestChannelSync Esta mensagem é usada para solicitar um canal do tipo sincronia de pastas e arquivos entre computadores. Deve conter a porta que será utilizada no canal e a identificação do canal. Mais informações sobre esse canal serão apresentadas no capítulo 8. 16 - RequestChannelCreater Esta mensagem é usada para solicitar um módulo de criação de canal para sincronia de pastas e arquivos entre computadores. Deve conter apenas a porta que será usada para troca de informações. 5.2. MÓDULO DO NÚCLEO DE CONECTIVIDADE Responsável por receber as mensagens da rede, decodificar para a estrutura correspondente, tomar decisão sobre o que fazer com a informação contida na mensagem e disparar os eventos correspondentes às mensagens recebidos. Este módulo possui um socket configurado em UDP para receber pacotes de qualquer IP em uma porta padrão (4790). Essa configuração permite que o projeto utilize broadcast para identificar os computadores na rede. Como não há restrição dos IPs de origem do pacote, ao enviar uma mensagem em broadcast, o próprio computador recebe a mensagem enviada, nesse caso, este módulo descarta essa mensagem. Este módulo permite expandir as operações a serem realizadas com uma mensagem recebida por meio de uma lista dinâmica que contém a função a ser 33 executada e o tipo de mensagem que deve ter sido recebida para executar a função associada. Isso permite que outros módulos utilizem o módulo do núcleo de conectividade para receber determinadas mensagens, como é o caso da operação de formação de amizade ou a operação de encontrar um computador na rede. Todavia, as operações padrões implementadas no módulo não podem ser desativadas, assim sendo, mesmo que uma operação padrão seja expandida, ela não sobrescreve as operações padrões, nem as demais operações expandidas. As operações padrões existentes estão associadas ao recebimento das seguintes mensagens: 1 - BroadcastHello Ao receber este tipo de mensagem, o módulo de relacionamentos é usado para verificar se o computador de origem é um amigo. Em caso afirmativo, o módulo de relacionamentos é notificado que um amigo entrou na rede e o módulo de notificação de conectividade é informado que deve enviar as próprias credenciais (MyCredential) para o computador que é amigo. 3 - BroadcastIAmHere Similar à mensagem acima, porém não utiliza o módulo de notificação de conectividade. 4 - MyCredential Idem à mensagem acima. 6 - BroadcastConnectionDrop Informa o módulo de relacionamentos que o computador associado ao IP de origem da mensagem não está mais conectado na rede. 7 - BroadcastCheckHostname Ao receber esta mensagem, compara-se o hostname contido na mensagem com o hostname que está associado ao computador no programa. Caso sejam iguais o módulo do núcleo de notificação de conectividade é informado que deve enviar as próprias credenciais (MyCredential) para o computador que enviou o broadcast. 34 8 - BroadcastCheckHost Idem à operação descrita acima, porém comparando a identificação única do computador. 9 - RequestAddFriend Ao receber esta mensagem, o módulo de relacionamentos é informado que uma solicitação de amizade foi criada. O módulo de relacionamentos é que usará as informações contidas na mensagem para dar continuidade ao processo de solicitação de amizade. 14 - RequestChannelTransfer Informa ao módulo de controle de canais que um canal do tipo transferência de arquivos foi solicitado, incluindo o IP de origem, a porta a ser usada e a identificação do canal. Indiretamente um módulo de transferência de arquivos é iniciado com os parâmetros recebidos. 15 - RequestChannelSync Idem à mensagem acima, porém solicita um canal de sincronia de pastas e arquivos e, indiretamente, um módulo de sincronia de pastas e arquivos é iniciado com os parâmetros recebidos. 16 - RequestChannelCreater Informa ao módulo de controle de canais que há um pedido de criação de canal de sincronia de pastas e arquivos. Indiretamente um módulo de criação de canal é iniciado com os parâmetros recebidos. 5.3. MÓDULO DE NOTIFICAÇÃO DE CONECTIVIDADE Responsável por transmitir todas as mensagens que são usadas pelo módulo de conectividade, isso cria certo nível de abstração dos demais módulos em relação ao módulo de conectividade. Também é responsável por enviar a mensagem de notificação de presença em rede e de identificar as interfaces de rede e suas alterações em relação à conectividade das mesmas. A operação de identificação de mudanças nas interfaces de rede e de notificação de presença precisam ser inicializadas, assim como o núcleo do módulo de conectividade também precisa ser inicializado. Como estas três 35 operações estão interligadas, todas elas são inicializadas seguindo a ordem: Verificador de interfaces, Núcleo do módulo de conectividade e, por último, a Notificação de presença. Cada uma dessas operaçõespossui uma thread que fica executando em segundo plano, por esse motivo essas operações devem ser interrompidas ao final do programa. O verificador de interfaces cria duas listas com as interfaces disponíveis no sistema (de acordo com o que a API do sistema retorna). Uma das listas contém o nome da interface (informado pelo sistema), o IP associado à interface, sendo 0.0.0.0 quando a interface está desconectada, e o IP de broadcast, sendo 0.0.0.0 quando a interface está desconectada. A outra lista possui, além das mesmas informações da lista citada acima, o MAC Address de cada interface. Enquanto que a lista com menos informações possui apenas as interfaces que estão conectadas, a outra lista é mais completa, contendo, também, as interfaces que não estão conectadas. A lista contendo apenas as interfaces conectadas é usada na operação de envio de broadcast, para garantir que a mensagem seja enviada por todas as interfaces, pois, em testes efetuados durante o desenvolvimento do projeto, quando um dos computadores possuía mais de uma interface conectada, o broadcast enviado para o endereço 255.255.255.255 era transmitido por apenas uma das interfaces. O endereço de broadcast contido na lista é exclusivo para aquela interface, pois é calculado usando o IP e a máscara da rede a qual a interface encontra-se conectada. Essas duas listas são preenchidas assim que a operação de verificação de interfaces é iniciada e, após isso, esse processo é repetido a cada minuto, até que o sinal de parada seja enviado para a operação. A notificação de presença inicia enviando um BroadcastHello, em seguida entra em um loop em que espera dez minutos e envia um BroadcastIAmHere, até que o sinal de parada seja enviado ou até que uma solicitação de envio de credenciais seja efetuado. Neste segundo caso, a espera de dez minutos é interrompida e uma mensagem de MyCredential é enviado, em unicast, para o IP contido na lista de IPs a enviar as credenciais (que corresponde aos IPs que outros módulos solicitaram para enviar as credenciais), em seguida retorna-se ao loop normal (espera e envio de BroadcastIAmHere). Quando o sinal de parada é enviado, a mensagem BroadcastConnectionDrop é automaticamente enviado. 36 Além dessas duas operações citadas, o módulo de notificação de conectividade possui funções para o envio de cada um dos tipos de mensagens disponíveis no núcleo de conectividade. O argumento dessas funções corresponde às informações que devem estar contidas nas respectivas mensagens. Com exceção da função de envio da mensagem de BroadcastCheckHostname, todas as demais mensagens são enviadas em unicast e, por isso, o primeiro argumento da função é o IP de destino. 5.4. CONSIDERAÇÕES Os módulos de conectividade possuem seus próprios métodos de operação interna e foram feitos para operar de forma autônoma e responsiva, disparando tarefas ou acionando callbacks referentes às mensagens recebidas que foram enviados por outro computador através do outro módulo de conectividade, mantendo, assim, uniformidade no conteúdo que circula nesse meio. Nas implementações mais recentes, por exemplo, os canais (de transferência ou sincronia), este módulo só é utilizado para formar uma conexão TCP entre os computadores envolvidos, porém o módulo ainda permite que outras operações sejam realizadas sem a necessidade do uso de outra conexão, que é o caso da operação de formação de amizade, que será descrito no próximo capítulo, onde se utiliza o recurso de callback para receber as respostas da solicitação. 37 CAPÍTULO 6 6. RELACIONAMENTOS Os módulos deste capítulo são responsáveis pelas informações de cada computador com o qual o computador host possui vínculo, que é chamado de “amizade” neste projeto, no que diz respeito a armazenar essas informações, gerenciar essas informações (incluindo o processo de formar amizade), identificar um computador na rede e validar as credenciais dos computadores para verificar a autenticidade da amizade. Ao longo deste capítulo, esses módulos serão explicados quanto a sua funcionalidade e operabilidade, assim como os casos de uso dos mesmos. Porém, antes de explicar os módulos, é necessário abordar sobre as informações que caracterizam um amigo. 6.1. AMIGO Amigo é uma forma de chamar o computador (peer) que possui um relacionamento estabelecido com o computador host. No escopo do projeto, amigo é uma estrutura que armazena as informações referentes ao computador (peer) e a essa amizade, afinal, não há motivos para armazenar uma cópia de qualquer outro computador na rede (e que possua o projeto em execução) sendo que esse não possui relações com o computador host. Essa estrutura armazena a identificação do amigo dentro do banco de dados (que é usado em outros módulos que requerem essa identificação), a identificação única do amigo (que é diferente da identificação do banco de dados), o hostname do amigo, o IP do amigo, as duas chaves de segurança formadas com o amigo e a data e hora da última mensagem recebida do amigo. Todas essas informações ficam armazenadas em uma tabela no banco de dados. O uso de uma chave de identificação interna diferente da identificação única é para evitar possíveis conflitos num futuro caso haja mudança no método 38 de se obter identificação única. Ao abrir o banco de dados o IP de todos os amigos são definidos como 0.0.0.0 e a data e hora da última mensagem recebida é zerada. Assim sendo, um amigo só é considerado online quando o IP for diferente de 0.0.0.0 e a data e hora da última mensagem recebida não for anterior a quinze minutos em relação à data e hora atual. Figura 9 – Diagrama de relacionamento dos módulos de relacionamento 6.2. MÓDULO DO NÚCLEO DE RELACIONAMENTOS Conforme pode ser observado no diagrama da Figura 9, este módulo é responsável por todas as operações que envolvem o banco de dados e, desta forma, oferece funções que permitem acessar os elementos do banco de dados, da tabela de relacionamentos, além de possuir funções únicas que disparam outras tarefas dentro do programa. É importante ressaltar que existe uma restrição em relação ao tempo de vida do resultado das consultas ao banco de dados, pois esses resultados são uma cópia do conteúdo do banco de dados, e não são atualizadas dinamicamente, sendo necessária uma nova consulta para atualizar os dados. Além de que todo o projeto foi criado para funcionar de forma assíncrona, assim sendo, é possível que a informação seja modificada no banco de dados logo após ela ter sido consultada, como é o caso de quando se recebe BroadcastHello ou BroadcastIAmHere, em que a informação de IP e última data e hora de recebimento de mensagem são atualizados. Pensando nisso, toda consulta realizada no projeto é usada de imediato. Existem, neste módulo, sete funções que processam diretamente sobre o banco de dados: Uma operação para adicionar um amigo ao banco de dados, uma operação de atualizar as informações de um amigo no banco de dados, uma função que preenche um array com todos os amigos do banco de dados, e quatro Relacionamento Núcleo BD Conectividade Núcleo Notificação Formação Procura Validação 39 funções que retornam um amigo com base em uma consulta, que pode ser a identificação única, o IP, o hostname ou a identificação interna. Com exceção das funções de consulta, as demais funções de acesso direto ao banco de dados devem ser evitadas, procurando utilizar alguma outra função do módulo que realize este trabalho. A fim de reduzir o uso de recursos, existe uma função que apenas verifica se existe um amigo com uma determinada identificação única no banco de dados, onde, ao invés de copiar todas as informações do amigo do banco de dados para a estrutura em memória, é feito uma simples consulta ao banco de dados para verificar se o amigo existe, retornando