Streaming API

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) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
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# - Fundamentos
Curso de C# - Fundamentos
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.

ASP.NET Core – Criando uma Streaming API

Quando falamos de streaming de dados, a primeira coisa que vem a cabeça são streaming mídia: áudio e mídia. Mas em algumas situações, pode ser necessário a implementação de streaming de dados textuais.

Utilizado para a entrega de dados em tempo real, o streaming de dados em uma API é uma técnica utilizada principalmente por redes sociais, como Twitter e Facebook.

No .NET Core este tipo de recurso pode ser implementado via SignalR, mas neste artigo exemplificaremos isso utilizando a estrutura tradicional do ASP.NET Core. Futuramente abordarei o uso do SignalR.

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

Criando uma Web API Simples

Para exemplificar a API Streaming, inicialmente iremos criar uma API simples:

dotnet new web -n ApiSimples

Que conterá apenas um model:

namespace ApiSimples.Models
{
    public class Item
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }

        public override string ToString() => $"{Id} - {Name} - {IsComplete}";
    }
}

E um controller:

namespace ApiSimples.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoController : ControllerBase
    {
        private static List<Item> _itens = new List<Item>();

        [HttpGet]
        public ActionResult<List<Item>> Get() => _itens;

        [HttpPost]
        public async Task<ActionResult<Item>> Post([FromBody] Item value)
        {
            if(value == null)
                return BadRequest();

            if(value.Id == 0)
            {
                var max = _itens.Max(i => i.Id);
                value.Id = max+1;
            }

            _itens.Add(value);
            return value;
        }

        [HttpPut("{id}")]
        public async Task<ActionResult<Item>> Put(long id, [FromBody] Item value)
        {
            var item = _itens.SingleOrDefault(i => i.Id == id);
            if(item != null)
            {
                _itens.Remove(item);
                value.Id = id;
                _itens.Add(value);
                return item;
            }

            return BadRequest();
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> Delete(long id)
        {
            var item = _itens.SingleOrDefault(i => i.Id == id);
            if(item != null)
            {
                _itens.Remove(item);
                return Ok(new { Description = "Item removed" });
            }

            return BadRequest();
        }
    }
}

À frente este controller será modificado com adição do streaming.

Não se esqueça de adicionar o suporte para o controller em Startup:

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

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

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Adicionando o streaming de dados

No ASP.NET Core, ao acessar uma rota, a solicitação é processada e o seu resultado retornado para o usuário. Ao definir um streaming de dados, a solicitação se manterá em um processamento contínuo, retornando dados de um evento, enquanto a conexão se mantiver ativa.

Por não seguir o comportamento padrão, o streaming não pode retornar um objeto ActionResult, que é o tipo de retorno mais comum. Assim, a primeira coisa a ser feita é definir um ActionResult customizado:

public class StreamResult : IActionResult
{
    private readonly CancellationToken _requestAborted;
    private readonly Action<Stream, CancellationToken> _onStreaming;

    public StreamResult(Action<Stream, CancellationToken> onStreaming, CancellationToken requestAborted)
    {
        _requestAborted = requestAborted;
        _onStreaming = onStreaming;
    } 

    public Task ExecuteResultAsync(ActionContext context)
    {
        var stream = context.HttpContext.Response.Body;
        context.HttpContext.Response.GetTypedHeaders().ContentType = new MediaTypeHeaderValue("text/event-stream");
        _onStreaming(stream, _requestAborted);
        return Task.CompletedTask;
    }
}

Note que esta classe implementa a interface IActionResult. Assim, ela poderá ser utilizada no lugar de ActionResult.

O ponto mais importante dela é a definição do callback onStreaming, que é recebido por parâmetro:

public StreamResult(Action<Stream, CancellationToken> onStreaming, CancellationToken requestAborted)
{
  _requestAborted = requestAborted;
  _onStreaming = onStreaming;
} 

Este callback será utilizado para salvar os clients que estiverem “ouvindo” o streaming. E o CancellationToken será utilizado para finalizar o streaming caso a solicitação seja interrompida/cancelada.

Voltando ao controller, é necessário definir uma coleção para salvar os clients:

private static ConcurrentBag<StreamWriter> _clients = new ConcurrentBag<StreamWriter>();

Esta coleção precisa ser thread-safe, por isso que foi utilizada uma coleção do namespace System.Collections.Concurrent.

Agora é possível definir o endpoint do streaming de dados:

[HttpGet]
[Route("streaming")]
public IActionResult Streaming()
{
    return new StreamResult(
        (stream, cancelToken) => {
            var wait = cancelToken.WaitHandle;
            var client = new StreamWriter(stream);
            _clients.Add(client);

            wait.WaitOne();

            StreamWriter ignore;
            _clients.TryTake(out ignore);
        }, 
        HttpContext.RequestAborted);
}

Note que na função callback da classe StreamResult estamos adicionando o client na coleção e aguarda-se que a solicitação seja cancelada. Com isso, ao acessar o endpoint, a solicitação se manterá ativa.

No momento já temos um streaming de dados, mas nada está sendo enviado para os clients. Para isso, definiremos um método que irá percorrê-los e escreverá dados no stream:

private async Task WriteOnStream(Item data, string action)
{
    foreach (var client in _clients)
    {
        string jsonData = string.Format("{0}\n", JsonSerializer.Serialize(new { data, action }));
        await client.WriteAsync(jsonData);
        await client.FlushAsync();
    }
}

Os dados escritos, serão as ações do nosso controller:

public async Task<ActionResult<Item>> Post([FromBody] Item value)
{
    if(value == null)
        return BadRequest();

    if(value.Id == 0)
    {
        var max = _itens.Max(i => i.Id);
        value.Id = max+1;
    }

    _itens.Add(value);

    await WriteOnStream(value, "Item added");

    return value;
}

[HttpPut("{id}")]
public async Task<ActionResult<Item>> Put(long id, [FromBody] Item value)
{
    var item = _itens.SingleOrDefault(i => i.Id == id);
    if(item != null)
    {
        _itens.Remove(item);
        value.Id = id;
        _itens.Add(value);

        await WriteOnStream(value, "Item updated");

        return item;
    }

    return BadRequest();
}

[HttpDelete("{id}")]
public async Task<ActionResult> Delete(long id)
{
    var item = _itens.SingleOrDefault(i => i.Id == id);
    if(item != null)
    {
        _itens.Remove(item);
        await WriteOnStream(item, "Item removed");
        return Ok(new { Description = "Item removed" });
    }

    return BadRequest();
}

Pronto, o nosso streaming de dados já está completo.

Windows Server 2016 - Internet Information Services
Curso de Windows Server 2016 - Internet Information Services
CONHEÇA O CURSO

123 Testando…

Para testar a API, inicialmente é necessário acessar o endpoint do streaming de dados:

Note que o navegador indicará que a página está sendo carregada. Este é o comportamento padrão. Ao enviar uma solicitação para o controller, ela será mostrada nesta página:

A página sempre mostrará que está sendo carregada, mas todas as ações que forem geradas do nosso controller serão exibidas nela.

Simples, não é? Você pode ver esta aplicação completa no meu Github.

No próximo artigo mostrarei como obter isso utilizando o SignalR.

© 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