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:

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:
pathmust always start with/, and in the front-end connection configuration it is very important to include the slash exactly as defined inpath.
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
hostandpublic+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.localhostmyapp.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.localhostmy-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:
localhost127.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:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
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()}`)
}
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()}`)
}
uidRoom = _ws.path().getString("uid")
dbRoom = _db.get("room", uidRoom)
if dbRoom == null:
_log.warn(f"Invalid room {uidRoom}.")
_ws.close()
else:
_log.info(f"New WebSocket Session: {_ws.sessionId()}")
uidRoom = _ws.path().getString("uid")
dbRoom = _db.get("room", uidRoom)
if dbRoom == null
_log.warn("Invalid room #{uidRoom}.")
_ws.close()
else
_log.info("New WebSocket Session: #{_ws.sessionId()}")
end
val uidRoom = _ws.path().getString("uid")
val dbRoom = _db.get("room", uidRoom)
if (dbRoom == null) {
_log.warn("Invalid room ${uidRoom}.")
_ws.close()
} else {
_log.info("New WebSocket Session: ${_ws.sessionId()}")
}
def uidRoom = _ws.path().getString("uid")
def 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:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`The session ${_ws.sessionId()} sent this message: ${_ws.message()}`)
_log.info(`The session ${_ws.sessionId()} sent this message: ${_ws.message()}`)
_log.info(f"The session {_ws.sessionId()} sent this message: {_ws.message()}")
_log.info("The session #{_ws.sessionId()} sent this message: #{_ws.message()}")
_log.info("The session ${_ws.sessionId()} sent this message: ${_ws.message()}")
_log.info("The session ${_ws.sessionId()} sent this message: ${_ws.message()}")
DELETE = connection closed
It is executed when a client browser disconnection occurs.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`Session ${_ws.sessionId()} has disconnected.`)
_log.info(`Session ${_ws.sessionId()} has disconnected.`)
_log.info(f"Session {_ws.sessionId()} has disconnected.")
_log.info("Session #{_ws.sessionId()} has disconnected.")
_log.info("Session ${_ws.sessionId()} has disconnected.")
_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.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
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.")
}
}
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.")
}
}
if _ws.isStream():
while _ws.awaitStream():
if _ws.isBinaryStreamed():
_log.info("New binary message received: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elif _ws.isTextStreamed():
_log.info("New text message received: "+ _ws.textStreamed())
_ws.sendText("Ok! The message was received successfully.")
if _ws.isStream()
while _ws.awaitStream()
if _ws.isBinaryStreamed()
_log.info("New binary message received: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elsif _ws.isTextStreamed()
_log.info("New text message received: "+ _ws.textStreamed())
end
_ws.sendText("Ok! The message was received successfully.")
end
end
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.")
}
}
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
GETservice 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.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
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)
)
)
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)
)
)
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)
)
)
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)
)
)
val 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)
)
)
final 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:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
// The sent UID is obtained directly from the request.
const uid = _req.getString('uid')
// The sent UID is obtained directly from the request.
const uid = _req.getString('uid')
# The sent UID is obtained directly from the request.
uid = _req.getString('uid')
# The sent UID is obtained directly from the request.
uid = _req.getString('uid')
// The sent UID is obtained directly from the request.
val uid = _req.getString('uid')
// The sent UID is obtained directly from the request.
final 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
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_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")
)
_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")
)
_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")
)
_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")
)
_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")
)
_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
hostandpublic+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
Hostis defined, themy-apppart must be adjusted to the corresponding Netuno application name. If there is_in the application name, then in theHostaddress it will be replaced by-; for example, the applicationmy_appwill have theHostvalue 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]