Nodejs Websocket connection in an ES6 class - node.js

I am not sure if this is the correct way to implement a websocket connection in NodeJS, but the problem I am having is not with WebSockets but with class variables.
This is my WebSocketClass:
class WebSocketCalss {
constructor ( httpserver )
{
console.log("Initializing WebSocketCalss");
this.httpServer = httpserver;
this.connection = null;
this.client = null;
this.initializeWebSocket();
}
initializeWebSocket()
{
var WebSocketServer = require('websocket').server;
var wsServer = new WebSocketServer({
httpServer: this.httpServer
});
wsServer.on('request', function(request) {
console.log((new Date()) + ' Connection from origin ' + request.origin + '.');
this.connection = request.accept(null, request.origin);
console.log((new Date()) + ' Connection accepted.');
this.connection.sendUTF(JSON.stringify({ type: "history", data: "data"} ));
var t = 0;
/* ---- Client ---- */
var W3CWebSocket = require('websocket').w3cwebsocket;
this.client = new W3CWebSocket('wss://ws.bitstamp.net');
this.client.onerror = function() {
console.log('Connection Error');
};
this.client.onopen = function() {
console.log('WebSocket Client Connected');
var subscribeMsg = {
"event": "bts:subscribe",
"data": {
"channel": "live_trades_btcusd"
}
};
this.client.send(JSON.stringify(subscribeMsg));
};
this.client.onclose = function() {
console.log('echo-protocol Client Closed');
};
this.client.onmessage = function(e) {
if (typeof e.data === 'string') {
var bitstampPrice = JSON.parse(e.data).data.price;
console.log(bitstampPrice);
this.connection.sendUTF(bitstampPrice);
}
};
});
//this.connection.sendUTF(JSON.stringify({ type: "history", data: "data"} ));
}
}
module.exports = (httpserver) => { return new WebSocketCalss(httpserver) }
It maybe hairy, so this is what I am trying to do:
My NodeJS server will open a WebSocket connection to my client (browser)
In this WebSocket, I want to send a value that is received from another WebSocket (that is, my NodeJS will connect as a client)
Things seem to work fine individually, however, when I try to send the value (that I received as a client), to my own client (as I am the server), I get
Cannot read property 'send' of undefined
Basically, inside the callback, the this variable are not defined. It is as if this is a new object.
I am not familiar with ES6 so I believe I am doing something fundamentally wrong.
If anyone could shed some light in to this that'd be very much appreciated.

When you use this inside a function(), the context of the this is bound to the function and not the outside class.
this.client.onopen = () => {
console.log('WebSocket Client Connected');
var subscribeMsg = {
"event": "bts:subscribe",
"data": {
"channel": "live_trades_btcusd"
}
};
this.client.send(JSON.stringify(subscribeMsg));
};

Related

NodeJS WebSocket multiple path

Hi i try to make a websocket server class with multiple paths.
The connection Event is emitted when i call the websocket server with a valid path.
But the message Event is never emitted.
When i use the code example i have no problem but with my class nothing happen, only the connection event.
I don't understand why...
Thank's for your help
The WebSocket server class
const http = require('http');
const WebSocket = require('ws');
const url = require('url');
class WSServer {
#server;
#wsServer = {};
#ip;
#port;
#origin;
#routes = {};
constructor(ip, port, origin = null, routes = ['/']) {
this.#ip = ip;
this.#port = port;
this.#origin = origin;
this.#routes = routes;
this.#server = http.createServer((request, response) => {
console.log((new Date()) + ' Received request for ' + request.url);
response.writeHead(404);
response.end();
});
this.#routes.forEach(route => {
this.#wsServer[route] = new WebSocket.Server({ noServer: true });
})
this.#server.listen(this.#port, this.#ip, () => {
console.log((new Date()) + ' Server is listening on port ' + this.#port);
});
this.#server.on('upgrade', (request, socket, head) => {
const pathname = url.parse(request.url).pathname;
console.log(pathname);
this.#routes.forEach(route => {
if (pathname === route) {
this.#wsServer[route].handleUpgrade(request, socket, head, (ws) => {
this.#wsServer[route].emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
});
}
clients(route = '/') {
return this.#wsServer[route].connections;
}
on(route = '/', event, func) {
this.#wsServer[route].on(event, func);
}
}
module.exports = WSServer;
Code example
const WSServer = require('./WSServer');
const wsServer = new WSServer('localhost', '7070', null, ['/db', '/']);
wsServer.on('/', 'connection', function test(conn, req) {
conn.on('message', msg => {
console.log(msg);
})
});
wsServer.on('/db', 'connection', function test(conn, req) {
conn.on('message', msg => {
console.log(msg);
})
});
Problem solved !
the error come from the route loop in the onupgrade http server event...
the correction
if (this.#wsServer[pathname] !== undefined) {
this.#wsServer[pathname].handleUpgrade(request, socket, head, (ws) => {
this.#wsServer[pathname].emit('connection', ws, request);
});
} else {
socket.destroy();
}

relaying redis subscription messages to websocket

subscribing to redis with a pattern (*:*:*) which recieves data every minute; also operating as a websocket server which checks for a particular subscription message, and if the data for the particular subscription message is returned by redis, directly push the data returned from redis to that WS channel
for example: if the WS client wants to subscribe to a channel called Binance:BTC-USDT:1m, and that data is coming from redis every minute, how do I send it to the WS client as soon as I get new data from Redis?
What would be the most efficient way to implement this?
export {};
const redis = require("redis");
const WebSocket = require("ws");
const subscriber = redis.createClient();
subscriber.psubscribe("*:*:*");
const wss = new WebSocket.Server({ port: 8080 });
subscriber.on("pmessage", function (pattern, channel, message) {
console.log(message);
});
wss.on("connection", function connection(ws) {
ws.on("message", function incoming(message) {
console.log("Server Received: %s", message);
});
ws.send("something from server");
});
ws.ts
import { IWSClient } from "./types/IChannel";
export default class WSHandler {
sockets: Object = {};
bindChannelWS(channel: any, client: IWSClient) {
if (this.sockets[channel] === undefined) {
this.sockets[channel] = [client];
} else {
this.sockets[channel].push(client);
}
}
publishToChannel(channel: string, message: string) {
if (this.sockets[channel] !== undefined) {
this.sockets[channel].forEach(function (client: IWSClient) {
console.log(JSON.parse(message));
client.send(message);
});
}
}
}
index.ts
export {};
const redis = require("redis");
const WebSocket = require("ws");
import { IWSClient } from "./types/IChannel";
import WSHandler from "./ws";
const subscriber = redis.createClient();
subscriber.psubscribe("*:*:*");
const wss = new WebSocket.Server({ port: 8080 });
var wsHandler = new WSHandler();
subscriber.on("pmessage", function (
pattern: any,
channel: string,
message: string
) {
wsHandler.publishToChannel(channel, message);
});
wss.on("connection", function connection(ws: IWSClient) {
ws.on("message", function incoming(_subMessages: string) {
let subMessages: Array<string> = JSON.parse(_subMessages);
console.log("Server Received: ", subMessages);
subMessages.forEach((channel) => wsHandler.bindChannelWS(channel, ws));
});
ws.send(JSON.stringify({ connection: "Initiated" }));
});

New connection cause the current connections to stop working

I am making the chat application using socket (which I'm new at) with multiple tenants structure and using namespaces. Here's my code:
Socket server:
index.js
class Server {
constructor() {
this.port = process.env.PORT || 3000;
this.host = process.env.HOST || `localhost`;
this.app = express();
this.http = http.Server(this.app);
this.rootSocket = socketio(this.http);
}
run() {
new socketEvents(this.rootSocket).socketConfig();
this.app.use(express.static(__dirname + '/uploads'));
this.http.listen(this.port, this.host, () => {
console.log(`Listening on ${this.host}:${this.port}`);
});
}
}
const app = new Server();
app.run();
socket.js
var redis = require('redis');
var redisConnection = {
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASS
};
var sub = redis.createClient(redisConnection);
var pub = redis.createClient(redisConnection);
class Socket {
constructor(rootSocket) {
this.rootIo = rootSocket;
}
socketEvents() {
/**
* Subscribe redis channel
*/
sub.subscribe('visitorBehaviorApiResponse');
//TODO: subscribe channel..
// Listen to redis channel that published from api
sub.on('message', (channel, data) => {
data = JSON.parse(data);
console.log(data);
const io = this.rootIo.of(data.namespace);
if (channel === 'visitorBehaviorApiResponse') {
io.to(data.thread_id).emit('receiveBehavior', data);
io.to('staff_room').emit('incomingBehavior', data);
}
})
sub.on('error', function (error) {
console.log('ERROR ' + error)
})
var clients = 0;
this.rootIo.on('connection', (rootSocket) => {
clients++;
console.log('root:' + rootSocket.id + ' connected (total ' + clients + ' clients connected)');
const ns = rootSocket.handshake['query'].namespace;
// Dynamic namespace for multiple tenants
if (typeof (ns) === 'string') {
const splitedUrl = ns.split("/");
const namespace = splitedUrl[splitedUrl.length - 1];
const nsio = this.rootIo.of(namespace);
this.io = nsio;
this.io.once('connection', (socket) => {
var visitors = [];
console.log('new ' + socket.id + ' connected');
// once a client has connected, we expect to get a ping from them saying what room they want to join
socket.on('createChatRoom', function (data) {
socket.join(data.thread_id);
if (typeof data.is_staff !== 'undefined' && data.is_staff == 1) {
socket.join('staff_room');
} else {
if (visitors.some(e => e.visitor_id === data.visitor_id)) {
visitors.forEach(function (visitor) {
if (visitor.visitor_id === data.visitor_id) {
visitor.socket_ids.push(socket.id);
}
})
} else {
data.socket_ids = [];
data.socket_ids.push(socket.id);
visitors.push(data);
}
socket.join('visitor_room');
}
//TODO: push to redis to check conversation type
});
socket.on('sendMessage', function (data) {
console.log(data);
pub.publish('chatMessage', JSON.stringify(data));
this.io.in(data.thread_id).emit('receiveMessage', data);
this.io.in('staff_room').emit('incomingMessage', data);
// Notify new message in room
data.notify_type = 'default';
socket.to(data.thread_id).emit('receiveNotify', data);
}.bind(this))
socket.on('disconnect', (reason) => {
sub.quit();
console.log('client ' + socket.id + ' left, ' + reason);
});
socket.on('error', (error) => {
console.log(error);
});
});
}
// Root disconnect
rootSocket.on('disconnect', function () {
clients--;
console.log('root:' + rootSocket.id + ' disconnected (total ' + clients + ' clients connected)');
});
});
}
socketConfig() {
this.socketEvents();
}
}
module.exports = Socket;
Client:
const server = 'https://socket-server'
const connect = function (namespace) {
return io.connect(namespace, {
query: 'namespace=' + namespace,
resource: 'socket.io',
transports: ['websocket'],
upgrade: false
})
}
const url_string = window.location.href
const url = new URL(url_string)
const parameters = Object.fromEntries(url.searchParams)
const socket = connect(`${server}/${parameters.shopify_domain}`)
var handleErrors = (err) => {
console.error(err);
}
socket.on('connect_error', err => handleErrors(err))
socket.on('connect_failed', err => handleErrors(err))
socket.on('disconnect', err => handleErrors(err))
The problem that I met is when socket server got a new connection, the existing connections will be stopped working util they make a page refreshing to reconnect a new socket.id.
And when a namespace's client emit data, it sends to other namespaces, seem my code is not work correctly in a namespace.
Could you take a look at my code and point me where I'm wrong?
Thanks
1) Get UserId or accessToken while handshaking(in case of accessToken decrypt it).
and store userID: socketId(in Redis or in local hashmap) depends upon the requirement .
2) When u are going to emit to particular user fetch the socketid to that userid from redis or local hashmap
and emit to it.
**io.to(socketId).emit('hey', 'I just met you');**
3) If you are using multiple servers use sticky sessions
4) Hope this will help you

Dynamically Create Kafka Consumers That Send Data Through a Websocket

I am using kafka-node to read stream data and pass it to my web app with Web Sockets using NodeJS. This works fine if I able to define the kafka producer server and the topic I am interested in, however for my use case the end users will input the kafka producer server and the topic and my NodeJS backend will be responsible to receive that request and to create the appropriate kafka/websocket connections.
My idea was the following:
Create a rest API to which the web app could send requests to in order to create a new kafka consumer/web socket connection (/registerTopic)
Save the new kafka consumers in a global array when I create a new kafka consumer so that I can later pause or resume the stream with another rest API call (/pauseTopic and /resumeTopic)
I ran into problems trying to move the WebSocket code into /registerTopic...Whenever I do this everything acts very strangely and I suddenly get 1000x messages at once and then 40-50x messages every second even though the kafka producer is only sending 1 message per second. Any ideas on how I can get this working?
const express = require("express");
const app = express();
const cors = require('cors');
const bodyParser = require('body-parser');
const fs = require('fs');
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
var WebSocketServer = require('websocket').server;
var http = require('http');
var https = require('https');
var kafka = require('kafka-node');
var topics = [];
var privateKey = fs.readFileSync('PATH', 'utf8');
var certificate = fs.readFileSync('PATH', 'utf8');
var credentials = { key: privateKey, cert: certificate };
var consumers = new Set();
// This was working without any issues before I tried to make this dynamic!
/* var Consumer = kafka.Consumer,
client = new kafka.KafkaClient('localhost:9092'),
consumer = new Consumer(
client, [{ topic: 'numtest', partition: 0 }], { autoCommit: false }); */
var server = http.createServer(function (request, response) {
console.log(' Request recieved : ' + request.url);
response.writeHead(404);
response.end();
});
server.listen(8080, function () {
console.log('Listening on port : 8080');
});
webSocketServer = new WebSocketServer({
httpServer: server,
autoAcceptConnections: false
});
function iSOriginAllowed(origin) {
return true;
}
// This was working without any issues before I tried to make this dynamic!
/* webSocketServer.on('request', function (request) {
if (!iSOriginAllowed(request.origin)) {
request.reject();
console.log(' Connection from : ' + request.origin + ' rejected.');
return;
}
let connection = request.accept('echo-protocol', request.origin);
console.log(' Connection accepted : ' + request.origin);
connection.on('message', function (message) {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
}
});
consumer.on('message', function (message) {
console.log('msg');
connection.sendUTF(message.value);
});
connection.on('close', function (reasonCode, description) {
console.log('Connection ' + connection.remoteAddress + ' disconnected.');
});
}); */
var httpsServer = http.createServer(credentials, app);
httpsServer.listen(3000, () => {
console.log("Server running on port 3000");
});
app.get("/getTopics", (req, res, next) => {
res.json(topics);
});
app.post("/registerTopic", (req, res) => {
try {
var client = new kafka.KafkaClient(req.body.host);
var Consumer = kafka.Consumer;
consumer = new Consumer(
client, [{ topic: req.body.topic, partition: 0 }], { autoCommit: false });
let consumerExists = false;
for (let c = 0; c < [...consumers].length; c++) {
if ([...consumers][c].topic == req.body.topic && [...consumers][c].sessionId == req.body.sessionId) {
consumerExists = true;
}
}
if (!consumerExists) {
consumers.add({ 'topic': req.body.topic, 'sessionId': req.body.sessionId, 'consumer': consumer });
}
client.loadMetadataForTopics([], function (error, results) {
Object.keys(results[1].metadata).forEach(function (key) {
var value = results[1].metadata[key];
if (!value['0'].topic.includes('__') && !value['0'].topic.includes('offset')) {
topics.push({ 'producer': req.body.host, 'topic': value['0'].topic });
}
});
});
webSocketServer.on('request', function (request) {
if (!iSOriginAllowed(request.origin)) {
request.reject();
console.log(' Connection from : ' + request.origin + ' rejected.');
return;
}
let connection = request.accept('echo-protocol', request.origin);
console.log(' Connection accepted : ' + request.origin);
connection.on('message', function (message) {
if (message.type === 'utf8') {
console.log('Received Message: ' + message.utf8Data);
}
});
consumer.on('message', function (message) {
console.log('msg');
connection.sendUTF(message.value);
});
connection.on('close', function (reasonCode, description) {
console.log('Connection ' + connection.remoteAddress + ' disconnected.');
});
});
res.json("Working");
} catch (error) {
console.error(error);
res.status(400).send('Unable to register new topic')
}
});
app.post("/pauseTopic", (req, res) => {
try {
console.log(req.body);
let filteredConsumer = [...consumers].filter(function (item) {
console.log(req.body.topic, item.sessionId);
if (item.topic == req.body.topic && item.sessionId == req.body.sessionId) {
return c;
}
});
console.log(filteredConsumer);
//filteredConsumer[0].consumer.pause();
res.json("Working");
} catch (error) {
console.error(error);
res.status(400).send('Unable to register new topic')
}
});
app.post("/resumeTopic", (req, res) => {
try {
let filteredConsumer = [...consumers].filter(function (item) {
if (item.topic == req.body.topic && item.sessionId == req.body.sessionId) {
return item;
}
});
filteredConsumer[0].consumer.resume();
res.json("Working");
} catch (error) {
console.error(error);
res.status(400).send('Unable to register new topic')
}
});

share mqtt client object between files

I connect to MQTT this way:
//mqtt.js
const mqtt = require('mqtt');
var options = {
//needed options
};
var client = mqtt.connect('mqtt://someURL', options);
client.on('connect', () => {
console.log('Connected to MQTT server');
});
I want to export the client object this way:
//mqtt.js
module.exports = client;
So that I can import it in other files and make use of it this way:
//anotherFile.js
const client = require('./mqtt');
client.publish(...)
However, we all know that this will not work! How can I achieve this ?
Update
I tried promise and get a very strange behavior. When I use the promise in the same file (mqtt.js) like the code below, everything is OK:
//mqtt.js
const mqtt = require('mqtt');
var mqttPromise = new Promise(function (resolve, reject) {
var options = {
//needed options
};
var client = mqtt.connect('mqtt://someURL', options);
client.on('connect', () => {
client.subscribe('#', (err) => {
if (!err) {
console.log('Connected to MQTT server');
resolve(client);
} else {
console.log('Error: ' + err);
reject(err);
}
});
});
});
mqttPromise.then(function (client) {
//do sth with client
}, function (err) {
console.log('Error: ' + err);
});
But when I export the promise and use it in another file, like this:
//mqtt.js
//same code to create the promise
module.exports = mqttPromise;
//anotherFile.js
const mqttPromise = require('./mqtt');
mqttPromise.then(function (client) {
//do sth with client
}, function (err) {
console.log('Error: ' + err);
});
I get this error:
TypeError: mqttPromise.then is not a function
You can probably achieve your goal creating 2 files, one for handling mqtt methods and another to manage the connection object.
Here's the file for the mqtt handler:
//mqttHandler.js
const mqtt = require('mqtt');
class MqttHandler {
constructor() {
this.mqttClient = null;
this.host = 'YOUR_HOST';
this.username = 'YOUR_USER';
this.password = 'YOUR_PASSWORD';
}
connect() {
this.mqttClient = mqtt.connect(this.host, {port: 1883});
// Mqtt error calback
this.mqttClient.on('error', (err) => {
console.log(err);
this.mqttClient.end();
});
// Connection callback
this.mqttClient.on('connect', () => {
console.log(`mqtt client connected`);
});
this.mqttClient.on('close', () => {
console.log(`mqtt client disconnected`);
});
}
// // Sends a mqtt message to topic: mytopic
sendMessage(message, topic) {
this.mqttClient.publish(topic, JSON.stringify(message));
}
}
module.exports = MqttHandler;
Now lets use the exported module to create a mqtt client connection on another file:
//mqttClient.js
var mqttHandler = require('./mqttHandler');
var mqttClient = new mqttHandler();
mqttClient.connect();
module.exports = mqttClient;
With this exported module you can now call your client connection object and use the methods created in the mqttHandler.js file in another file :
//main.js
var mqttClient = require('./mqttClient');
mqttClient.sendMessage('<your_topic>','<message>');
Although there may be a better method to perform your task, this one worked pretty well for me...
Hope it helps!
cusMqtt.js
const mqtt = require("mqtt");
function prgMqtt() {
const options = {
port: 1883,
host: "mqtt://xxxxxxx.com",
clientId: "mqttjs_" + Math.random().toString(16).substr(2, 8),
username: "xxxxxx",
password: "xxxxxx",
keepalive: 60,
reconnectPeriod: 1000,
protocolId: "MQIsdp",
protocolVersion: 3,
clean: true,
encoding: "utf8",
};
prgMqtt.client = mqtt.connect("mqtt://xxxxxxxx.com", options);
prgMqtt.client.on("connect", () => {
prgMqtt.client.subscribe("Room/Fan");
console.log("connected MQTT");
});
prgMqtt.client.on("message", (topic, message) => {
console.log("message is " + message);
console.log("topic is " + topic);
// client.end();
});
}
exports.prgMqtt = prgMqtt;
index.js/main program call
const { prgMqtt } = require("./startup/cusMqtt");
prgMqtt();
another .js
const { prgMqtt } = require("../startup/cusMqtt");
router.get("/:id", async (req, res) => {
prgMqtt.client.publish("Room/Reply", "Replied Message");
});

Resources