Artigo
· Dez. 21, 2020 9min de leitura

Um tutorial sobre WebSockets

Introdução

A maior parte da comunicação servidor-cliente na web é baseada em uma estrutura de solicitação e resposta. O cliente envia uma solicitação ao servidor e o servidor responde a esta solicitação. O protocolo WebSocket fornece um canal bidirecional de comunicação entre um servidor e um cliente, permitindo que os servidores enviem mensagens aos clientes sem primeiro receber uma solicitação. Para obter mais informações sobre o protocolo WebSocket e sua implementação no InterSystems IRIS, consulte os links abaixo.

Este tutorial é uma atualização de "Asynchronous Websockets - um tutorial rápido" para Caché 2016.2+ e InterSystems IRIS 2018.1+.

Operação assíncrona vs síncrona

No InterSystems IRIS, uma conexão WebSocket pode ser implementada de forma síncrona ou assíncrona. O modo como a conexão WebSocket entre o cliente e o servidor opera é determinado pela propriedade “SharedConnection” da classe %CSP.WebSocket.

  • SharedConnection=1 : operação assíncrona

  • SharedConnection=0: operação síncrona

Uma conexão WebSocket entre um cliente e um servidor hospedado em uma instância do InterSystems IRIS inclui uma conexão entre a instância IRIS e o Web Gateway. Na operação síncrona de WebSocket, a conexão usa um canal privado. Na operação assíncrona de WebSocket, um grupo de clientes WebSocket compartilha um conjunto de conexões entre a instância IRIS e o Web Gateway. A vantagem de uma implementação assíncrona de WebSockets se destaca quando se tem muitos clientes se conectando ao mesmo servidor, pois esta implementação não exige que cada cliente seja tratado por uma conexão exclusiva entre o Web Gateway e a instância IRIS.

Neste tutorial, implementaremos WebSockets de forma assíncrona. Portanto, todas as janelas de bate-papo abertas compartilham um conjunto de conexões entre o Web Gateway e a instância IRIS que hospeda a classe do servidor WebSocket.

Visão geral da aplicação de chat

O “hello world” do WebSockets é uma aplicação de chat em que um usuário pode enviar mensagens que são transmitidas a todos os usuários logados na aplicação. Neste tutorial, os componentes da aplicação de chat incluem:

  • Servidor: implementado em uma classe que estende %CSP.WebSocket

  • Cliente: implementado por uma página CSP

A implementação desta aplicação de chat irá realizar o seguinte:

  • Os usuários podem transmitir mensagens para todas as janelas do chat abertas

  • Os usuários on-line aparecerão na lista “Usuários on-line” de todas as janelas de chat abertas

  • Os usuários podem alterar seu nome de usuário compondo uma mensagem começando com a palavra-chave “alias” e esta mensagem não será transmitida, mas atualizará a lista de “Usuários On-line”

  • Quando os usuários fecham a janela de chat, eles são removidos da lista de “Usuários on-line”

Para ver o código fonte da aplicação de chat, visite este repositório no GitHub.

O Cliente

O lado do cliente da nossa aplicação de chat é implementado por uma página CSP contendo o estilo para a janela do chat, a declaração da conexão WebSocket, eventos do WebSocket e métodos que tratam da comunicação de e para o servidor, e funções auxiliares que empacotam mensagens enviadas para o servidor e processam as mensagens de entrada.

Primeiro, veremos como a aplicação inicia a conexão WebSocket usando uma biblioteca Javascript WebSocket.

    ws = new WebSocket(((window.location.protocol === "https:")? "wss:":"ws:")
                    + "//"+ window.location.host + "/csp/user/Chat.Server.cls");

new cria uma nova instância da classe WebSocket. Isso abre uma conexão WebSocket com o servidor usando o protocolo "wss" (indica o uso de TLS para o canal de comunicação WebSocket) ou "ws". O servidor é especificado pelo número da porta do servidor web e nome do host da instância que define a classe Chat.Server (essas informações estão contidas na variável window.location.host). O nome de nossa classe no servidor (Chat.Server.cls) está incluído no URI de abertura do WebSocket como uma solicitação GET para o recurso no servidor.

O evento ws.onopen é disparado quando a conexão WebSocket é estabelecida com êxito, fazendo a transição de um estado de conectando para um estado aberto.

    ws.onopen = function(event){
        document.getElementById("headline").innerHTML = "CHAT - CONNECTED";
    };

Este evento atualiza o cabeçalho da janela do chat para indicar que o cliente e o servidor estão conectados.

Enviando mensagens

A ação de um usuário enviando uma mensagem aciona a função de envio. Essa função atua como um invólucro em torno do método ws.send, que contém a mecânica de envio da mensagem do cliente ao servidor pela conexão WebSocket.

function send() {
    var line=$("#inputline").val();
    if (line.substr(0,5)=="alias"){
        alias=line.split(" ")[1];
        if (alias==""){
            alias="default";
        }
        var data = {}
        data.User = alias
        ws.send(JSON.stringify(data));
        } else {
        var msg=btoa(line);
        var data={};
        data.Message=msg;
        data.Author=alias;
        if (ws && msg!="") {
            ws.send(JSON.stringify(data));
        }
    }
    $("#inputline").val("");
}

send envia pacotes as informações a serem enviadas ao servidor em um objeto JSON, definindo pares de chave/valor de acordo com o tipo de informação que está sendo enviada (atualização de alias ou mensagem geral). btoa traduz o conteúdo de uma mensagem geral em uma string ASCII codificada em base 64.

Recebendo mensagens

Quando o cliente recebe uma mensagem do servidor, o evento ws.onmessage é acionado.

ws.onmessage = function(event) {
    var d=JSON.parse(event.data);
    if (d.Type=="Chat") {
        $("#chat").append(wrapmessage(d));
            $("#chatdiv").animate({ scrollTop: $('#chatdiv').prop("scrollHeight")}, 1000);
    } else if(d.Type=="userlist") {
        var ul = document.getElementById("userlist");
        while(ul.firstChild){ul.removeChild(ul.firstChild)};
        $("#userlist").append(wrapuser(d.Users));
    } else if(d.Type=="Status"){
        document.getElementById("headline").innerHTML = "CHAT - connected - "+d.WSID;
    }
};

Dependendo do tipo de mensagem que o cliente recebe (“Chat”, “userlist” ou “status”), o evento onmessage chama wrapmessage ou wrapuser para popular as seções apropriadas da janela de chat com os dados de entrada. Se a mensagem recebida for uma atualização de status, o cabeçalho de status da janela do chat é atualizado com o ID do WebSocket, que identifica a conexão WebSocket bidirecional associada à janela do chat.

Componentes adicionais do cliente

Um erro na comunicação entre o cliente e o servidor aciona o método WebSocket onerror, que emite um alerta que nos notifica do erro e atualiza o cabeçalho de status da página.

ws.onerror = function(event) {
    document.GetElementById("headline").innerHTML = "CHAT - error";
    alert("Received error"); 
};

O método onclose é disparado quando a conexão WebSocket entre o cliente e o servidor é fechada, e atualiza o cabeçalho de status.

ws.onclose = function(event) {
    ws = null;
    document.getElementById("headline").innerHTML = "CHAT - disconnected";
}

O servidor

O lado do servidor da aplicação de chat é implementado pela classe Chat.Server, que estende de %CSP.WebSocket. Nossa classe de servidor herda várias propriedades e métodos de %CSP.WebSocket, alguns dos quais discutirei abaixo. Chat.Server também implementa métodos personalizados para processar mensagens de entrada e transmitir mensagens para o(s) cliente(s).

Antes de iniciar o servidor

OnPreServer() é executado antes de o servidor WebSocket ser criado e é herdado da classe %CSP.WebSocket.

Method OnPreServer() As %Status
{
    set ..SharedConnection=1
    if (..WebSocketID '= ""){ 
        set ^Chat.WebSocketConnections(..WebSocketID)=""
    } else {
        set ^Chat.Errors($INCREMENT(^Chat.Errors),"no websocketid defined")=$HOROLOG 
    }
    Quit $$$OK
}

Este método define o parâmetro da classe SharedConnection em 1, indicando que nossa conexão WebSocket será assíncrona e suportada por vários processos que definem conexões entre a instância InterSystems IRIS e o Web Gateway. O parâmetro SharedConnection só pode ser alterado em OnPreServer(). O OnPreServer() também armazena o ID WebSocket associado ao cliente no ^Chat.WebSocketConnections global.

O método do servidor

O corpo principal da lógica executada pelo servidor está contido no método Server().

Method Server() As %Status
{
    do ..StatusUpdate(..WebSocketID)
    for {       
        set data=..Read(.size,.sc,1) 
        if ($$$ISERR(sc)){
            if ($$$GETERRORCODE(sc)=$$$CSPWebSocketTimeout) {
                //$$$DEBUG("no data")
            }
            if ($$$GETERRORCODE(sc)=$$$CSPWebSocketClosed){
                kill ^Chat.WebSocketConnections(..WebSocketID)
                do ..RemoveUser($g(^Chat.Users(..WebSocketID))) 
                kill ^Chat.Users(..WebSocketID)
                quit  // Client closed WebSocket
            }
        } else{
            if data["User"{
                do ..AddUser(data,..WebSocketID)
            } else {
                set mid=$INCREMENT(^Chat.Messages)
                set ^Chat.Messages(mid)=data
                do ..ProcessMessage(mid)
            }
        }
    }
    Quit $$$OK
}

Este método lê as mensagens recebidas do cliente (usando o método Read da classe %CSP.WebSockets), adiciona os objetos JSON recebidos ao ^Chat.Messages global e chama o ProcessMessage() para encaminhar a mensagem a todos os outros clientes de chat conectados. Quando um usuário fecha sua janela de chat (encerrando assim a conexão WebSocket com o servidor), a chamada do método Server() para Read retorna um código de erro que avalia a macro $$$CSPWebSocketClosed e o método prossegue para tratar o encerramento de acordo.

Processamento e distribuição de mensagens

ProcessMessage() adiciona metadados à mensagem de chat recebida e chama o SendData(), passando a mensagem como um parâmetro.

ClassMethod ProcessMessage(mid As %String)
{
    set msg = ##class(%DynamicObject).%FromJSON($GET(^Chat.Messages(mid)))
    set msg.Type="Chat"
    set msg.Sent=$ZDATETIME($HOROLOG,3)
    do ..SendData(msg)
}

ProcessMessage() recupera a mensagem formatada em JSON do ^Chat.Messages global e a converte em um objeto do InterSystems IRIS usando a classe %DynamicObject e o método '%FromJSON. Isso nos permite editar facilmente os dados antes de encaminharmos a mensagem para todos os clientes de chat conectados. Adicionamos um atributo Type com o valor “Chat”, que o cliente usa para determinar como lidar com a mensagem recebida. SendData() envia a mensagem para todos os outros clientes de chat conectados.

ClassMethod SendData(data As %DynamicObject)
{
    set c = ""
    for {
        set c = $order(^Chat.WebSocketConnections(c))
        if c="" Quit
        set ws = ..%New()
        set sc = ws.OpenServer(c)
        if $$$ISERR(sc) { do ..HandleError(c,"open") } 
        set sc = ws.Write(data.%ToJSON())
        if $$$ISERR(sc) { do ..HandleError(c,"write") }
    }
}

SendData() converte o objeto do InterSystems IRIS de volta em uma string JSON (data.%ToJSON()) e envia a mensagem para todos os clientes de chat. SendData() obtém o ID WebSocket associado a cada conexão cliente-servidor do ^Chat.WebSocketConnections global e usa o ID para abrir uma conexão WebSocket por meio do método OpenServer da classe %CSP.WebSocket. Podemos usar o método OpenServer para fazer isso porque nossas conexões WebSocket são assíncronas - extraímos do pool existente de processos do IRIS-Web Gateway e atribuímos um ID WebSocket que identifica a conexão do servidor a um cliente de chat específico. Por fim, o método Write() %CSP.WebSocket envia a representação da string JSON da mensagem para o cliente.

Conclusão

Esta aplicação de chat demonstra como estabelecer conexões WebSocket entre um cliente e um servidor hospedado pelo InterSystems IRIS. Para continuar lendo sobre o protocolo e sua implementação no InterSystems IRIS, dê uma olhada nos links na introdução.

Discussão (0)2
Entre ou crie uma conta para continuar