O blog da AWS

Gerenciando sessões de usuários anônimos em aplicações baseadas em WebSocket API

Por Alexey Paramonov, Solutions Architect, ISV.

Esta publicação de blog demonstra como reconectar usuários anônimos ao WebSocket API sem perder o contexto de suas sessões. Você aprenderá como vincular IDs de conexão do WebSocket API ao usuário lógico, o que fazer quando a conexão falha e como armazenar e recuperar informações de sessão quando o usuário se reconecta.

WebSocket APIs são comuns em aplicações interativas modernas. Por exemplo, para acompanhar preços de ações, seguir feeds de chat ao vivo, colaborar com outros em ferramentas de produtividade ou jogar jogos online. Outro exemplo descrito no post do blog “Implementing reactive progress tracking for AWS Step Functions” usa WebSocket APIs para enviar progresso de volta ao cliente.

O backend está ciente do ID de conexão do cliente para encontrar o cliente certo para enviar dados. Mas se a conexão falhar temporariamente, o cliente se reconecta com um novo ID de conexão. Se o backend não tiver um mecanismo para associar um novo ID de conexão ao mesmo cliente, o contexto de interação é perdido e o usuário deve começar tudo de novo. Em um feed de chat ao vivo, isso poderia significar pular para a mensagem mais recente com perda de algumas mensagens anteriores. E no caso do rastreamento de progresso do AWS Step Functions, o usuário perderia as atualizações de progresso.

Visão geral

O exemplo usa WebSocket APIs do Amazon API Gateway. Ele usa AWS Lambda para gerenciamento de conexão WebSocket e para simular um teleimpressor para gerar um fluxo de caracteres com estado para fins de teste. Existem duas tabelas do Amazon DynamoDB para armazenar IDs de conexão e sessões de usuário, bem como uma MediaWiki API opcional para recuperar um artigo.

O diagrama a seguir descreve a arquitetura:

Arquitetura de referência

  1. O navegador gera um ID de usuário aleatório e o armazena localmente no armazenamento de sessão. O cliente envia o ID de usuário dentro do cabeçalho Sec-WebSocket-Protocol para o WebSocket API.
  2. A rota padrão OnConnect do WebSocket API invoca a função Lambda OnConnect e passa o ID de conexão e o ID de usuário para ela. A função Lambda OnConnect determina se o ID de usuário existe na tabela DynamoDB e cria um novo item ou atualiza o existente.
  3. Quando a conexão está aberta, o cliente envia a solicitação Read para a função Lambda Teleprinter.
  4. A função Lambda Teleprinter baixa um artigo sobre WebSocket APIs da Wikipedia e o armazena como uma string na memória.
  5. A função Lambda Teleprinter verifica se há um estado anterior armazenado na tabela Sessions no DynamoDB. Se for um novo usuário, a função Lambda Teleprinter começa a enviar o artigo desde o início, caractere por caractere, de volta ao cliente via WebSocket API. Se for um usuário que retorna, a função Lambda Teleprinter recupera a última posição do cursor (o último caractere enviado com sucesso) da tabela DynamoDB e continua de lá.
  6. A função Lambda Teleprinter envia 1 caractere a cada 100ms de volta ao cliente.
  7. O cliente recebe o artigo e o imprime caractere por caractere conforme cada caractere chega.
  8. Se a conexão quebrar, o WebSocket API chama a função Lambda OnDisconnect automaticamente. A função marca o ID de conexão para o ID de usuário fornecido como inativo.
  9. Se o usuário não retornar dentro de 5 minutos, o agendador do Amazon EventBridge invoca a função Lambda OnDelete, que exclui itens com mais de 5 minutos de inatividade das tabelas Connections e Sessions.

Um teleimpressor retorna dados um caractere por vez em vez de uma única resposta. Quando a função Lambda busca um artigo, ela o alimenta caractere por caractere dentro do loop for com um atraso de 100ms em cada iteração. Isso demonstra como o usuário pode continuar lendo o artigo após se reconectar e não começar o feed do início novamente. O padrão pode ser útil para limitação de tráfego, desacelerando as interações do usuário com o backend.

Entendendo a funcionalidade do código de exemplo

Quando o usuário se conecta ao WebSocket API, o cliente gera um ID de usuário e o salva no armazenamento de sessão do navegador. O ID de usuário é uma string aleatória. Quando o cliente abre uma conexão WebSocket, o frontend envia o ID de usuário dentro do cabeçalho Sec-WebSocket-Protocol, que é padrão para WebSocket APIs. Ao usar a biblioteca JavaScript WebSocket, não há necessidade de especificar o cabeçalho manualmente. Para inicializar uma nova conexão, use:

const newWebsocket = new WebSocket(wsUri, userId);

wsUri é a URL do WebSocket API implantado e userId é uma string aleatória gerada no navegador. O userId vai para o cabeçalho Sec-WebSocket-Protocol porque WebSocket APIs geralmente oferecem menos flexibilidade na escolha de cabeçalhos em comparação com APIs RESTful. Outra maneira de passar o ID de usuário para o backend poderia ser um parâmetro de string de consulta.

O backend recebe a solicitação de conexão com o ID de usuário e o ID de conexão. O WebSocket API gera o ID de conexão automaticamente. É importante incluir o Sec-WebSocket-Protocol na resposta do backend, caso contrário a conexão fecha imediatamente:

    return {
        'statusCode': 200,
        'headers': {
            'Sec-WebSocket-Protocol': userId
        }
    }

A função Lambda armazena essas informações na tabela Connections do DynamoDB:

    ddb.put_item(
        TableName=table_name,
        Item={
            'userId': {'S': userId},
            'connectionId': {'S': connection_id},
            'domainName': {'S': domain_name},
            'stage': {'S': stage},
            'active': {'S': True}
        }
    )

O atributo active indica se a conexão está ativa. No caso de inatividade por um limite de tempo especificado, a função Lambda OnDelete exclui o item automaticamente. A operação Put é útil aqui porque você não precisa consultar o DB se o usuário já existe. Se for um novo usuário, Put cria um novo item. Se for uma reconexão, Put atualiza o connectionId e define active como True novamente.

A chave primária para a tabela Connections é userId, o que ajuda a localizar usuários mais rapidamente quando eles se reconectam. A aplicação depende de um índice secundário global (GSI) para localizar connectionId. Isso é necessário para marcar a conexão como inativa quando o WebSocket API chama a função OnDisconnect automaticamente.

Agora que a aplicação tem gerenciamento de conexão, você pode recuperar dados de sessão. A função Lambda Teleprinter recebe um ID de conexão do WebSocket API. Você pode consultar o GSI da tabela Connections e descobrir se o usuário existe:

    def get_user_id(connection_id):
        response = ddb.query(
            TableName=connections_table_name,
            IndexName='connectionId-index',
            KeyConditionExpression='connectionId = :c',
            ExpressionAttributeValues={
                ':c': {'S': connection_id}
            }
        )

        items = response['Items']

        if len(items) == 1:
            return items[0]['userId']['S']

        return None

Outra tabela DynamoDB chamada Sessions é usada para verificar se o usuário tem um contexto de sessão existente. Se sim, a função Lambda recupera a posição do cursor e retoma a teletipagem. Se for um usuário totalmente novo, a função Lambda começa a enviar caracteres desde o início. A função armazena uma nova posição do cursor se a conexão quebrar. Isso facilita a detecção de conexões obsoletas e o armazenamento da posição atual do cursor na tabela Sessions.

    Try:
        api_client.post_to_connection(
            ConnectionId=connection_id,
            Data=bytes(ch, 'utf-8')
        )
    except api_client.exceptions.GoneException as e:
        print(f"Found stale connection, persisting state")
        store_cursor_position(user_id, pos)
        return {
            'statusCode': 410
        }

    def store_cursor_position(user_id, pos):
        ddb.put_item(
            TableName=sessions_table_name,
            Item={
                'userId': {'S': user_id},
                'cursorPosition': {'N': str(pos)}
            }
        )

Depois disso, a função Lambda Teleprinter termina.

Servindo usuários autenticados

O mesmo mecanismo também funciona para usuários autenticados. A principal diferença é que ele pega um ID fornecido de um token JWT ou outra forma de autenticação e o usa em vez de um ID de usuário gerado aleatoriamente. O backend depende de identificação de usuário inequívoca e pode exigir apenas pequenas alterações para lidar com usuários autenticados.

Excluindo usuários inativos

A função OnDisconnect marca as conexões de usuário como inativas e adiciona um timestamp ao atributo ‘lastSeen’. Se o usuário nunca se reconectar, a aplicação deve limpar itens inativos das tabelas Connections e Sessions. Dependendo de seus requisitos operacionais, existem duas opções.

Opção 1: Usando EventBridge Scheduler

A aplicação de exemplo usa EventBridge para agendar a execução da função OnDelete a cada 5 minutos. O código a seguir mostra como o AWS SAM adiciona o agendador à função OnDelete:

  OnDeleteSchedulerFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: app.handler
      Runtime: python3.9
      CodeUri: handlers/onDelete/
      MemorySize: 128
      Environment:
        Variables:
          CONNECTIONS_TABLE_NAME: !Ref ConnectionsTable
          SESSIONS_TABLE_NAME: !Ref SessionsTable
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref ConnectionsTable
      - DynamoDBCrudPolicy:
          TableName: !Ref SessionsTable
      Events:
        Schedule:
          Type: ScheduleV2
          Properties:
            ScheduleExpression: rate(5 minute)

A seção Events da definição da função é onde o AWS SAM configura a execução agendada. Altere os valores em ScheduleExpression para atender aos seus requisitos de agendamento.

A função OnDelete depende do GSI para encontrar IDs de usuário inativos. O trecho de código a seguir mostra como consultar conexões com mais de 5 minutos de inatividade:

    five_minutes_ago = int((datetime.now() - timedelta(minutes=5)).timestamp())

    stale_connection_items = table_connections.query(
        IndexName='lastSeen-index',
        KeyConditionExpression='active = :hk and lastSeen < :rk',
        ExpressionAttributeValues={
            ':hk': 'False',
            ':rk': five_minutes_ago
        }
    )

Depois disso, a função itera pela lista de IDs de usuário e os exclui das tabelas Connections e Sessions:

    # remove inactive connections
    with table_connections.batch_writer() as batch:
        for item in inactive_users:
            batch.delete_item(Key={'userId': item['userId']})

    # remove inactive sessions
    with table_sessions.batch_writer() as batch:
        for item in inactive_users:
            batch.delete_item(Key={'userId': item['userId']})

O exemplo usa batch_writer() para evitar solicitações para cada ID de usuário.

Opção 2: Usando DynamoDB TTL

O DynamoDB fornece um mecanismo integrado para expirar itens chamado Time to Live (TTL). Esta opção é mais fácil de implementar. Com TTL, não há necessidade de agendador EventBridge, função Lambda OnDelete e GSI adicional para rastrear o período de tempo. Para configurar o TTL, use o atributo ‘lastSeen’ como um tempo de criação do objeto. O TTL exclui ou arquiva o item após um período de tempo especificado. Ao usar AWS SAM ou modelos do AWS CloudFormation, adicione TimeToLiveSpecification ao seu código.

O TTL normalmente exclui itens expirados dentro de 48 horas. Se seus requisitos operacionais exigirem tempo mais rápido e mais previsível, use a opção 1. Por exemplo, se sua aplicação agrega dados enquanto o usuário está offline, use a opção 1. Mas se sua aplicação armazena dados de sessão estáticos, como a posição do cursor usada neste exemplo, a opção 2 pode ser uma alternativa mais fácil.

Implantando o exemplo

Pré-requisitos

Certifique-se de que você pode gerenciar recursos AWS do seu terminal.

  • AWS CLI e AWS SAM CLI instalados.
  • Você tem uma conta AWS. Caso contrário, visite esta página.
  • Seu usuário tem permissões suficientes para gerenciar recursos AWS.
  • Git está instalado.
  • NPM está instalado (apenas para implantação local do frontend).

Você pode encontrar o código-fonte e o README aqui.

O repositório contém tanto o código do frontend quanto do backend. Para implantar o exemplo, siga este procedimento:

  1. Abra um terminal e clone o repositório:
    git clone https://github.com/aws-samples/websocket-sessions-management.git
  2. Navegue até a raiz do repositório.
  3. Construa e implante o modelo AWS SAM:
    sam build && sam deploy --guided
  4. Anote o valor de WebSocketURL na saída. Você precisará dele mais tarde.

Com o backend implantado, teste-o usando o frontend hospedado.

Testando a aplicação

Testando a aplicação

Você pode assistir a uma demonstração animada aqui.

Observe que o aplicativo gerou um ID de usuário aleatório na inicialização. O aplicativo mostra o ID de usuário acima no cabeçalho.

  1. Cole a URL do WebSocket no campo de texto. Você pode encontrar a URL na saída do console depois de ter implantado com sucesso seu modelo AWS SAM. Alternativamente, você pode navegar até o AWS Management Console (certifique-se de estar na região correta), selecione a API que você implantou recentemente, vá para “Stages”, selecione o estágio implantado e copie o valor “WebSocket URL”.
  2. Escolha Connect. O aplicativo abre uma conexão WebSocket.
  3. Escolha Tele-read para começar a receber o artigo da Wikipedia caractere por caractere. Novos caracteres aparecem na segunda metade da tela conforme chegam.
  4. Escolha Disconnect para fechar a conexão WebSocket. Reconecte novamente e escolha Tele-read. Sua sessão retoma de onde você parou.
  5. Escolha Reset Identity. O aplicativo fecha a conexão WebSocket e altera o ID de usuário. Escolha Connect e Tele-read novamente e seu feed de caracteres começa do início.

Limpeza

Para limpar os recursos, no diretório raiz do repositório execute:

sam delete

Isso remove todos os recursos provisionados pelo arquivo template.yml.

Conclusão

Nesta publicação, você aprende como acompanhar sessões de usuário ao usar WebSockets API e não perder o contexto da sessão quando o usuário se reconecta novamente. Aplique os aprendizados deste exemplo para melhorar sua experiência de usuário ao usar WebSocket APIs para aplicações web-frontend e móveis, onde as conexões de internet podem ser instáveis.

Para mais recursos de aprendizado Serverless, visite  Serverless Land.


Este conteúdo foi traduzido do post original do blog, que pode ser encontrado aqui.

Autor

James Beswick é Sr. Manager, Tech Evangelism na Amazon Web Services.

Tradutores

Nicolas Tarzia é Senior Technical Account Manager na AWS, com mais de 13 anos de experiência, com ampla experiência em arquitetura cloud, engenharia e design de software. Sua área de interesse são tecnologias serverless.
https://www.linkedin.com/in/nicolastarzia
Daniel Abib é Arquiteto de Soluções Sênior e Especialista em Amazon Bedrock na AWS, com mais de 25 anos trabalhando com gerenciamento de projetos, arquiteturas de soluções escaláveis, desenvolvimento de sistemas e CI/CD, microsserviços, arquitetura Serverless & Containers e especialização em Machine Learning. Ele trabalha apoiando Startups, ajudando-os em sua jornada para a nuvem.
https://www.linkedin.com/in/danielabib/