I am having a lot of difficulty trying to get websockets to work correctly. I can talk to my server from a client, but I cannot figure out how to talk back. The docs on the subject are not quite clear enough, and google/AI is getting itself mixed up between the older version of websockets and the new version.
here is my current websocket layout, mostly copied from the example:
component hint="websockets"
{
Application.datasourceName = 'mytable';
public static function onFirstOpen( wsclients )
{
static.wsclients = arguments.wsclients;
local.dt = {};
dt.message = 'ehhlol';
static.wsclients.broadcast(serializeJSON(dt));
thread name="threadDataQueue" oClients=static.wsclients
{
while( attributes.oClients.size() > 0 )
{
local.dt = {};
dt.message = 'main branch';
dt.clientSize = attributes.oClients.size();
attributes.oClients.broadcast(serializeJSON(dt));
sleep(1000);
}
}
}
function onOpen( wsclient )
{
//static.wsclients.broadcast("There are now #static.wsclients.size()# connections");
//onOpen(), we have no way of identifying who just made contact, need to wait for the 1st message with information about user to be sent
//arguments.wsclient.send( 'incoming' );
}
function onOpenAsync( wsclient )
{
}
function onMessage( wsclient, message )
{
if( isJSON(arguments.message))
{
local.msg = deserializeJSON(arguments.message);
if( structKeyExists(msg, 'init') )
{
local.wsInfo = websocketInfo(false);
local.wsInstances = wsInfo.instances;
for ( var wsI in wsInstances )
{
local.thisQuery = wsI.session.queryString;
local.params = {};
listEach(thisQuery, function(paramPair)
{
local.key = listFirst(paramPair, "=");
local.value = listLast(paramPair, "=");
params[key] = value;
}, "&");
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'SELECT ID
FROM websocket
WHERE ID = :ID AND token = :token AND userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='ID', value='#msg.ID#', cfsqltype='cf_sql_bigint');
queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
queryService.addParam( name='token', value='#params['uuid']#', cfsqltype='cf_sql_longvarchar');
local.qFind = queryService.execute().getResult();
if( qFind.recordcount > 0 )
{
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE websocket
SET websocketID = :webID
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_longvarchar');
local.qUpdate = queryService.execute();
break;
}
}
catch(any e)
{
//failed
arguments.wsclient.send( e.Message );
arguments.wsclient.close();
}
}
}
else
{
//receiving a non init message from client, this should contain no data, just used to keepAlive
//update users activeCon
if( structKeyExists(msg, "heartbeat") )
{
local.wsInfo = websocketInfo(false);
local.wsInstances = wsInfo.instances;
local.found = 0;
for ( var wsI in wsInstances )
{
if( structKeyExists(wsI.session.requestParameter, 'uuid'))
{
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'SELECT ID, userID
FROM websocket
WHERE token = :token AND userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='token', value='#wsI.session.requestParameter.uuid[1]#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
local.qFind = queryService.execute().getResult();
if( qFind.recordcount > 0)
{
//found the userID who sent the message
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE users
SET activeCon = :activeCon
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='activeCon', value='#Now()#', cfsqltype='cf_sql_datetime');
queryService.addParam( name='ID', value='#qFind.userID#', cfsqltype='cf_sql_bigint');
local.qUpdate = queryService.execute();
//update the websocket time too
local.queryService = new query(datasource = "#Application.datasourceName#");
local.sql = 'UPDATE websocket
SET lastModified = :webService
WHERE ID = :ID';
queryService.setSQL(sql);
queryService.addParam( name='webService', value='#Now()#', cfsqltype='cf_sql_datetime');
queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_bigint');
local.qUpdate = queryService.execute();
local.dt = {};
dt.type = 0;
dt.heartbeat = 1;
arguments.wsclient.send( serializeJSON(dt) );
found++;
break;
}
}
catch(any e)
{
//close connection, there is an error
arguments.wsclient.send( e.Message );
arguments.wsclient.close();
}
}
else
{
//close connection, something is wrong
arguments.wsclient.close();
}
}
if( found == 0)
{
arguments.wsclient.close();
}
}
}
}
local.dt = {};
dt.type = 0;
return dt;
}
function onClose( wsclient, reasonPhrase )
{
//delete specfic websocket from db because a new one will be started on refresh/close
local.wsInfo = websocketInfo(false);
local.wsInstances = wsInfo.instances;
for ( var wsI in wsInstances )
{
//wsI.session has all info
try
{
local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
local.sql = 'DELETE FROM websocket
WHERE websocketID = :webID AND userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
local.qClose = queryService.execute().getResult();
}
catch(any e)
{
//failed
arguments.wsclient.send( e.Message );
}
}
}
function onError( wsclient, cfcatch )
{
}
public static function onLastClose()
{
}
public void function sendMessage( required string jsonData)
{
variables.wsclient.send(jsonData);
}
}
And I run a little test script to send messages:
<cfscript>
wsInfo = websocketInfo(false);
//if ( !wsInfo.instances.len() )
// return;
wsInstances = wsInfo.instances;
//var item = getRedisData();
//var stItem = deserializeJSON( item );
for ( wsI in wsInstances )
{
queryService = new query(datasource = GetApplicationSettings().defaultdatasource);
sql = 'SELECT userID
FROM webebsocket
WHERE websocketID = :socket';
queryService.setSQL(sql);
queryService.addParam( name='socket', value='#wsI.session.id#', cfsqltype='cf_sql_varchar');
qWeb = queryService.execute().getResult();
if( qWeb.recordCount > 0 )
{
queryService = new query(datasource = GetApplicationSettings().defaultdatasource);
sql = 'SELECT *
FROM msg
WHERE userID = :userID';
queryService.setSQL(sql);
queryService.addParam( name='userID', value='#qWeb.userID#', cfsqltype='cf_sql_bigint');
qMsgs = queryService.execute().getResult();
writeDump(qMsgs);
cfloop( query="qMsgs")
{
writeDump(ID)
dt = {}
dt.type = 0;
dt.message = '';
if( !isNull(joinID) )
{
//join message
dt.type = 1;
dt.message = 'You have been invited into a room!'
}
wsI.component.sendMessage(serializeJSON(dt))
}
}
//writeDump(wsI);
writeDump(wsI.session);
writeDump(wsI.component);
//if ( GetMetadata( wsI.component ).name == 'test' && wsI.component.hasRole( stItem.data.role ) ) {
// wsI.component.sendMessage( item );
//}
}
</cfscript>
And of course, after all of that, I get an error:
Lucee 7.0.2.106 Error (expression)
Message Component [/websocket.cfc] has no accessible Member with name [wsclient]
AI For AI-driven exception analysis setup, see AI Setup Guide.
Function sendMessage
Component websocket
My biggest issue is, I see that I have a static.wsclients, but I do not understand how I parse thru it to find the .send() method for a specific client.
Another issue I am having is onFirstOpen( wsclients ) only runs the thread for 50 seconds vs forever. And the 50 seconds I am assuming is the timeout for the websocket. So any guidance on that might help as well.