Skip to content

Commit

Permalink
Merge pull request #1435 from webonix/master
Browse files Browse the repository at this point in the history
Recipe: extension - websocket
  • Loading branch information
michaeloffner authored Jun 9, 2024
2 parents 2b6aae7 + 89e060e commit a346217
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
250 changes: 250 additions & 0 deletions docs/recipes/extension-websocket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<!--
{
"title": "WebSocket Extension",
"id": "extension-websocket",
"categories": [
"websocket"
],
"description": "How to install, congigure and create WebSockets",
"keywords": [
"Lucee",
"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.

## Installation

The Extension can be installed via Lucee Administor

![Lucee Admin: Extensions - Application](../_images/extension/websocket/lucee-admin-extension.png)

Or downloaded the LEX file from [https://download.lucee.org/](https://download.lucee.org/) and save to `

![Lucee Download LEX File](../_images/extension/websocket/websocket-lex.png)

eg Dockerfile

```Dockerfile
ADD https://ext.lucee.org/org.lucee.websocket.extension-1.0.0.4-BETA.lex /lucee/lucee-server/deploy/
```

Or using Environment Variables

eg docker-compose.yml

```yml
environment:
- LUCEE_EXTENSIONS=07082C66-510A-4F0B-B5E63814E2FDF7BE;version=1.0.0.4-BETA
```
## 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*

```json
{
"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()](../_images/extension/websocket/websocketInfo.png)
<em>TODO: update with new version</em>

## Component

```lucee
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:

```javascript
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

```java
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

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

For example
```lucee
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

```java
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

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

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

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

You can add your own function to the WebSocket Componet

```lucee
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 continuosly 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

```lucee
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 ( *excuding role management functions* )

```lucee
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);
}
...
}
```

```lucee
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) ) {
<b>wsI.sendMessage(item);</b>
}
}
```
[Task Event Gateway](event-gateways-overview.md) is a good candidate for this script

*TODO: link to recipe page*

0 comments on commit a346217

Please sign in to comment.