WebSocket Extension

WebSocket Extension

This extension adds a WebSocket Server to your Lucee Server that runs over TCP on port 80 for WS: and 443 for WSS:

WebSocket Listeners are created with a CFML Component - one per channel.

Please note, on Windows, there more are limitation regarding how many websockets can be used, than with Linux.

Installation

There are multiple ways to install the docker extension.

Lucee Administrator

The Extension can be installed via Lucee Administrator

Lucee Admin: Extensions - Application

Manual Installation

Download the LEX file from https://download.lucee.org/ and save to /lucee/lucee-server/deploy/ (takes up to a minute for Lucee to pick up and install)

Lucee Download LEX File

Docker

In Docker there are different ways to install it.

Copy it into the deploy folder like this via a Dockerfile:

ADD https://ext.lucee.org/websocket-extension-3.0.0.14-RC.lex /lucee/lucee-server/deploy/

Using Environment Variables like this:

environment:
  - LUCEE_EXTENSIONS=3F9DFF32-B555-449D-B0EB5DB723044045;version=3.0.0.14-RC

Or simply define it in the .CFConfig.json file (Lucee 6+)

{
  "extensions": [
    {
      "name": "WebSocket",
      "path": "/your/path/extensions/websocket.extension-3.0.0.14-RC.lex",
      "id": "3F9DFF32-B555-449D-B0EB5DB723044045"
    }
  ]
}

See this example for more details about setting up Extension in .CFConfig.json.

Configuration

By default, Lucee Server will look in {lucee-config}/websockets/ for WebSocket Components.

Lucee Server will create a config file if one does not exists at {lucee-config}websocket.json with the following defaults

{lucee-config}: /lucee/lucee-server/context

{
  "directory": "{lucee-config}/websockets/",
  "requestTimeout": 50,
  "idleTimeout": 300
}

The WebSocket Extension comes with a helper function websocketInfo() that well show the current configurations settings. More on other details later ...

websocketInfo() TODO: update with new version

Component

[!IMPORTANT] a Lucee restart is required when a new WebSocket CFC is added (just like for a ReST CFC)

component hint="used to test websocket client" {
public static function onFirstOpen(wsclients) {}
    function onOpen(wsclient) {}
    function onOpenAsync(wsclient) {}
    function onMessage(wsclient, message) {}
    function onClose(wsclient, ReasonPhrase) {}
    function onError(wsclient,cfcatch) {}
public static function onLastClose() {}

}

Javascript Client

Given that the Component was saved as {lucee-config}/websockets/test.cfc, here is native Javascript to open and use a connection to your Lucee WebSocket:

socket = new WebSocket("ws://127.0.0.1:80/ws/test");

socket.onopen = function (evt) { console.log(["onopen()", evt]); };

socket.onmessage = (event) => { console.log(event.data); };

socket.onerror = function (error) { console.error(error); };

socket.send("Hello, Lucee Extension!");

socketclose();

Broadcast Message to all Clients

A broadcast is a message send to all connected clients

To be able to do this, we need to know who is connected. The first time a connection is made, onFirstOpen(wsclients) is fired. wsclients is a Java class with the following methods

size():number                  // the number of clients connected
broadcast(any message):boolean // send message to all clients
getClients():Client[]          // return array of all clients currently connected
close():void                   // closes all clients

SO we can save that for furture use

public static function onFirstOpen(wsclients) {
    static.wsclients = arguments.wsclients;
}

For example

function onOpen(wsclient) {
    static.wsclients.broadcast("There are now ##static.wsclients.size()## connections");
}

Send Message to one Client

When a connection is instantiated, onOpen(wsclient) is fired. wsclient is a Java class with the following methods

client.broadcast(message):void // send message to all connected clients
client.send(message):void      // send message to the client
client.isOpen():boolean        // is the client still connected?
client.isClose():boolean       // is the client no longer connected?
client.close():void            // closes the connection of the client

To send a message using wsclient

function onOpen(wsclient) {
    arguments.wsclient.send("You are connected to Lucee WebSocket");
}

You can also send a message from onOpen() by returning a string

function onOpen(wsclient) {
    return "Welcome to the test websocket channel";
}

You can add your own function to the WebSocket Component

public void function sendMessage(
    required string jsonData
) {
    variables.wsclient.send(jsonData);
}

function onOpen(wsclient) { sendMessage("Hello, Lucee WebSocket!"); }

Using Lucee WebSocket to PUSH data to Client

With webSocets being a bidirectional communication channel, your Lucee Server no longer limited to responding to a request, it can now push data to the client.

This means the user no longer has to refresh a page to see if data is updated, or have a Javascript looping function that is continuously calling a ReST API to get lasted data.

When your application has data ready for the user, have the WebSocket push the data to the cient!

Make use of Static Function

Add a thread to start a background process, and have it continuously looping for as long as there are clients connected

public static function onFirstOpen(wsclients) {
    static.wsclients = arguments.wsclients;
    thread name="threadDataQueue" oClients=static.wsclients {
		while( attributes.oClients.size() > 0 ) {
			data = getDataFromSomewhere();
			attributes.oClients.broadcastMessage(data);
			sleep(1000);
		}
    }
}

Function getDataFromSomewhere() is respoible for obtaining the data that needs to be sent to the client. RedisQueue is an example of where data can be stored. Your Lucee application can Push data to a Redis Queue, and getDataFromSomewhere() can Pop one record at a time.

Using webSocketInfo() to Send Message to Client

WebSocketInfo() also has an array of instances - one for each client call to a WebSocket Component. So looping through the array, gives you access to the Component, and then you can call any of it'sfunction

For Example ( excluding role management functions )

component hint="Test WebSocket"  {
	variables.roles = [];
public boolean function hasRole( required string role ) {
	return ( variables.roles.find( arguments.role ) > 0 );
}
public void function sendMessage( required string jsonData ) {
	variables.wsclient.send( jsonData );
}
...

}

var wsInfo = websocketInfo(false);
if ( !wsInfo.instances.len() )
    return;

var wsInstances = wsInfo.instances;

var item = getRedisData(); var stItem = deserializeJSON( item ); for ( var wsI in wsInstances ) { if ( GetMetadata( wsI ).name == 'test' && wsI.hasRole( stItem.data.role ) ) { wsI.sendMessage( item ); } }

Task Event Gateway is a good candidate for this script

TODO: link to recipe page

See also