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:

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:
pathtrebuie 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ă înpath.
Î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șipublic+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.localhostaplicatiamea.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.localhostaplicatia-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:
localhost127.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:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
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()}`)
}
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()}`)
}
uidCamera = _ws.path().getString("uid")
dbCamera = _db.get("camera", uidCamera)
if dbCamera == null:
_log.warn(f"Cameră invalidă {uidCamera}.")
_ws.close()
else:
_log.info(f"Sesiune WebSocket nouă: {_ws.sessionId()}")
uidCamera = _ws.path().getString("uid")
dbCamera = _db.get("camera", uidCamera)
if dbCamera == null
_log.warn("Cameră invalidă #{uidCamera}.")
_ws.close()
else
_log.info("Sesiune WebSocket nouă: #{_ws.sessionId()}")
end
val uidCamera = _ws.path().getString("uid")
val dbCamera = _db.get("camera", uidCamera)
if (dbCamera == null) {
_log.warn("Cameră invalidă ${uidCamera}.")
_ws.close()
} else {
_log.info("Sesiune WebSocket nouă: ${_ws.sessionId()}")
}
def uidCamera = _ws.path().getString("uid")
def 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:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`Sesiunea ${_ws.sessionId()} a trimis acest mesaj: ${_ws.message()}`)
_log.info(`Sesiunea ${_ws.sessionId()} a trimis acest mesaj: ${_ws.message()}`)
_log.info(f"Sesiunea {_ws.sessionId()} a trimis acest mesaj: {_ws.message()}")
_log.info("Sesiunea #{_ws.sessionId()} a trimis acest mesaj: #{_ws.message()}")
_log.info("Sesiunea ${_ws.sessionId()} a trimis acest mesaj: ${_ws.message()}")
_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.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`Sesiunea ${_ws.sessionId()} s-a deconectat.`)
_log.info(`Sesiunea ${_ws.sessionId()} s-a deconectat.`)
_log.info(f"Sesiunea {_ws.sessionId()} s-a deconectat.")
_log.info("Sesiunea #{_ws.sessionId()} s-a deconectat.")
_log.info("Sesiunea ${_ws.sessionId()} s-a deconectat.")
_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ă.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
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 (_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 _ws.isStream():
while _ws.awaitStream():
if _ws.isBinaryStreamed():
_log.info("Mesaj binar nou primit: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elif _ws.isTextStreamed():
_log.info("Mesaj text nou primit: "+ _ws.textStreamed())
_ws.sendText("Ok! Mesajul a fost primit cu succes.")
if _ws.isStream()
while _ws.awaitStream()
if _ws.isBinaryStreamed()
_log.info("Mesaj binar nou primit: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elsif _ws.isTextStreamed()
_log.info("Mesaj text nou primit: "+ _ws.textStreamed())
end
_ws.sendText("Ok! Mesajul a fost primit cu succes.")
end
end
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 (_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
GETva 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.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
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)
)
)
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)
)
)
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)
)
)
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)
)
)
val 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)
)
)
final 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:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
// UID-ul trimis este obținut direct din cerere.
const uid = _req.getString('uid')
// UID-ul trimis este obținut direct din cerere.
const uid = _req.getString('uid')
# UID-ul trimis este obținut direct din cerere.
uid = _req.getString('uid')
# UID-ul trimis este obținut direct din cerere.
uid = _req.getString('uid')
// UID-ul trimis este obținut direct din cerere.
val uid = _req.getString('uid')
// UID-ul trimis este obținut direct din cerere.
final 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
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_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")
)
_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")
)
_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")
)
_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")
)
_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")
)
_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șipublic+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, parteaaplicatia-meatrebuie ajustată la numele corespunzător al aplicației Netuno. Dacă există_în numele aplicației, atunci în adresaHostva fi înlocuit cu-; de exemplu, aplicațiaaplicatia_meava avea valoareaHostastfel: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]