Využití WebSocket protokolu pomocí SignalR

14.11.2020
SignalR je open source framework, který umožňuje oboustrannou komunikaci mezi klientem a serverem. Framework používá WebSocket protokol, ale je možné nastavit i fallback na server-sent eventy nebo techniku zvanou Long Polling. Díky oboustranné komunikaci přes TCP spojení je možné ze serveru posílat data přímo klientovi, aniž by klient musel aktualizovat stránku nebo znova navazovat HTTP komunikaci. To umožňuje velmi rychlý přenos informací, který se dá využít v mnoha aplikacích (chat, hry, real time dashboardy, práce s mapou, informování o stavu běžících úloh, ...). Je možné streamovat i video/audio, ale tady bych zvážil i framework WebRTC, který se na přenos video/audio dat specializuje a umožňuje P2P komunikaci.

Slovníček

WebSocket Aplikační protokol, umožňující obousměrnou komunikaci mezi serverem a webovým prohlížečem. Standardizován byl v roce 2011 a implementován napříč prohlížeči byl zhruba kolem roku 2013.
Fallback Záložní řešení, které se použije pokud primární řešení z nějakého důvodu selže.
Server-sent event Technologie umožňující posílat data ze serveru klientovi, implementována do prohlížečů byla zhruba kolem roku 2011 (s výjimkou IE).
Long Polling Technika, kdy klient pošle serveru HTTP požadavek a pokud server nemá k dispozici nová data, tak na požadavek neodpoví a nechá ho timeoutovat. Klient po timeoutu nebo odpovědi od serveru posílá nový požadavek.
P2P Peer-to-peer je komunikace mezi dvěma počítači (napřímo bez serveru jako prostředníka).
SignalR Hub API na backendu, které se používá pro WebSocket komunikaci.

Obsah

Popis řešení

Představa je taková, že webový server poskytne html stránku, která umožní uživatelům kreslit do <canvas> html tagu, pomocí javascriptu. Javascript ale veškeré souřadnice pohybu myši bude zároveň posílat na server, který souřadnice předá dalším připojeným uživatelům zavoláním javascriptové funkce, kterou každý uživatel bude mít staženou ve svém prohlížeči

Implementace

Jako webový framework jsem zvolil ASP.NET Core 3, jehož je SignalR součástí.

Backend

Ve Startup.cs zaregistrujeme middleware a nadefinujeme adresu k Hub API

public void ConfigureServices(IServiceCollection services)
{
  services.AddControllersWithViews();
  services.AddSignalR();
}
app.UseEndpoints(endpoints =>
{
  endpoints.MapControllerRoute(
      name: "default",
      pattern: "{controller=Home}/{action=Index}/{id?}");

  endpoints.MapHub<DrawingHub>("/drawinghub");
});

nyní aplikace ví, že má veškeré požadavky na adresu \drawinghub přeposílat třídě DrawingHub, kterou ale musíme nejdříve vytvořit. Vytvoříme si tedy novou třídu s velice jednoduchou API funkcí DrawLine pro přeposílání souřadnic

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SharedDrawing.Hubs
{
    public class DrawingHub : Hub
    {
        public async Task DrawLine(int x1, int y1, int x2, int y2)
        {
            await Clients.Others.SendAsync("DrawLine", x1, y1, x2, y2);
        }
    }
}

Frontend

Pro kreslení si vypůjčím kód z mozilla stránek, který jen trochu upravím. Uživatel musí mít také stáhnutého SignalR klienta, v našem případě javascriptovou knihovnu, která bude zajišťovat komunikaci se serverem. HTML, CSS a odkazy na JS scripty vložíme do index.html.

<style>
  canvas {
      border: 1px solid black;
      width: 365px;
      height: 200px;
  }
</style>

<p>Shared drawing</p>
<canvas id="myPics" width="365" height="200"></canvas>

@section Scripts {
  <script src="https://cdnjs.cloudflare.com/ajax/libs/aspnet-signalr/1.1.4/signalr.min.js"></script>
  <script src="~/js/drawingHub.js"></script>
}

Vytvoříme také drawingHub.js, na který se odkazujeme. Obsahuje mouse eventy, ke kterým přidáme SignalR funkce na připojení k serveru connection.start(), na poslouchání příchozích příkazů connection.on() a do drawLine funkce přidáme volání Hub API connection.invoke(). Jedná se o tři klíčové funkce pro komunikaci s API. DrawLine funkce je rozšířená o parametr callSignalR, který je nastavený na false, v případě kdy se funkce volá z příchozího volání serveru. Parametr řídí přeposílání souřadnic zpátky na server, aby nedošlo k vytvoření nekonečné smyčky.

/* Drawing events
 * https://developer.mozilla.org/en-US/docs/Web/API/Element/mousemove_event
 **********************************/

// When true, moving the mouse draws on the canvas
let isDrawing = false;
let x = 0;
let y = 0;

const myPics = document.getElementById("myPics");
const context = myPics.getContext("2d");

// event.offsetX, event.offsetY gives the (x,y) offset from the edge of the canvas.

// Add the event listeners for mousedown, mousemove, and mouseup
myPics.addEventListener("mousedown", e => {
    x = e.offsetX;
    y = e.offsetY;
    isDrawing = true;
});

myPics.addEventListener("mousemove", e => {
    if (isDrawing === true) {
        drawLine(context, x, y, e.offsetX, e.offsetY);
        x = e.offsetX;
        y = e.offsetY;
    }
});

window.addEventListener("mouseup", e => {
    if (isDrawing === true) {
        drawLine(context, x, y, e.offsetX, e.offsetY);
        x = 0;
        y = 0;
        isDrawing = false;
    }
});

/* SignalR
 **********************************/

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

// connect to the server
connection
    .start()
    .then(function () {
        console.log("connected to the drawing hub");
    })
    .catch(function (error) {
        return console.error(error.toString());
    });

// listen to remote procedure calls (RPC)
connection.on("DrawLine", function (x1, y1, x2, y2) {
    console.log("DrawLine", x1, y1, x2, y2);
    drawLine(context, x1, y1, x2, y2, callSignalR = false);
});

// function is called via mousemove and mouseup events
function drawLine(context, x1, y1, x2, y2, callSignalR = true) {
    context.beginPath();
    context.strokeStyle = "black";
    context.lineWidth = 1;
    context.moveTo(x1, y1);
    context.lineTo(x2, y2);
    context.stroke();
    context.closePath();

    // check if we want to call server, to prevent looping
    if (callSignalR) {
        // call DrawLine action on the server.
        connection.invoke("DrawLine", x1, y1, x2, y2)
            .catch(function (err) {
                return console.error(err.toString());
            });
    }
}

Výsledek

Nyní máme vše připravené a můžeme aplikaci vyzkoušet. Aplikaci spustíme přímo z visual studia (IIS Express), otevřeme dvě okna prohlížeče a můžeme z obou stran kreslit :-) Připojit se může i více uživatelů, protože server nám souřadnice přeposílá na všechny připojené klienty.

Odkazy