Sari la conținutul principal

WebSocket

Mesaje instantanee între server și client.

Introducere​

Cu WebSocket este posibil să se stabilească o conexiune permanentă între server și browserul clientului.

Aceasta înseamnă că, în orice moment, browserul poate folosi conexiunea stabilită prin WebSocket pentru a trimite date către server, fără a fi obligatoriu să primească un răspuns. Același lucru este valabil și pentru server, care poate contacta browserul clientului trimițând date imediat.

Comunicarea se realizează instantaneu, iar în orice moment este posibil să transmitem date dintr-o parte în cealaltă în mod independent și imediat.

Este foarte util pentru operații în timp real cu comunicări imediate; este ca un chat, o conversație între browser și server, în care oricând oricare parte poate trimite un mesaj, iar cealaltă îl primește.

De exemplu, este foarte utilizat de la soluții generale de chat până la jocuri; îl folosim și pentru a prezenta modificări de date în direct, adică atunci când există nevoia de a realiza o comunicare cât mai imediată posibil.

Pe scurt, diferența dintre comunicarea clasică HTTP și WebSocket:

HTTP clasic vs WebSocket

Pe partea de server este creat un endpoint, o adresă care permite stabilirea conexiunilor WebSocket.

Serverul are capacitatea de a trimite date doar către o conexiune specifică; adică este posibil să trimită mesaje noi unui participant anume dintr-un chat.

De asemenea, serverul poate face broadcast, adică trimiterea de date către toate conexiunile active de la o anumită adresă, adică atunci când un mesaj este trimis tuturor participanților la chat care se află într-o cameră sau într-un grup.

Activare și configurare​

Pentru a activa și configura WebSocket în aplicația Netuno, este necesar să editați fișierul de configurare al aplicației pentru mediul utilizat, cum ar fi:

  • 📂 config/_development.json
  • 📂 config/_production.json

Introduceți și ajustați următorii parametri:

...
"ws": {
"hosts": [...], "<-- OPȚIONAL": "mai multe în documentația de mai jos.",
"endpoints": [
{
"name": "grup",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/grup",
"path": "/",
"service": "/services/ws/grup"
},
{
"name": "camera",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/camera",
"path": "/{uid}",
"service": "/services/ws/camera"
}
]
},
...

endpoints​

Este un array care permite definirea faptului că serverul va suporta WebSocket prin multiplele adrese publice specificate (endpoints); astfel, fiecare endpoint este o adresă publică ce suportă primirea conexiunilor prin WebSocket de la browsere.

name​

Numele de identificare al endpoint-ului.

enabled​

Permite activarea și dezactivarea endpoint-ului; valoarea implicită este true (activ).

sendTimeout​

Limita de timp maximă pentru trimiterea unui mesaj către clienți; valoarea implicită este 60000 milisecunde, adică echivalează cu 1 minut.

idleTimeout​

Limita de timp maximă pentru inactivitatea conexiunii; valoarea implicită este 300000 milisecunde, adică echivalează cu 5 minute.

maxText​

Limita în bytes pentru dimensiunea maximă a mesajului; valoarea implicită este 1048576, echivalentă cu 1 megabyte.

public​

Definește baza adresei publice care va permite primirea conexiunilor prin WebSocket venite din browsere.

Adresa finală este formată din valoarea configurației public, urmată de ceea ce este definit în path.

path​

Definește partea finală a căii adresei publice; poate fi dinamică, ceea ce permite crearea, de exemplu, a mai multor canale separate. De exemplu, este util pentru a crea mai multe camere.

Atenție: path trebuie să înceapă întotdeauna cu /, iar în configurația conexiunii din front-end este foarte important să conțină bara exact așa cum este definită în path.

În front-end, adresa finală va fi public + path; vedeți exemplul:

...
"public": "/ws/grup",
"path": "/",
...

Atunci adresa finală în front-end va fi /ws/grup/, cu bara la final.

service​

Adresa bazei serviciului de back-end al aplicației care procesează conexiunile și comunicarea prin WebSocket.

hosts​

Opțional, putem defini hosturile la care endpoint-urile noastre sunt disponibile; adică, la conectare vine adresa Host din headerul HTTP, iar acest host este considerat doar informația de domeniu sau IP, fiind eliminată indicația portului. Aceasta este folosită pentru a asocia și găsi endpoint-ul corespunzător.

Prin urmare, endpoint-ul, pentru a funcționa corect, trebuie să combine host și public + path, astfel:

ws://[host]:9000[public][path]

În producție, cu NGINX, proxy invers și SSL, ar trebui să fie astfel:

wss://[host][public][path]

Netuno definește unele domenii locale cu prefixul numelui aplicației, care se asociază automat ca valoare implicită pentru hosturile endpoint-urilor; în plus, în aplicația în dezvoltare adaugă și localhost și 127.0.0.1 la hosturile implicite. De aceea definirea hosturilor este opțională, pentru că există această configurare automată.

Următoarele hosturi sunt definite automat pentru endpoint-uri, dacă numele aplicației este aplicatiamea:

  • Acesta direcționează direct către IP-ul local 127.0.0.1:
    • aplicatiamea.local.netu.no
  • Pentru ca acestea să funcționeze, este necesar să le înregistrați în fișierul hosts al sistemului de operare:
    • aplicatiamea.localhost
    • aplicatiamea.local

Dacă numele aplicației este aplicatia_mea, atunci hosturile automate devin:

  • Acesta direcționează direct către IP-ul local 127.0.0.1:
    • aplicatia-mea.local.netu.no
  • Pentru ca acestea să funcționeze, este necesar să le înregistrați în fișierul hosts al sistemului de operare:
    • aplicatia-mea.localhost
    • aplicatia-mea.local

În domeniu nu este permis _ (underscore), iar în numele aplicațiilor nu este permis - (cratimă); de aceea extern folosim - (cratimă) în adresă, dar Netuno transformă automat - (cratimă) în _ (underscore) pentru a găsi aplicația corectă.

Aplicația implicită sau aplicația definită la inițializarea serverului Netuno are suplimentar următoarele hosturi:

  • localhost
  • 127.0.0.1

Pentru a defini hosturi personalizate, folosim următoarele configurații:

...
"ws": {
"endpoints": [
{
"hosts": ["domeniul-meu1.com", "www.domeniul-meu1.com"],
...
},
{
"hosts": ["domeniul-meu2.com", "www.domeniul-meu2.com"],
...
}
]
},
...

Dacă toate endpoint-urile funcționează pentru aceleași hosturi, atunci putem defini global astfel:

...
"ws": {
"hosts": ["domeniul-meu.com", "www.domeniul-meu1.com"],
"endpoints": [
...
]
},
...

Asigurați-vă că în adresa finală WebSocket folosiți întotdeauna un domeniu sau IP care este un host valid; altfel nu va funcționa, deoarece endpoint-ul nu va putea fi găsit intern.

Serviciu pentru endpoint-uri​

POST = conexiune nouă​

Serviciul configurat este executat cu metoda POST când este stabilită o conexiune nouă, de exemplu:

server/services/ws/camera/post.js
const uidCamera = _ws.path().getString("uid")
const dbCamera = _db.get("camera", uidCamera)

if (dbCamera == null) {
_log.warn(`Cameră invalidă ${uidCamera}.`)
_ws.close()
} else {
_log.info(`Sesiune WebSocket nouă: ${_ws.sessionId()}`)
}

De obicei, _ws.sessionId() este salvat în baza de date pentru a fi utilizat în comunicări viitoare.

PUT = mesaj nou​

Dacă mesajul primit nu este în format JSON sau dacă este un JSON care nu este în formatul suportat pentru executarea unui serviciu specific (service), atunci va fi executat acest serviciu generic:

server/services/ws/camera/put.js
_log.info(`Sesiunea ${_ws.sessionId()} a trimis acest mesaj: ${_ws.message()}`)

DELETE = conexiune închisă​

Este executat când are loc o deconectare a browserului clientului.

server/services/ws/camera/delete.js
_log.info(`Sesiunea ${_ws.sessionId()} s-a deconectat.`)

GET = flux continuu de mesaje​

Începe execuția când are loc o conexiune nouă a browserului clientului, iar execuția este menținută până la încheierea conexiunii.

Adică serviciul pentru metoda HTTP GET primește continuu mesajele care vin de la client în stream; este un flux deschis și continuu de mesaje care poate atât să primească, cât și să trimită.

server/services/ws/camera/get.js
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Mesaj binar nou primit: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Mesaj text nou primit: "+ _ws.textStreamed())
}
_ws.sendText("Ok! Mesajul a fost primit cu succes.")
}
}

if cu _ws.isStream() verifică dacă este într-adevăr o cerere internă gestionată de WebSocket pentru realizarea streamului.

În while cu _ws.awaitStream() se așteaptă sosirea unui nou flux de mesaje, adică se așteaptă sosirea unui mesaj nou. Imediat ce există un mesaj nou, este executat codul intern al while.

_ws.awaitStream() funcționează ca o buclă infinită care așteaptă un mesaj nou.

Apoi, în if-urile interne din while se verifică tipul mesajului, dacă mesajul este binar sau text; în ambele cazuri, conținutul acestor mesaje este afișat în loguri. Observați că poate fi utilizat orice format de mesaj, iar acestea pot fi procesate în orice mod convenabil.

În final, în _ws.sendText este trimis un mesaj de răspuns către client, confirmând recepția.

Fiecare execuție a serviciului GET va fi o conexiune activă.

Când conexiunea cu endpoint-ul WebSocket este finalizată, execuția acestui serviciu se încheie intern.

Comunicare​

sendService​

Execută serviciul și trimite output-ul generat către un client specific conectat prin WebSocket; utilizează ID-ul de conexiune al clientului (sessionId).

Exemplu de apel al serviciului​

Codul de mai jos poate fi utilizat în serviciul pentru metoda PUT al endpoint-ului (service); observați că este folosit _ws.sessionId(), care obține ID-ul sesiunii de conexiune a clientului. Dacă identificatorul sesiunii este salvat în baza de date, sendService ar putea fi executat în orice alt serviciu, în orice moment.

const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identificarea conexiunii WebSocket curente.
_val.map() // Parametrizarea serviciului care va fi executat.
.set("method", "POST")
.set("service", "/services/camera/participant/nou")
.set(
"data", // Parametrii care vor fi trimiși serviciului și primiți prin _req.
_val.map()
.set("uid", uid)
)
)

Adică, în configurație path-ul configurat în endpoint are valoarea /{uid}, permițând clientului să se conecteze prin adresa publică:

  • /ws/camera/d641f095-30eb-4025-a39c-b2e3e497eab7

Atunci valoarea lui uid va fi d641f095-30eb-4025-a39c-b2e3e497eab7.

Apoi va fi executat serviciul care se află la URL-ul /services/camera/participant/nou, iar valoarea lui uid este transmisă.

uid poate fi obținut în codul serviciului executat, ca parametru al cererii HTTP; vedeți un exemplu:

/server/services/camera/participant/nou.js
// UID-ul trimis este obținut direct din cerere.
const uid = _req.getString('uid')

broadcastService​

Pentru a realiza broadcast este necesar numele endpoint-ului WebSocket, definirea căii (path) dacă este cazul și serviciul care va fi executat.

Output-ul serviciului va fi trimis către toate conexiunile active din endpoint-ul specificat.

Exemplu de broadcast​

/server/services/camera/participant/nou.js
_ws.broadcastService(
"admin", // Numele endpoint-ului WebSocket.
"/", // Calea configurată în endpoint (path).
_val.map() // Parametrizarea serviciului care va fi executat.
.set("service", "/services/camera/participant/lista")
)

În acest exemplu, conținutul output-ului generat de serviciul camera/participant/lista va fi trimis tuturor celor conectați la endpoint-ul admin.

Client WS - NPM​

Pentru a facilita integrarea cu frontend-ul, este disponibil modulul NPM:

Comandă de instalare:

  • bun install @netuno/ws-client
import _ws from '@netuno/ws-client';

_ws.config({
url: 'ws://localhost:9000/ws/exemplu/',
servicesPrefix: '/services',
autoReconnect: true,
connect: (event) => {
console.info('WebSocket conectat', event);
},
close: (event) => {
console.warn('WebSocket închis', event);
},
error: (error) => {
console.error('Eroare WebSocket', error);
},
message: (data, event) => {
console.debug('Mesaj WebSocket', data);
}
});

Asigurați-vă că URL-ul combină corect valorile configurate în endpoint pentru host și public + path, astfel:

ws://[host]:9000[public][path]

Iar URL-ul de producție cu integrarea NGINX, proxy invers și SSL va fi probabil astfel:

wss://[host][public][path]

Apoi puteți iniția conexiunea:

_ws.connect();

Executarea serviciului prin WebSocket​

Putem executa servicii ale API-ului REST prin conexiunea WebSocket; aceasta înseamnă că nu va fi realizată nicio cerere HTTP, astfel încât datele cererii sunt trimise ca mesaj, iar răspunsul serviciului este obținut ca mesaj primit înapoi.

Înainte de a începe, trebuie să creăm un listener pentru răspunsul serviciului, de exemplu:

const referintaListener = _ws.addListener({
method: 'GET', // Opțional; omite pentru a asculta toate metodele.
service: 'calea/serviciului/meu/aici',
success: (data) => {
console.log('Răspunsul serviciului executat prin WebSocket:', data);
},
fail: (error)=> {
console.log('Serviciul executat prin WebSocket a eșuat:', error);
}
});

Acum putem trimite o cerere de executare a serviciului; răspunsul serviciului va fi obținut în listenerul creat mai sus. Executăm serviciul astfel:

_ws.sendService({
method: 'GET', // Opțional; cererile folosesc GET când este omis.
service: 'calea/serviciului/meu/aici',
data: {
message: 'Salut...'
}
});

Pentru a elimina listenerul adăugat, utilizați:

_ws.removeListener(referintaListener);

Și pentru a închide conexiunea WebSocket:

_ws.close();

Proxy invers NGINX​

În NGINX, proxy-ul invers poate fi configurat astfel:

location /ws {
proxy_pass http://localhost:9000;
proxy_set_header Host aplicatia-mea.local.netu.no;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_http_version 1.1;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
add_header X-Frame-Options "";
}

La linia 3, unde este definit Host, partea aplicatia-mea trebuie ajustată la numele corespunzător al aplicației Netuno. Dacă există _ în numele aplicației, atunci în adresa Host va fi înlocuit cu -; de exemplu, aplicația aplicatia_mea va avea valoarea Host astfel: aplicatia-mea.local.netu.no.

Reporniți NGINX pentru ca configurația să intre în vigoare.

Domeniile configurate în NGINX, adică parametrizarea server_name, nu trebuie să fie în hosturile endpoint-ului din aplicația Netuno, deoarece în configurația de proxy de mai sus Hostul din headerul HTTP este schimbat într-o adresă implicită care este întotdeauna configurată automat de Netuno, atâta timp cât prefixul cu numele aplicației este corect.

Dacă serverul NGINX utilizează portul 80, atunci URL-ul final va fi:

  • ws://[host][public][path]

Iar cu SSL/HTTPS, dacă serverul NGINX utilizează portul 443, atunci URL-ul final va fi:

  • wss://[host][public][path]