SignalR

Criando um Chat com ASP.NET Core SignalR

No artigo passado, abordei aplicações em tempo real com a biblioteca SignalR, onde demonstrei seu uso em uma streaming API.

Como citei no artigo, um uso muito comum desta biblioteca é na criação de chats. Para conhecer mais recursos dela, vamos criar este tipo de aplicação.

Começando pelo começo

Antes de mais nada, criaremos uma aplicação web:

dotnet new web -n ChatSignalR

E nela, iremos criar um Hub:

namespace ChatSignalR.Hubs
{
    public class ChatHub: Hub
    {
        public async Task SendMessage(string usuario, string mensagem)
        {
            await Clients.All.SendAsync("ReceiveMessage", usuario, mensagem);
        }
    }
}

Note que para esta aplicação o Hub possui um método. Será para este método que o cliente enviará mensagens. Assim que a mensagem for recebida, o Hub a enviará para todos os clientes.

Esta será a dinâmica do nosso chat. Um chat aberto, onde todos os usuários conectados receberão todas as mensagens dos demais usuários.

Na parte do servidor, por fim, é necessário habilitar o SignalR e registrar o hub na classe Startup:

public class Startup
{
    public void ConfigureServices(IServiceCollection services) => services.AddSignalR();

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseDefaultFiles(); 
        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapHub<ChatHub>("/chat");
        });
    }
}

Criando o cliente:

O nosso cliente será uma página web simples, que conterá um campo para o usuário informar seu nome e uma mensagem:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Mensagens</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" >
</head>
<body>
    <div class="container col-6">
        <div class="form-group">
            <label for="usuario">Usuário</label>
            <input type="text" id="usuario" class="form-control"/>
        </div>
        <div class="form-group">
            <label for="mensagem">Mensagem</label>
            <textarea class="form-control" id="mensagem" rows="2"></textarea>
        </div>
        <input type="button" class="btn btn-primary" id="send" value="Enviar Mensagem" />
    </div>
    <div class="row">
        <div class="col-12">
            <hr />
        </div>
    </div>
    <div class="container col-6">
        <ul class="list-group" id="messagesList"></ul>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/aspnet-signalr/1.1.4/signalr.min.js'></script>
    <script src='main.js'></script>
</body>
</html>

Na parte do JavaScript, conectaremos ao Hub do servidor e definiremos o envio e recebimento de mensagens:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chat").build();
$("#send").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = $("<li></li>").text(user + ": " + msg);
    li.addClass("list-group-item");
    $("#messagesList").append(li);
});

connection.start().then(function () {
    $("#send").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

$("#send").on("click", function (event) {
    var user = $("#usuario").val();
    var message = $("#mensagem").val();
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});

Note que quando uma mensagem for recebida, ela é exibida em uma lista:

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = $("<li></li>").text(user + ": " + msg);
    li.addClass("list-group-item");
    $("#messagesList").append(li);
});

E no envio é indicado o método definido no Hub:

$("#send").on("click", function (event) {
    var user = $("#usuario").val();
    var message = $("#mensagem").val();
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
C# (C Sharp) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
CONHEÇA O CURSO

1,2, 3…. Testando

Agora, ao executar a aplicação e abrir várias abas, note que todos os usuários irão receber as mensagens que forem enviadas:

Simples, não é?

Você pode obter o código da aplicação demonstrado neste artigo no meu Github.

Aplicações em tempo real com ASP.NET Core SignalR

No meu artigo sobre streaming API, citei que no ASP.NET Core a entrega de dados em tempo real pode ser realizada via SignalR, que é uma biblioteca criada para este fim. Neste artigo vamos conhecê-la em detalhes.

Na época que isso aqui era só mato…

Os protocolos base da internet são baseados em um modelo cliente-servidor síncrono. O cliente envia uma solicitação, o servidor a processa e retorna uma resposta, finalizando a conexão. Neste cenário, criar uma aplicação que possua/necessite de comunicação em tempo real, com acesso assíncrono de um ou mais usuários é algo extremamente complexo.

Procurando contornar isso, ao longo do tempo foram surgindo tecnologias/técnicas que permitem a implementação de comunicação em tempo real, como:

  • WebSockets;
  • Long Polling;
  • Server-Sent Events.

Entretanto, cada uma possui sua complexidade e limitação técnica. Felizmente, para os desenvolvedores ASP.NET surgiu uma nova opção, o SignalR.

C# (C Sharp) - Introdução ao ASP.NET Core
Curso de C# (C Sharp) - Introdução ao ASP.NET Core
CONHEÇA O CURSO

SignalR

O SignalR é uma biblioteca open source criada em 2011 por David Fowler e Damian Edwards com o intuito de facilitar a implementação de aplicações em tempo real no ASP.NET.

Na época da sua criação, o WebSocket havia acabado de ser padronizado, não era popular e não estava disponível em todos os navegadores. Assim, para desenvolver uma aplicação em tempo real, o desenvolvedor precisava decidir se a limitaria apenas a alguns navegadores ou utilizava mais de uma tecnologia/técnica.

O SignalR foi criado para resolver este problema. Nos bastidores, ele define qual é o melhor tipo de protocolo, tecnologia ou técnica que a conexão irá utilizar baseado no que o cliente e servidor suportam. Com isso, fornece um endpoint (chamado de Hub) que pode enviar e receber mensagens em tempo real. Como é fácil de ser utilizado, rapidamente o SignalR se tornou a principal opção para se desenvolver aplicações em tempo real no ASP.NET.

Hubs

Para abstrair a comunicação entre clientes e servidores, o SignalR fornece um pipeline de auto nível chamado Hub. O SignalR lida com o envio através dos limites da máquina automaticamente, permitindo que clientes chamem os métodos no servidor e vice-versa. É possível passar parâmetros fortemente tipados para os métodos, o que habilita model binding.

Para realizar esta comunicação, o SignalR fornece dois protocolos: um baseado em JSON e outro binário, baseado em MessagePack. O MessagePack geralmente cria mensagens menores, em comparação com o JSON. O navegador precisa suportar XHR level 2 para ter suporte ao MessagePack.

Colocando a mão na massa

Para exemplificar o uso do SignalR, vamos alterar a aplicação demostrada no artigo de streaming API.

Na versão 3.0 do .NET Core, o SignalR foi adicionado na biblioteca padrão, assim para utilizá-lo não é necessário adicionar nenhuma biblioteca externa ao projeto.

Inicialmente iremos criar um Hub. Um Hub é uma classe que herda a classe Hub do namespace Microsoft.AspNetCore.SignalR:

public class StreamingHub: Hub
{
    //Vazia porque a comunicação será realizada do servidor para o cliente
}

Como neste exemplo quem irá gerar os eventos é o servidor, não é será necessário definir nenhum método dentro da classe.

Para habilitar o SignalR no projeto, é necessário adicioná-lo no método ConfigureServices da classe Startup:

public void ConfigureServices(IServiceCollection services)
{ 
    services.AddControllers();
    services.AddSignalR();
}

Nesta mesma classe, no método Configure é necessário indicar o endpoint do hub que acabamos de definir:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseDefaultFiles(); 
    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapHub<StreamingHub>("/streaminghub"); 
    });
}

Por fim, no controller é indicado que o Hub será recebido via injeção de dependência:

private readonly IHubContext<StreamingHub> _streaming;

public TodoController(IHubContext<StreamingHub> streaming) => _streaming = streaming;

Isso permitirá o envio de mensagens pelo servidor.

No artigo anterior foi definido que a cada ação do controller uma mensagem era enviada para o endpoint de streaming. Isso é feito no método WriteOnStream:

private async Task WriteOnStream(Item data, string action)
{
    string jsonData = string.Format("{0}\n", JsonSerializer.Serialize(new { data, action }));

    foreach (var client in _clients)
    {
        await client.WriteAsync(jsonData);
        await client.FlushAsync();
    }
}

Como o nosso Hub irá executar o mesmo procedimento, podemos enviar sua mensagem neste mesmo método:

private async Task WriteOnStream(Item data, string action)
{
    string jsonData = string.Format("{0}\n", JsonSerializer.Serialize(new { data, action }));

    //Utiliza o Hub para enviar uma mensagem para ReceiveMessage
    await _streaming.Clients.All.SendAsync("ReceiveMessage", jsonData);

    foreach (var client in _clients)
    {
        await client.WriteAsync(jsonData);
        await client.FlushAsync();
    }
}

Definindo o cliente

Para que um cliente se conecte ao endpoint do hub é necessário utilizar uma biblioteca apropriada. Apenas assim, o SignalR conseguirá determinar qual tipo de protocolo utilizar durante a conexão. Felizmente a Microsoft criou bibliotecas clientes para as mais variadas linguagens: JavaScript, Java, C#, entre outras.

Neste exemplo iremos utilizar a biblioteca do JavaScript, que pode ser obtida via NPM (pacote @aspnet/signalr), mas aqui irei utilizar a versão CDNJS:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Mensagens</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
</head>
<body>
    <ul id="messagesList"></ul>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/aspnet-signalr/1.1.4/signalr.min.js'></script>
    <script src='main.js'></script>
</body>
</html>

Por fim, é necessário definir no JavaScript a comunicação com o endpoint (arquivo main.js):

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/streaminghub").build();

connection.on("ReceiveMessage", function (message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = document.createElement("li");
    li.textContent = msg;
    document.getElementById("messagesList").appendChild(li);
});

connection.start().then(function () {
    var li = document.createElement("li");
    li.textContent = "Connetado!";
    document.getElementById("messagesList").appendChild(li);
}).catch(function (err) {
    return console.error(err.toString());
});

Note que inicialmente é criada uma conexão:

var connection = new signalR.HubConnectionBuilder().withUrl("/streaminghub").build();

Esta conexão ficará “ouvindo” o evento ReceiveMessage:

connection.on("ReceiveMessage", function (message) {
    var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
    var li = document.createElement("li");
    li.textContent = msg;
    document.getElementById("messagesList").appendChild(li);
});

E quando a conexão for aberta isso é informado:

connection.start().then(function () {
    var li = document.createElement("li");
    li.textContent = "Connetado!";
    document.getElementById("messagesList").appendChild(li);
}).catch(function (err) {
    return console.error(err.toString());
});
C# (C Sharp) Básico
Curso de C# (C Sharp) Básico
CONHEÇA O CURSO

Agora ao executar a aplicação e realizar ações no endpoint Todo, o SignalR informará os clientes conectados no Hub:

Outro uso comum do SignalR é na criação de um chat. No próximo artigo abordarei mais algumas características dele e apresentarei o exemplo de um chat.

Você pode obter o código da aplicação demostrado neste arquivo no meu Github.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200