SOCKETBOX PROJECT
SocketBox é um projeto que foi passado na disciplina de redes de computadores com o objetivo de aprender a implementar e lidar com sockets.
Minha aplicação tem o objetivo de funcionar como um dropbox. Os usuários logados tem a possibilidade de faze uploads, downloads e compartilhar arquivos com outros usuários. No tocante ao download, o usuário pode fazer o download tanto dos arquivos upados por ele como os arquivos compartilhados com ele. Também é possível ter versionamento de arquivos, arquivos enviados com o mesmo nome receberão um número de versão de acordo com a cronologia de upload.
Temos um protocolo de comunicação, as mensagens são enviadas como um json e são separadas por um \nend-message\n. No geral as mensagens tem dois campos principais: header e body. Toda mensagem deve ter esses dois campos e eles devem obedecer a documentação que seguirá nos próximos tópicos. Segue abaixo um exemplo de um tipo de uma solicitação de criação de conta:
{
"body": {
"content": {
"username": "joao",
"password": "123456",
"password_confirmation": "123456"
}
},
"header": {
"ack": "",
"type": "create"
}
}
Veremos que a depender do tipo da mensagem, temos um cenário diferente no body.
O header irá guiar como a mensagem deve ser lida.
Este campo nos dirá qual o tipo da mensagem. Existem algumas possibilidades para esse campo:
Quando um cliente quer criar uma conta no SocketBox.
Caso o cliente queira fazer o login em alguma conta no SocketBox.
Esse campo é utilizado quando um cliente logado deseja fazer algum upload de arquivo para o SocketBox.
Caso o usuário queira fazer um download de um arquivo(upado ou compartilhado) contido em sua conta, ele o fará por meio desse tipo de mensagem.
Lista todos os arquivos upados e compartilhados.
Compartilha um arquivo upado com outra conta dentro do SocketBox.
Esse campo é utilizado basicamente pelo servidor, para indicar ao cliente o resultado das requisições feitas. Quando temos o campo type no header com algum conteúdo, temos o ack vázio ” ”, e vice-versa.
Essa parte da mensagem representa, de fato, o conteúdo que será necessário para que os requests enviados pelo cliente sejam processados de maneira correta. Baseado no tipo da mensagem teremos diferentes padrões de body. No geral teremos dois cenários, o primeiro é quando o ack está ativo e o type vazio, o segundo é quando o ack está vazio e o type está ativo. Ativo lê-se com algum conteúdo diferente de vazio(“”).
Quando o campo ack no header tiver algum valor, e consequentemente o tipo estiver vazio, significa que o servidor está respondendo algum pedido do cliente. A depender do pedido teremos conteúdos diferentes, mas uma coisa converge para todos: caso o pedido tenha sido realizado com sucesso teremos um status com número 1014, caso contrário teremos um status 0000. Status 1014 teram mensagens padronizadas, já status 0000 terão mensagens a depender do erro ocorrido. Segue abaixo exemplos de respostas ack para os diferentes tipos:
Basicamente todos esses tipos de mensagem tem um ack parecido com o abaixo, o que muda é que para cada um deles teriamos upload_response, login_reponse, etc e obviamente teríamos mensagens diferentes. Caso ocorresse algum erro durante a execução também receberiamos o mesmo padrão, entretanto agora com um status de erro e uma mensagem explicando o erro a medida do possível.
{
"body": {
"content": "SUCCESS: Account was created successful!",
"status": "1014"
},
"header": {
"ack": "create_response",
"type": ""
}
}
Já esses dois tipos funcionam de forma diferente, ao invés de nossa mensagem de sucesso temos o conteúdo que desejavamos quando fizemos os requests, ou seja, para o list_files recebemos uma lista com os arquivos upados ou compartilhados, já o download recebemos o arquivo do servidor e o cliente trata de salvar no devido local. Segue um exemplo de resposta para um list_files:
{
"body": {
"content": {
"shared": "",
"uploads": "teste.txt----------33----------\n"
},
"status": "1025"
},
"header": {
"ack": "list_files_response",
"type": ""
}
}
Nos itens seguintes seguirão exemplos de pedidos request do cliente para o servidor, com as suas respectivas explicações dos conteúdos do body.
- username: username do usuário que quer entrar na conta;
- password: senha do usuário.
{
"body": {
"content": {
"username": "pereira",
"password": "123"
}
},
"header": {
"ack": "",
"type": "login"
}
}
- username/password: usado para identificar o usuário que está fazendo o request;
- file_name: nome do arquivo a ser upado;
- file_type: tipo do arquivo a ser upado;
- upload: arquivo lido em bits e convertido para base64.
{
"body": {
"content": {
"username": "pereira",
"file_name": "teste",
"password": "123",
"upload": "dXBsb2FkIGRlIGFycXVpdm8gcGFyYQpvIFJFQURNRSEK\n",
"file_type": ".txt"
}
},
"header": {
"ack": "",
"type": "upload"
}
}
- username/password: usado para identificar o usuário que está fazendo o request.
{
"body": {
"content": {
"username": "pereira",
"password": "123"
}
},
"header": {
"ack": "",
"type": "list_files"
}
}
- username/password: usado para identificar o usuário que está fazendo o request;
- file_name: nome do arquivo a ser feito o download;
- download_type: indica se o arquivo será baixado dos arquivos compartilhados ou dos arquivos upados.
{
"body": {
"content": {
"username": "pereira",
"file_name": "teste.txt",
"download_type": "uploads",
"password": "123"
}
},
"header": {
"ack": "",
"type": "download"
}
}
- username/password: usado para identificar o usuário que está fazendo o request;
- target: usuário que receberá um arquivo compartihado;
- file_name: nome do arquivo a ser compartilhado;
- file_type: tipo do arquivo a ser compartilhado;
- download_type: de onde o arquivo compartilhado virá, no caso de um share_file request esse campo sempre terá o valor “uploads”
{
"body": {
"content": {
"username": "pereira",
"target": "lucas",
"file_type": ".txt",
"file_name": "teste",
"download_type": "uploads",
"password": "123"
}
},
"header": {
"ack": "",
"type": "share_file"
}
}
Inicialmente eu pensava em colocar codificação huffman em cada arquivo enviado e tratar melhor o versionamento dos arquivos, contudo, não conseguiria fazer em tempo ábil, mas será implementado em futuras versões. Alguns detalhes como auto-completar quando o usuário estiver digitando o diretório também deveriam ser implementados, mas acabei não dando prioridade a isso, e sim ao entendimento de como o socket e suas comunicações funcionam, as perfumarias do projeto não foram meus objetivos principais, portanto algumas coisas como digitar os diretórios dos arquivos para fazer upload podem se tornar cansativos.
No começo eu estava montando uma arquitetura para o projeto que não fazia o menor sentido, minhas mensagens chegavam duplicadas, tentava fazer um fluxo de usuário tanto no servidor como no cliente. Aos poucos fui percebendo que não fazia sentido, o fluxo do usuário no lado do cliente não deve ser visto no servidor, o servidor deve receber apenas os pedidos de requests e tratá-los de modo que a aplicação faça sentido. Diante disso, decidi fazer um refatoramento em todo o código e tentar implementar um protocolo de comunicação, de modo que eu não precisasse me preocupar com mensagens duplicadas. Daí veio a ideia de utilizar json estruturado em header e body, visto que o json tem um bom suporte em python e é uma estrutura para envio de mensagens bastante clara. Além disso precisava de marcações para delimitar o começo e o término das mensagens. A partir desse refatoramento meu código já ficou bastante genérico, e as funções foram seguindo com tranquilidade. Na parte do upload tive problema com o upload de algumas imagens JPEG que dava erro na hora de codificação para o envio do json, com isso precisei ficar utilizando encodes e decodes para base64, o que eu tive a impressão que os arquivos ficaram mais pesados. Verifiquei que alguns arquivos muito grandes, demoravam um tempo muito grande para serem enviados, não tive tempo de corrigir essa latência e estava em fase de observação para ver o que realmente estava havendo.