Skip to main content

WebSocket

Instant messages between the server and the client.

Introduction​

With WebSocket it is possible to establish a permanent connection between the server and the client's browser.

This means that at any time the browser can use the connection established via WebSocket to send data to the server, without being required to get a response. The same happens for the server, which can contact the client's browser by sending data immediately.

Communication is performed instantly and at any time it is possible to transfer data from one side to the other independently and immediately.

It is very useful for performing real-time operations with immediate communications; it is like a chat between the browser and the server, where at any time either side can send a message and the other receives it.

For example, it is widely used from general chat solutions to games; we also use it to present live data changes, that is, when there is a need to perform communication as immediately as possible.

In a simplified way, the difference between classic HTTP communication and WebSocket:

classic-http-vs-websoket

On the server side, an endpoint is created, an address that allows establishing WebSocket connections.

The server can send data only to a specific connection, meaning it is possible to send new messages to a specific chat participant.

The server can also perform a broadcast, which is sending data to all active connections at a given address, meaning when a message is sent to all chat participants who are in a room or group.

Activation and Configuration​

To activate and configure WebSocket in your Netuno application, you need to edit the application configuration file for the environment being used, such as:

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

Insert and adjust the following parameters:

...
"ws": {
"hosts": [...], "<-- OPTIONAL": "more about it in the documentation below.",
"endpoints": [
{
"name": "pool",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/pool",
"path": "/",
"service": "/services/ws/pool"
},
{
"name": "room",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/room",
"path": "/{uid}",
"service": "/services/ws/room"
}
]
},
...

endpoints​

It is an array that allows defining that the server will support WebSocket through multiple specified public addresses (endpoints), so each endpoint is a public address that supports receiving connections via WebSockets from browsers.

name​

Endpoint identification name.

enabled​

Allows enabling and disabling the endpoint; the default is true (enabled).

sendTimeout​

Maximum time limit for sending a message to clients; the default is 60000 milliseconds, which is equivalent to 1 minute.

idleTimeout​

Maximum inactivity time limit on the connection; the default is 300000 milliseconds, which is equivalent to 5 minutes.

maxText​

Byte limit for the maximum message length; the default is 1048576, equivalent to 1 megabyte.

public​

Defines the base of the public address that will allow receiving WebSocket connections from browsers.

The final address consists of the public configuration value followed by what is defined in path.

path​

Defines the final part of the public address path; it can be dynamic, which allows creating, for example, multiple separate channels, and is useful for creating multiple rooms.

Attention: path must always start with /, and in the front-end connection configuration it is very important to include the slash exactly as defined in path.

On the front-end the final address will be public + path, see the example:

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

So the final address on the front-end will be /ws/pool/, with the trailing slash.

service​

Address of the application's back-end service base that processes WebSocket connections and communication.

hosts​

Optionally we can define the hosts on which our endpoints are available; that is, the connection includes the Host address in the HTTP header, and exactly this host is considered using only the domain or IP information and discarding the port indication, which is what is used to associate and find the corresponding endpoint.

Therefore, for the endpoint to work correctly it must combine host and public + path, as follows:

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

In production with NGINX, reverse proxy and SSL it should look like this:

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

Netuno defines some local domains with the app name prefix, which it automatically associates as the default value for endpoint hosts; additionally, in the development app it also adds localhost and 127.0.0.1 to the default hosts, which is why defining hosts is optional, because this automatic configuration exists.

The following hosts are automatically defined for endpoints if the app name is myapp:

  • This one directs directly to the local IP 127.0.0.1:
    • myapp.local.netu.no
  • For these to work, they must be registered in the operating system's hosts file:
    • myapp.localhost
    • myapp.local

If the app name is my_app, then the automatic hosts become:

  • This one directs directly to the local IP 127.0.0.1:
    • my-app.local.netu.no
  • For these to work, they must be registered in the operating system's hosts file:
    • my-app.localhost
    • my-app.local

The domain does not allow _ (underscore), and app names do not allow - (hyphen), so externally we use - (hyphen) in the address, but Netuno automatically transforms - (hyphen) into _ (underscore) to find the correct application.

The default application or the application defined at Netuno server startup additionally has the following hosts:

  • localhost
  • 127.0.0.1

To define custom hosts we use the following configurations:

...
"ws": {
"endpoints": [
{
"hosts": ["my-domain1.com", "www.my-domain1.com"],
...
},
{
"hosts": ["my-domain2.com", "www.my-domain2.com"],
...
}
]
},
...

If all endpoints work for the same hosts, then we can define them globally like this:

...
"ws": {
"hosts": ["my-domain.com", "www.my-domain1.com"],
"endpoints": [
...
]
},
...

Make sure the final WebSocket address always uses a domain or IP that is a valid host; otherwise it will not work because the endpoint cannot be found internally.

Service for Endpoints​

POST = new connection​

The service configured with the POST method is executed when a new connection is established, for example:

server/services/ws/room/post.js
const uidRoom = _ws.path().getString("uid")
const dbRoom = _db.get("room", uidRoom)

if (dbRoom == null) {
_log.warn(`Invalid room ${uidRoom}.`)
_ws.close()
} else {
_log.info(`New WebSocket Session: ${_ws.sessionId()}`)
}

Usually _ws.sessionId() is stored in the database to be used in future communications.

PUT = new message​

If the received message is not in JSON format, or if it is a JSON that is not in the format supported for executing a specific service (service), then this generic service will be executed:

server/services/ws/room/put.js
_log.info(`The session ${_ws.sessionId()} sent this message: ${_ws.message()}`)

DELETE = connection closed​

It is executed when a client browser disconnection occurs.

server/services/ws/room/delete.js
_log.info(`Session ${_ws.sessionId()} has disconnected.`)

GET = continuous message stream​

It starts execution when a new client browser connection occurs, and execution is maintained until the connection ends.

That is, the service for the HTTP GET method keeps receiving messages coming from the client continuously in a stream; in other words, it is an open and continuous flow of messages that can both receive and send.

server/services/ws/room/get.js
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("New binary message received: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("New text message received: "+ _ws.textStreamed())
}
_ws.sendText("Ok! The message was received successfully.")
}
}

The if with _ws.isStream() checks whether it really is an internal request managed by WebSocket to perform the stream.

In the while with _ws.awaitStream(), it waits for a new message stream to arrive, that is, it waits for a new message to arrive. As soon as there is a new message, the internal while code is executed.

_ws.awaitStream() works like an infinite loop waiting for a new message.

Then in the internal ifs of the while, the message type is checked, whether the message is binary or text; in both cases the content of these messages is shown in the logs. Note that any message format can be used and can be processed in any convenient way.

Finally, in _ws.sendText a response message is sent to the client, confirming receipt.

Each execution of the GET service will be an active connection.

When the connection to the WebSocket endpoint is terminated, the execution of this service is finished internally.

Communication​

sendService​

Executes the service and sends the generated output to a specific client connected via WebSocket; it uses the client connection ID (sessionId).

Example of Service Call​

The code below can be used in the endpoint PUT method service (service); note that _ws.sessionId() is used, which gets the client connection session ID. If the session identifier is stored in the database, sendService could be executed in any other service at any time.

const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identification of the current WebSocket connection.
_val.map() // Parameters for the service that will be executed.
.set("method", "POST")
.set("service", "/services/room/participant/new")
.set(
"data", // Parameters that will be passed to the service and received through _req.
_val.map()
.set("uid", uid)
)
)

That is, in the configuration the path configured in the endpoint has the value /{uid}, thus allowing the client to connect through the public address:

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

Then the value of uid will be d641f095-30eb-4025-a39c-b2e3e497eab7.

After that, the service located at the URL /services/room/participant/new will be executed and the uid value is passed.

The uid can be obtained in the code of the executed service as an HTTP request parameter; see an example:

/server/services/room/participant/new.js
// The sent UID is obtained directly from the request.
const uid = _req.getString('uid')

broadcastService​

To perform the broadcast, you need the WebSocket endpoint name, define the path (path) if applicable, and the service that will be executed.

The service output will be sent to all active connections on the specified endpoint.

Broadcast Example​

/server/services/room/participant/new.js
_ws.broadcastService(
"admin", // WebSocket endpoint name.
"/", // Path configured in the endpoint (path).
_val.map() // Parameters for the service that will be executed.
.set("service", "/services/room/participant/list")
)

In this example, the output content generated by the room/participant/list service will be sent to everyone connected to the admin endpoint.

WS Client - NPM​

To make integration with the frontend easy, the NPM module is provided:

Installation command:

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

_ws.config({
url: 'ws://localhost:9000/ws/example/',
servicesPrefix: '/services',
autoReconnect: true,
connect: (event) => {
console.info('WebSocket Connected', event);
},
close: (event) => {
console.warn('WebSocket Closed', event);
},
error: (error) => {
console.error('WebSocket Error', error);
},
message: (data, event) => {
console.debug('WebSocket Message', data);
}
});

Make sure the URL correctly combines the values configured in the endpoint for host and public + path, as follows:

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

The production URL with NGINX integration, reverse proxy, and SSL will probably look like this:

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

Next you can start the connection:

_ws.connect();

Execute Service via WebSocket​

We can execute REST API services through the WebSocket connection, which means no HTTP request is made; the request data is sent as a message, and the service response is obtained as a message received back.

Before starting, we must create an observer for the service response, for example:

const listenerRef = _ws.addListener({
method: 'GET', // Optional; omit to listen to all methods.
service: 'path/to/my/service/here',
success: (data) => {
console.log('Response of the service executed via WebSocket:', data);
},
fail: (error)=> {
console.log('The service executed via WebSocket failed:', error);
}
});

Now we can send a service execution request; the service response will be obtained in the observer created above. We execute the service like this:

_ws.sendService({
method: 'GET', // Optional; requests use GET when omitted.
service: 'path/to/my/service/here',
data: {
message: 'Hello...'
}
});

To remove the added observer, use:

_ws.removeListener(listenerRef);

And to close the WebSocket connection:

_ws.close();

NGINX Reverse Proxy​

In NGINX the reverse proxy can be configured like this:

location /ws {
proxy_pass http://localhost:9000;
proxy_set_header Host my-app.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 "";
}

On line 3 where Host is defined, the my-app part must be adjusted to the corresponding Netuno application name. If there is _ in the application name, then in the Host address it will be replaced by -; for example, the application my_app will have the Host value like this: my-app.local.netu.no.

Restart NGINX for the configuration to take effect.

The domains configured in NGINX, that is, the server_name parameterization, do not need to be in the endpoint hosts in the Netuno application, because in the proxy configuration above the Host in the HTTP header is changed to a standard address that is already automatically configured by Netuno at all times, as long as the prefix with the application name is correct.

If the NGINX server uses port 80, then the final URL will be:

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

And with SSL/HTTPS, if the NGINX server uses port 443, then the final URL will be:

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