diff --git a/docs/_images/extension/websocket/lucee-admin-extension.png b/docs/_images/extension/websocket/lucee-admin-extension.png new file mode 100644 index 000000000..246680a67 Binary files /dev/null and b/docs/_images/extension/websocket/lucee-admin-extension.png differ diff --git a/docs/_images/extension/websocket/websocket-lex.png b/docs/_images/extension/websocket/websocket-lex.png new file mode 100644 index 000000000..5d1ed178a Binary files /dev/null and b/docs/_images/extension/websocket/websocket-lex.png differ diff --git a/docs/_images/extension/websocket/websocketInfo.png b/docs/_images/extension/websocket/websocketInfo.png new file mode 100644 index 000000000..b075f008c Binary files /dev/null and b/docs/_images/extension/websocket/websocketInfo.png differ diff --git a/docs/recipes/extension-websocket.md b/docs/recipes/extension-websocket.md new file mode 100644 index 000000000..250fcbb3c --- /dev/null +++ b/docs/recipes/extension-websocket.md @@ -0,0 +1,250 @@ + +# 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) +TODO: update with new version + +## 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) ) { + wsI.sendMessage(item); + } +} +``` +[Task Event Gateway](event-gateways-overview.md) is a good candidate for this script + +*TODO: link to recipe page* \ No newline at end of file