Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Websocket szerver Express alkalmazásban

Szűrő megjelenítése

Websocket szerver Express alkalmazásban

Kezdjük azzal, hogy mire jó nekünk egy websocket kapcsolat? A websocketek kétirányú, aszinkron kapcsolatot tudnak fenntartani a csatlakozott felek között. Chat szolgáltatások, játékok, valósidejű információs portálrendszerek, igazából elég sok felhasználási lehetősége van (vagy ahol nagyobb flexibilitásra van szükség, az alacsonyabb szintű TCP socketeket használják). A websocketek legnagyobb előnye, hogy egyrészt nagyon könnyen integrálhatóak "klasszikus" HTTP(S) webszerverekkel, másrészt az üzenetek "összeragasztása" nem a mi feladatunk (byte-streamekről lévén szó, TCP socket esetén még nekünk kell foglalkozni a byte-stream értelmezhető egységekké való visszaállításáról).

A websocket életciklusa egy standard HTTP kérésként indul, az egyetlen szembetűnő különbség azon a ponton csupán a megszokottól eltérő protokoll, ahogyan a kliens megszólítja a szervert: ws (titkosítatlan, a http megfelelője), illetve wss (titkosított, mint a https). A portok is lehetnek azonosak, tehát a 80 és a 443 továbbra is tökéletes választás. Az igazi különbség a kliens és a szerver között váltott első két üzenetben mutatkozik meg. A kliens egy GET kérést küld a HTTP szerver socket endpointjára, benne egy Upgrade: websocket és Connection: upgrade header-párral. Ez a két header jelzi a szerver számára, hogy a kliens a továbbiakban websocketen keresztül szeretne kommunikálni. Ha a szerver is képes erre, ugyanezzel a két headerrel válaszol, HTTP 101 Switching Protocols státusz kíséretében.

Ettől a ponttól kezdve a két fél egy aktív, kétirányú, folyamatosan nyitott kommunikációs vonalat használ, mely teljesen esemény-vezérelt. Mindkét fél látja, ha a másik üzenetet küldött (message esemény), ha bezárta a kapcsolatot (close esemény - szándékosan, vagy hálózati hiba miatt), illetve ha bármilyen hiba lépett fel a socket működésében (error esemény). Az open esemény speciális, akkor következik be, ha a websocket kapcsolat létrejött a két fél között -- ezt fel tudjuk használni például a kliens authentikálására. A websocketen átküldött üzenetek formátuma, kódolása, tartalma az implementáló felek közötti contracton múlik, teljesen tetszőleges, persze a legtöbb esetben ajánlott valamilyen strukturált formátumot, például JSON-t használni.

Az Express kibővítése websocket szerverré

Egy kisebb trükköt kell eljátszanunk az Express-szel, hogy az képes legyen működni websocket szerverként is. Pontosítanék, nem az Express fog websocket szerverként működni, hanem ő és a websocket szerver implementáció fognak osztozni egy közös HTTP szerveren. Az Express a háttérben egy standard, a Node.js runtimeban jelenlévő HTTP szervert épít, erre "köti rá" az endpointokat, amiket regisztrálunk benne, tehát a tényleges HTTP forgalmat nem az Express, hanem ez az alacsony szintű HTTP szerver kezeli. A jó hír számunkra, hogy pont erre lesz szükségünk a websocket szerver esetében is, a még jobb hír pedig az, hogy az Express a .listen() hívás visszatéréseként rendelkezésünkre is bocsátja az említett HTTP szervert (megjegyzés: kézzel is össze lehet rakni a HTTP szervert és azt odaadni az Express-nek, de így egyszerűbb).

A websocket szerver létrehozására a ws npm libraryt fogjuk használni (cikk végén linkelem). A létrejött HTTP szerver példányt kell majd átadnunk a WebSocketServer konfigurációjának (ha ezt nem tesszük meg, az is megpróbál készíteni egy saját HTTP szerver példányt, amit most szeretnénk elkerülni). A websocket szerveren regisztrálnunk kell egy connection esemény figyelőt, ezen keresztül tud reagálni a szerver alkalmazás a kliensek kapcsolódási kéréseire. A ws library, amennyiben egy kliens sikeresen kapcsolódott, egy Socket példányt hoz létre, innentől ezen a példányon keresztül tudunk kommunikálni a klienssel (kimenő és bejövő üzenetek tekintetében egyaránt).

A szerver létrehozása az alábbi módon történik:

// Az Express-re bízzuk a HTTP szerver létrehozását ...
const server = this.express
    // ...
    .listen(this.serverConfig.port, this.serverConfig.host, () => {
        // ...
    });

// ... majd azt használjuk a WebSocketServer inicializálására
this.webSocketServer = new WebSocketServer({
    server: server,
    clientTracking: true,
    path: "/agent"
});

Az alábbi kódrészlet pedig a kliensek kapcsolódási kéréseit figyeli:

// a callbackben levú socket példány innentől mindig erre a konkrét kommunikációs vonalra vonatkozik
this.webSocketServer.on("connection", (socket, request) => {

    // sikeres kapcsolódás esetén authorizáljuk azt
    if (!this.agentAuthorizer.authorize(socket, request)) {
        return;
    }

    // csatoljuk az esemény-figyelőket
    this.socketHandler.attachListener(socket);
});

A fenti néhány sorral elértük azt, hogy az Express és a WebSocketServer egy közös HTTP szerveren osztoznak majd, egyetlen port lesz nyitva az alkalmazáson, ahol a standard HTTP és a wwebsocket kommunikáció is lebonyolítható. A fenti konfiguráció konkrétan a /agent endpointot foglalja le websocket kommunikációs célokra, a kliensnek erre a konkrét endpointra kell kapcsolódnia. Az összes többi forgalom standard HTTP (REST) kommunikáció marad, melyet az Express kezel.

Fontos megjegyezni, hogy amennyiben proxy mögött van a szerverünk (például Apache HTTPD VirtualHost proxykat), a proxy szerveren is be kell kapcsolni a websocket támogatást, illetve wss kapcsolat esetén a megfelelő TLS certificate konfigurálása is szükséges.

Kommunikáció a socketen át

Amennyiben a szerver akar üzenni a kliensnek, a kapcsolódás óta már létezik a socket objektum, amin keresztül ezt megteheti. Az üzenet elküldése a socket objektum .send() metódusával történik, mely az elküldendő üzenetet várja bármilyen könnyen szeralizálható formátumban (string, byte-array, Buffer, stb.) illetve opcionálisan egy callback függvényt, amit sikeres elküldés esetén hív meg.

// a JSON-string tökéletes választás az elküldéshez
const jsonResponse = JSON.stringify(message);
// a socket változó egy WebSocket objektum a ws libraryből
socket.send(jsonResponse, callback);

Kliens oldalon ugyanez a helyzet, csak a socket objektumot neki kell létrehoznia (ez egyúttal meg is nyitja a kapcsolatot a szerverrel).

const socket = new WebSocket("http://localhost:9999/agent", {
    // headerként küldhetünk például authentikációhoz szükséges információt
    headers: {}
});

Az üzenetek fogadása mindkét oldalon az on-message esemény figyelő implementálásával történik:

// a data paraméter egy byte-stream, WebSocket.RawData típusba csomagolva
// a .toString() metódus meghívásával visszakapjuk az eredeti üzenetet
socket.on("message", data => {
    const message = JSON.parse(data.toString());
    // az üzenet itt már feldolgozható formátumban van
});

A konkrét use case a Domino esetében

Az új Domino egy nagyobb lélegzetvételű architekturális átalakításon esett át, melynek egyik célja az volt, hogy a Domino képes legyen több szervert is központosítottan kezelni. Tehát egyetlen koordinátor fut, az API, és több kisebb "agent" csatlakozik a koordinátorhoz. A regisztrált alkalmazások konfigurációja határozza meg, melyik alkalmazást melyik szerverre kell telepíteni. Ehhez alapesetben arra volna szükség, hogy minden agent rendelkezzen egy publikus endpointtal, melyen keresztül az megszólítható a deployment elindításához -- ez egyrészt teljesen értelmetlen, másrészt nem is igazán biztonságos. Ehelyett, az agentek csatlakoznak a koordinátorhoz, a standard HTTP kommunikáció viszont ahhoz kevés, hogy az agentet is meg tudja ezután szólítani a koordinátor. Erre a megoldás az, hogy a koordinátor websocket szerverként is tud üzemelni, így az agentek kétirányú kapcsolatot tudnak azzal nyitni, lehetővé téve, hogy a csatlakozott agenteket a koordinátor bármikor megszólítsa.

WebSocket szerver implementáció az új Domino-ban

WebSocket kliens implementáció az új Domino-ban

WS NPM library

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.