Skip to content

eugabrielbr/PASSCOM-socket

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sobre o projeto

Este projeto foi desenvolvido como parte da disciplina MI — Concorrência e Conectividade do curso de Engenharia de Computação da Universidade Estadual de Feira de Santana (UEFS). Ele representa um sistema de compra de passagens aéreas, criado para explorar conceitos de concorrência e conectividade em rede de computadores.

Introdução

Redes de computadores são essenciais para a comunicação e o compartilhamento de informações em um mundo digitalizado, e sua aplicação permite que serviços e pessoas se conectem o tempo inteiro. Nesse contexto, foi solicitado aos alunos de Engenharia de Computação da Universidade Estadual de Feira de Santana (UEFS) o desenvolvimento de um sistema para uma companhia aérea brasileira de baixo custo, que pretende permitir que seus clientes adquiram passagens online, oferecendo a capacidade de selecionar e reservar trechos de forma exclusiva. Este relatório detalha o desenvolvimento de um sistema de compras de passagens aéreas, onde o cliente pode selecionar seu percurso de viagem, visualizar trechos disponíveis e consultar suas disponibilidades. Para garantir uma comunicação eficaz entre clientes e o servidor, será utilizado o protocolo TCP/IP, que proporciona uma troca de dados segura e eficiente. A solução incluirá o desenvolvimento de uma API em Python, compatível com a API Socket TCP/IP, assegurando a confiabilidade e o desempenho do sistema de compras.

Equipe

Tutor

  • Prof. Me. Antonio Augusto Teixeira Ribeiro Coutinho (UEFS)

Metodologia

Nesta seção será apresentado todo o conjunto de métodos utilizados e o caminho percorrido desde o início até a conclusão da solução do problema

Arquitetura da solução

Primeiro, antes de explicar diretamente qual é a arquitetura da solução, é preciso destacar qual foi o planejamento inicial de comunicação entre um cliente que deseja comprar a passagem e o servidor que recebe a solicitação de compra. Segue na Figura 1 o diagrama que descreve a ideia do fluxo que serviu como base para o início do desenvolvimento.

Figura 1. Fluxograma de comunicação

A solução desenvolvida segue o modelo clássico de comunicação cliente-servidor, onde o cliente faz login na aplicação e solicita uma ou mais compras de passagens. De forma assíncrona, o servidor processa a compra e retorna o status ao cliente. A seguir, é apresentada uma explicação mais detalhada dos componentes implementados e como está funcionando para atingir o seu papel na solução.

Componentes principais

Cliente:

Responsável por interagir com o usuário, enviando para o servidor suas solicitações.

Criação do socket: Inicializa e estabelece uma conexão com o servidor utilizando um socket TCP, configurado com o IP e a porta especificados. Isso cria o ponto de comunicação necessário para a troca de dados entre o cliente e o servidor.

Login: Após conectar-se ao servidor, o cliente realiza a autenticação enviando um CPF. O servidor verifica se o CPF já está logado e responde de acordo.

Menu de opções: Exibe um menu principal ao cliente com opções para visualizar trechos disponíveis, comprar passagens ou encerrar a conexão. O menu permite ao usuário escolher a ação desejada e interage com o servidor conforme a escolha.

Envio e recebimento de mensagens: Dependendo da seleção do menu, o cliente envia comandos ao servidor e recebe respostas. Para visualizar trechos, o cliente solicita e exibe a lista de trechos. Para a compra de passagens, o cliente envia dados sobre a origem e o destino, recebendo opções de trechos e preços disponíveis.

Serialização de Dados com pickle: Utiliza o módulo pickle para converter objetos Python em uma forma que pode ser transmitida pela rede e para reconstruí-los ao serem recebidos. Isso assegura a integridade dos dados trocados entre cliente e servidor.

Tratamento de Erros e Conexão: Inclui mecanismos para tratar erros de conexão e comunicação, garantindo que problemas durante o envio e recebimento de dados sejam adequadamente gerenciados. Isso melhora a robustez e a confiabilidade da comunicação entre cliente e servidor.

Servidor:

Responsável por aceitar conexões, processar compras e efetuar a persistência de dados.

Criação do socket: O servidor inicia um socket TCP para escutar conexões dos clientes em um IP e porta específicos.

Processo de login: Após receber uma solicitação de login, o servidor verifica se o cliente já está conectado. Se não estiver, permite a conexão; caso contrário, rejeita.

Gerenciamento de Trechos e Clientes: Os dados de clientes e trechos são armazenados em um arquivo JSON. O servidor carrega e manipula essas informações, permitindo que os clientes vejam trechos disponíveis e façam compras.

Tratamento de requisições: O servidor processa pedidos como visualização de trechos, busca de rotas e compras, e responde conforme os dados do sistema.

Encerramento de conexões: Ao finalizar a interação, o servidor remove o cliente da lista de conectados e fecha a conexão.

Uso de grafo: O grafo é usado para representar as cidades e os trechos entre elas. Cada cidade é um nó, e as arestas conectando as cidades têm informações sobre o trecho, como preço, distância e vagas. O programa faz uma busca em profundidade (DFS) no grafo, explorando todas as rotas possíveis entre a origem e o destino. Para cada rota, o servidor calcula o preço total e retorna todas as opções de viagem disponíveis. Quando um cliente compra um trecho, o servidor diminui o número de vagas disponíveis no grafo, atualizando assim a disponibilidade de passagens em tempo real.

Gerenciamento do arquivo JSON: O servidor carrega e salva os dados de trechos e clientes no formato JSON, garantindo que as informações persistam e sejam atualizadas de forma segura.

Serialização de Dados com pickle: Para enviar e receber os dados complexos de clientes e trechos, o servidor utiliza a biblioteca pickle, facilitando a comunicação eficiente entre o servidor e os clientes.

Paradigma de comunicação

O paradigma implementado na solução do problema é o stateful; ou seja: quando o cliente conecta com o servidor, ele não precisa enviar a cada requisição todas as informações necessárias para ver algum trecho ou efetuar alguma compra. Dessa forma, o usuário só precisa enviar uma informação por vez (cpf, origem de viagem, destino de viagem, número de passagens, etc) de acordo com a necessidade da requisição atual, já que o programa o mantém conectado e com suas informações “carregadas” e atualizadas enquanto estiver online com o servidor. Segue abaixo, em específico, aplicações que provam o paradigma adotado.

Manutenção da Conexão e Login: O cliente se conecta ao servidor e mantém uma conexão aberta enquanto realiza várias operações, como consultar trechos e efetuar compras. O estado do cliente, como o CPF e a sessão de login, é mantido no servidor enquanto a conexão permanece ativa.

Lista de Clientes Conectados: O servidor mantém uma lista dos clientes conectados, verificando se o cliente já está logado antes de permitir uma nova conexão.

Manutenção do Estado das Compras e Trechos: O estado da compra e os trechos escolhidos são mantidos ao longo da sessão, permitindo que o cliente navegue entre diferentes opções e finalize a compra posteriormente, sem perder o contexto; ou seja, quando o cliente efetua a compra, essa informação é diretamente atrelada ao seu objeto, independente de qual ação ele esteja realizando no momento.

Sessão de Conexão: O cliente continua conectado ao servidor enquanto navega pelos menus, consulta trechos e faz compras. O estado do cliente é mantido no servidor durante toda a sessão, e o cliente pode realizar múltiplas operações sem precisar se autenticar novamente ou enviar todos os dados anteriores.

A escolha desse paradigma foi devido ao suas vantagens, como:

  • Manter um controle melhor de um usuário durante toda a execução da aplicação
  • Mais intuitivo, já que os dados de cada entidade são mantidos independente de uma nova requisição
  • Reduz a necessidade de enviar informações repetitivas, já que na implementação atual muitas trocas de informações são feitas na interação cliente-servidor
  • Se por acaso o cliente desconectar, mas tenha conseguido ao menos efetuar a compra, ela ficará salva e visível para o cliente em uma próxima conexão.

Protocolo de comunicação

O protocolo de comunicação de rede adotado na solução do projeto é o TCP/IP, que combina os protocolos TCP (Transmission Control Protocol) e IP (Internet Protocol). Com esse protocolo, é possível enviar mensagens para o servidor por meio de uma conexão via socket, garantindo a entrega confiável e ordenada dos dados. Segue um esquema na Figura 2 mostrando como o protocolo funciona e sua relação com os sockets.

Figura 2. Esquema de comunicação via socket

Segue abaixo a ordem de troca de mensagens entre cliente e servidor durante a execução da aplicação em algumas requisições.

Login/autenticação:

Cliente -> Servidor: cliente envia seu CPF e o tipo de operação, que neste caso é consulta de login

Servidor -> Cliente: retorna objeto cliente e status de permissão de acesso (True ou False)

Solicitação para ver trechos:

Cliente -> Servidor: requisição: trechos

Servidor -> Cliente: trechos disponíveis

Busca de possibilidades:

Cliente -> Servidor: requisição: viagem, origem e destino

Servidor -> Cliente: trechos disponíveis de acordo com a origem e destino

Compra de passagens:

Cliente -> Servidor: requisição: compra, trecho selecionado

Servidor -> Cliente: status de compra

Formatação e tratamento de dados

A formatação de dados utilizada para transmitir informações no código é a serialização com pickle. A biblioteca pickle permite transformar objetos Python em uma sequência de bytes, que pode ser facilmente enviada entre o cliente e o servidor por meio de sockets. Assim, os dados são serializados no lado do cliente e deserializados no lado do servidor (e vice-versa), mantendo a integridade das informações.

Embora seja fácil de implementar e funcione muito bem com programas escritos em Python, se houver a necessidade de lidar com um servidor desenvolvido em outra linguagem, estabelecer a comunicação cliente-servidor pode se tornar mais complexo, uma vez que a biblioteca pickle é específica para Python.

A escolha desse método de comunicação foi uma decisão de projeto, visando a comunicação via objetos, o que exige uma biblioteca específica para Python para a serialização e deserialização dos dados

Tratamento de conexões simultâneas

O sistema implementa threads para lidar com as conexões simultâneas. Segue abaixo o detalhamento delas:

Threads

As threads em Python são utilizadas para realizar a execução simultânea de múltiplas tarefas dentro de um mesmo processo. Isso permite que diferentes partes de um programa rodem "ao mesmo tempo", sendo útil para lidar com tarefas que podem ser executadas de forma independente, como, por exemplo, a conexão de diversos clientes em um mesmo período de tempo. Na solução do problema, especificamente na função que estabelece a conexão com um usuário, é criada uma nova thread para cada cliente, por meio da biblioteca threading. Isso garante que não ocorra um problema que foi frequente durante os testes: a necessidade de um cliente se desconectar para que outro possa fazer login. Mais detalhes sobre a implementação na documentação do projeto.

Tratamento de concorrência

O sistema implementa mutexes para lidar com as concorrência. Segue abaixo o detalhamento sobre:

Mutex (lock)

A biblioteca threading também oferece ferramentas para lidar com a concorrência de acesso de recursos, garantindo assim que nenhuma inconsistência de dados ocorra. Os mutexes foram utilizados nos seguintes contextos:

Salvar cliente e carregar cliente: tanto no momento de salvar um novo cliente quanto ao verificar se ele já está presente no banco de dados (JSON), o lock bloqueia o acesso, impedindo que múltiplas threads acessem o arquivo simultaneamente.

Adicionar cliente: no servidor, há uma lista de clientes que auxilia na verificação de clientes conectados. Como não é permitido que dois clientes com o mesmo CPF acessem o sistema simultaneamente, foi implementado um lock para evitar essa situação.

Editar trecho: durante a compra de passagens, um lock também é utilizado para garantir que o arquivo seja atualizado corretamente na etapa de modificação das vagas disponíveis, evitando inconsistências no número de vagas e garantindo uma atualização coerente.

Verificar clientes conectados: foi adicionado um lock também no momento de verificar se o cliente que está solicitando conexão já está conectado. Dessa forma, apesar de ser um acontecimento raro, o cliente não tentará fazer uma conexão multiplica com o mesmo CPF ao mesmo tempo, evitando possíveis bugs.

Desempenho

Para melhorar o desempenho, foi implementado threads com o mesmo objetivo já citado anteriormente: permitir conexões simultâneas. A melhora foi evidente porque no cenário anterior quando as threads ainda não haviam sido implementadas, um cliente precisaria esperar o outro desconectar para assim conseguir estabelecer a conexão com o servidor e efetuar alguma tarefa. Essa solução melhorou consideravelmente o throughput total.

Para efetuar os testes, foi utilizado um arquivo bat, onde nele é possível executar comandos de execução no terminal e simular vários clientes tentando se conectar ao mesmo tempo. Segue um exemplo dos comandos abaixo:

start cmd /k "cd /d D:\vscode\mi redes\PBL-rede-de-computadores && python servidor.py && pause" REM inicia o servidor 
start cmd /k "cd /d D:\vscode\mi redes\PBL-rede-de-computadores && python cliente.py 112132312 && pause" REM conecta o cliente 1 
start cmd /k "cd /d D:\vscode\mi redes\PBL-rede-de-computadores && python cliente.py 243536366 && pause" REM conecta o cliente 2 
start cmd /k "cd /d D:\vscode\mi redes\PBL-rede-de-computadores && python cliente.py 376586588 && pause" REM conecta o cliente 3 
start cmd /k "cd /d D:\vscode\mi redes\PBL-rede-de-computadores && python cliente.py 476345232 && pause" REM conecta o cliente 4 
start cmd /k "cd /d D:\vscode\mi redes\PBL-rede-de-computadores && python cliente.py 589736455 && pause" REM conecta o cliente 5 

No script acima, é iniciado o servidor e depois cinco clientes distintos são conectados em sequência.

Figura 3. Clientes conectados e servidor rodando de fundo

Emprego do docker

O Docker é uma plataforma que permite a criação e execução de contêineres, oferecendo isolamento e consistência em projetos cliente-servidor. Cada contêiner opera de forma independente, garantindo que as aplicações não conflitam, enquanto a portabilidade dos contêineres assegura que funcionem de maneira consistente em diferentes ambientes, como desenvolvimento, teste e produção. Além disso, é possível testar de forma fiel a conexão com multiplos clientes, já que com o docker compose, pode-se criar diversas instâncias do contêiner do cliente, permitindo o teste de várias conexões. Segue abaixo como executar o sistema via docker:

  1. Construir as imagens Docker: Navegue até a pasta onde o arquivo docker-compose.yml está e execute o seguinte comando

    docker compose build
  2. Construir contêiner do servidor e executa-lo:

    docker compose up servidor

Adicionar clientes:

  1. Criar contêiner de um único cliente

    docker compose up cliente
  2. Criar contêiners de vários clientes

    docker compose up --scale cliente=(número de clientes que deseja)

Documentação do código

Esta seção apresenta a documentação completa do código, detalhando seus métodos, parâmetros o objetivos

Cliente

Documentação cliente

Classe

Cliente

  • Objetivo: Representa um cliente com CPF e trechos.
  • Métodos:
    • __init__(cpf, trechos): Inicializa o cliente.
    • to_dict(): Retorna os dados do cliente em formato de dicionário.

Funções

start_client

  • Objetivo: Cria e conecta o socket do cliente.
  • Parâmetros:
    • host: Endereço do servidor .
    • port: Porta do servidor .

limpar_tela

  • Objetivo: Limpa a tela do terminal.

exibir_menu

  • Objetivo: Exibe o menu principal.

ver_trechos

  • Objetivo: Simula a visualização de trechos.

selecionar_origem

  • Objetivo: Seleciona a cidade de origem.
  • Parâmetros:
    • opcao: Opção escolhida pelo usuário.

print_cidades

  • Objetivo: Imprime as cidades disponíveis.

compra_menu

  • Objetivo: Simula o processo de compra.
  • Retorno: Cidades de origem e destino escolhidas.

menu

  • Objetivo: Exibe o menu e processa a escolha do usuário.
  • Parâmetros:
    • client_socket: Socket do cliente.

login

  • Objetivo: Realiza o login do cliente.
  • Parâmetros:
    • client_socket: Socket do cliente.
  • Retorno: Status de verificação de login.

main

  • Objetivo: Inicia o cliente.

Servidor

Documentação servidor

Funções

salvar_json

  • Objetivo: Salva dados em um arquivo JSON.
  • Parâmetros:
    • dados: Dados a serem salvos.
    • caminho_arquivo: Caminho do arquivo onde os dados serão salvos.

carregar_json

  • Objetivo: Carrega dados de um arquivo JSON.
  • Parâmetros:
    • caminho_arquivo: Caminho do arquivo a ser carregado.

salvar_trecho

  • Objetivo: Salva os trechos de viagem no arquivo JSON.
  • Parâmetros:
    • trechos: Trechos a serem salvos.

salvar_clientes

  • Objetivo: Salva os clientes no arquivo JSON.
  • Parâmetros:
    • clientes: Lista de clientes a serem salvos.

carregar_clientes

  • Objetivo: Carrega a lista de clientes do arquivo JSON.
  • Retorno: Lista de objetos Cliente.

carregar_trechos

  • Objetivo: Carrega os trechos de viagem do arquivo JSON.
  • Retorno: Cópia dos trechos.

editar_trecho

  • Objetivo: Edita o trecho, diminuindo as vagas disponíveis.
  • Parâmetros:
    • caminho: Caminho do trecho a ser editado.
    • cliente_conectado: CPF do cliente conectado.
    • lock: Lock para garantir acesso seguro aos dados.

adicionar_cliente_arquivo

  • Objetivo: Adiciona ou atualiza um cliente no arquivo JSON.
  • Parâmetros:
    • cliente: Cliente a ser adicionado ou atualizado.

encontrar_cliente

  • Objetivo: Encontra um cliente na lista.
  • Parâmetros:
    • cliente: Cliente a ser encontrado.
  • Retorno: Cliente encontrado ou None.

inicializar_clientes

  • Objetivo: Inicializa a lista de clientes conectados.
  • Retorno: Lista vazia.

adicionar_cliente_conectado

  • Objetivo: Adiciona um cliente à lista de conectados.
  • Parâmetros:
    • cliente: Cliente a ser adicionado.
    • lista_clientes: Lista de clientes conectados.

remover_cliente_conectado

  • Objetivo: Remove um cliente da lista de conectados.
  • Parâmetros:
    • cliente: Cliente a ser removido.
    • lista_clientes: Lista de clientes conectados.

listar_clientes

  • Objetivo: Lista os clientes conectados.
  • Parâmetros:
    • lista_clientes: Lista de clientes conectados.
    • lock: Lock para acesso seguro.
  • Retorno: Cópia da lista de clientes conectados.

adicionar_trecho_cliente

  • Objetivo: Adiciona um trecho à lista de trechos de um cliente.
  • Parâmetros:
    • cpf: CPF do cliente.
    • trecho: Trecho a ser adicionado.

busca_possibilidades

  • Objetivo: Busca todas as possibilidades de rotas entre origem e destino.
  • Parâmetros:
    • grafo: Grafo das cidades e seus trechos.
    • origem: Cidade de origem.
    • destino: Cidade de destino.
  • Retorno: Rotas disponíveis entre as cidades.

start_server

  • Objetivo: Inicia o servidor para aceitar conexões de clientes.
  • Parâmetros:
    • host: Endereço do servidor .
    • port: Porta do servidor .

thread_cliente

  • Objetivo: Lida com a comunicação de cada cliente em uma nova thread.
  • Parâmetros:
    • client_socket: Socket do cliente.
    • client_address: Endereço do cliente.
    • clientes_conectados: Lista de clientes conectados.
    • lock: Lock para acesso seguro.

checar_conexao

  • Objetivo: Verifica se um cliente já está conectado e realiza o login.
  • Parâmetros:
    • client_socket: Socket do cliente.
    • clientes_conectados: Lista de clientes conectados.
    • lock: Lock para acesso seguro.

main

  • Objetivo: Inicia o servidor.

Conclusão

O sistema desenvolvido é uma aplicação cliente-servidor que gerencia trechos de viagem e possibilita a compra de passagens de forma eficiente e interativa. A estrutura do código permite uma comunicação robusta entre cliente e servidor, utilizando sockets para transmissão de dados e pickle para serialização de objetos, garantindo a integridade das informações trocadas.

A implementação do cliente proporciona uma interface amigável, onde o usuário pode visualizar os trechos disponíveis e realizar compras de forma simplificada. Além disso, a lógica de verificação de login assegura que cada cliente tenha uma experiência personalizada, evitando conflitos de acesso simultâneo.

No lado do servidor, a funcionalidade de gerenciamento de clientes e trechos é otimizada por meio de operações em arquivos JSON, permitindo a persistência de dados e facilitando a manipulação de informações sobre as viagens. A utilização de threads garante que múltiplos clientes possam ser atendidos simultaneamente, aumentando a eficiência do sistema.

Em suma, o projeto demonstra uma abordagem bem-sucedida para a criação de um sistema de reservas de passagens, combinando uma interface de usuário intuitiva com um backend eficiente e seguro. Futuras melhorias podem incluir a adição de funcionalidades como uma interface gráfica para melhor interação e experiência do cliente.

Referências

EXACTA WORKS. Stateless vs. stateful. Medium, 2021. Disponível em: https://medium.com/exactaworks/stateless-vs-stateful-f596a6b6471d. Acesso em: 21 set. 2024.

PYTHON SOFTWARE FOUNDATION. Biblioteca de Sockets. Disponível em: https://docs.python.org/3/library/socket.html. Acesso em: 21 set. 2024.

URAPYTHON COMMUNITY. Introdução a sockets em Python. Medium, 2019. Disponível em: https://medium.com/@urapython.community/introdu%C3%A7%C3%A3o-a-sockets-em-python-44d3d55c60d0. Acesso em: 21 set. 2024.

HOSTINGER. O que é TCP/IP. Hostinger, 2024. Disponível em: https://www.hostinger.com.br/tutoriais/tcp-ip. Acesso em: 21 set. 2024.

About

Sistema de compras de passagens usando conceitos de rede de computadores

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors