Related
I am using mqttjs to connect to the aws iot mqtt by wss presigned URL
In my application, there are 2 mqtt connection, the first one is from the main thread, the second one is from a web worker thread. Both are created from a same class (MqttServcie), they have the same logic, same everything.
But the one from the main thread keep disconnecting and reconnecting.
The one from the worker is very stable, the connection is never die, it never has to be reconnected.
Do you have any idea why the connection from the main thread keep disconnecting?
And what make a connection ends? (other than connection timeout and lost wifi connection)
In the image below, I end the connection and create a new one after 5 times retrying fails, so the number of request is several, nevermind.
The client id is always random, so it never be kicked off by another client.
The MqttService class
/* eslint-disable no-useless-constructor, no-empty-function */
import mqtt, { MqttClient } from 'mqtt';
import { NuxtAxiosInstance } from '#nuxtjs/axios';
import RestService from './RestService';
import { Mqtt } from '~/types/Mqtt';
import { MILISECS_PER_SEC } from '~/configs';
import { logWithTimestamp } from '~/utils';
export type MqttServiceEventHandlers = {
close?: Array<() => void>;
disconnectd?: Array<() => void>;
connected?: Array<() => void>;
reconnect?: Array<() => void>;
reconnected?: Array<() => void>;
beforeReconect?: Array<() => void>;
};
export type MqttServiceEvent = keyof MqttServiceEventHandlers;
export interface IMqttService {
httpClient?: NuxtAxiosInstance;
presignedUrl?: string;
}
export class MqttService {
public client: MqttClient | null = null;
public closing = false;
public reconnecting = false;
// Example: "wss://abcdef123-ats.iot.us-east-2.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXXXXX%2F20230206%2Fus-east-2%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=20230206T104907Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=abcxyz123"
public presignedUrl = '';
public httpClient = null as null | NuxtAxiosInstance;
public retryCount = 0;
public retryLimits = 5;
public handlers: MqttServiceEventHandlers = {};
constructor(
{ httpClient, presignedUrl }: IMqttService,
public caller = 'main'
) {
if (httpClient) {
this.httpClient = httpClient;
} else if (presignedUrl) {
this.presignedUrl = presignedUrl;
} else {
throw new Error(
'[MqttService] a httpClient or presigned URL must be provided'
);
}
}
async connect() {
await this.updatePresignedUrl();
this.client = mqtt.connect(this.presignedUrl, {
clientId: this.generateClientId(),
reconnectPeriod: 5000,
connectTimeout: 30000,
resubscribe: true,
keepalive: 15,
transformWsUrl: (_url, _options, client) => {
// eslint-disable-next-line no-param-reassign
client.options.clientId = this.generateClientId();
logWithTimestamp(
`[MQTT] [${this.caller}] transformWsUrl()`,
client.options.clientId,
this.signature
);
return this.presignedUrl;
},
});
return this.setupHandlers();
}
protected setupHandlers() {
return new Promise<MqttClient>((resolve, reject) => {
this.client!.on('close', async () => {
if (this.closing) return;
if (this.retryCount >= this.retryLimits) {
(this.handlers.close || []).forEach((handler) => handler());
await this.disconnect();
logWithTimestamp(`[MQTT] [${this.caller}] connection has closed!`);
reject(new Error(`All retry attempts were failed (${this.caller})`));
return;
}
if (this.retryCount === 0) {
(this.handlers.beforeReconect || []).forEach((handler) => handler());
logWithTimestamp(
`[MQTT] [${this.caller}] connection lost`,
this.presignedUrl
);
}
// Re-generate new presigned URL at the 3rd attempt, or if the URL is expired
if (this.retryCount === 2 || this.isExpired) {
await this.updatePresignedUrl().catch(async () => {
await this.disconnect();
(this.handlers.close || []).forEach((handler) => handler());
logWithTimestamp(
`[MQTT] [${this.caller}] connection has closed! (Unable to get new presigned url)`
);
});
}
});
this.client!.on('reconnect', () => {
this.retryCount += 1;
this.reconnecting = true;
(this.handlers.reconnect || []).forEach((handler) => handler());
logWithTimestamp(
`[MQTT] [${this.caller}] reconnect (#${this.retryCount})`
);
});
this.client!.on('connect', () => {
if (this.reconnecting) {
(this.handlers.reconnected || []).forEach((handler) => handler());
}
this.retryCount = 0;
this.reconnecting = false;
(this.handlers.connected || []).forEach((handler) => handler());
logWithTimestamp(`[MQTT] [${this.caller}] connected`);
resolve(this.client!);
});
});
}
disconnect({ force = true, silent = false, ...options } = {}) {
this.closing = true;
return new Promise<void>((resolve) => {
if (this.client && this.isOnline()) {
this.client.end(force, options, () => {
this.reset(silent, '(fully)');
resolve();
});
} else {
this.client?.end(force);
this.reset(silent, '(client-side only)');
resolve();
}
});
}
reset(silent = false, debug?: any) {
this.client = null;
this.retryCount = 0;
this.reconnecting = false;
this.presignedUrl = '';
this.closing = false;
if (!silent) {
(this.handlers.disconnectd || []).forEach((handler) => handler());
}
logWithTimestamp(`[MQTT] [${this.caller}] disconnected!`, {
silent,
debug,
});
}
async destroy() {
await this.disconnect({ silent: true });
this.handlers = {};
}
// Get or assign a new wss url
async updatePresignedUrl(url?: string) {
if (this.httpClient) {
const service = new RestService<Mqtt>(this.httpClient, '/mqtts');
const { data } = await service.show('wss_link');
this.presignedUrl = data!.wss_link;
} else if (url) {
this.presignedUrl = url;
}
return this.presignedUrl;
}
on(event: MqttServiceEvent, handler: () => void) {
const { [event]: eventHanlders = [] } = this.handlers;
eventHanlders.push(handler);
this.handlers[event] = eventHanlders;
}
off(event: MqttServiceEvent, handler: () => void) {
const { [event]: eventHanlders = [] } = this.handlers;
const index = eventHanlders.findIndex((_handler) => _handler === handler);
eventHanlders.splice(index, 1);
}
get date() {
const matched = this.presignedUrl.match(/(X-Amz-Date=)(\w+)/);
if (!matched) return null;
return new Date(
String(matched[2]).replace(
/^(\d{4})(\d{2})(\d{2})(T\d{2})(\d{2})(\d{2}Z)$/,
(__, YYYY, MM, DD, HH, mm, ss) => `${YYYY}-${MM}-${DD}${HH}:${mm}:${ss}`
)
);
}
get expires() {
const matched = this.presignedUrl.match(/(X-Amz-Expires=)(\d+)/);
return matched ? Number(matched[2]) : null;
}
get signature() {
const matched = this.presignedUrl.match(/(X-Amz-Signature=)(\w+)/);
return matched ? matched[2] : null;
}
get expiresDate() {
if (!(this.date && this.expires)) return null;
return new Date(this.date.getTime() + this.expires * MILISECS_PER_SEC);
}
get isExpired() {
return !this.expiresDate || this.expiresDate <= new Date();
}
private generateClientId() {
return `mqttjs_[${this.caller}]_${Math.random()
.toString(16)
.substring(2, 10)}`.toUpperCase();
}
private isOnline() {
return typeof window !== 'undefined' && window?.$nuxt?.isOnline;
}
}
I'm using graphql-ws https://www.npmjs.com/package/graphql-ws to manage my websocket connection, but am unable to figure out how to handle a dropped connection. Once my internet drops (toggling wifi) or computer sleeps, subscriptions all drop and websocket never reconnects.
closed never gets called. Everything else works as expected, just the disconnects an issue.
createClient({
retryAttempts: 5,
shouldRetry: () => true,
url: "ws://localhost:8080",
on: {
connected: () => {
console.log("CONNECTED");
},
closed: () => {
console.log("CLOSED");
},
error: (e) => {
console.log(e);
},
},
})
);
You can use keepAlive, ping, and pong as a trigger to restart your connection, and keep retryAttempt to infinite.
That's my attempt at keeping the socket alive:
createClient({
url: 'wss://$domain/v1/graphql',
retryAttempts: Infinity,
shouldRetry: () => true,
keepAlive: 10000,
connectionParams: () => {
const access_token = getAccessTokenFunction();
return {
headers: {
Authorization: `Bearer ${access_token || ''}`
}
};
},
on: {
connected: (socket) => {
activeSocket = socket; // to be used at pings & pongs
// get the access token expiry time and set a timer to close the socket
// once the token expires... Since 'retryAttempts: Infinity' it will
// try to reconnect again by getting a fresh token.
const token_expiry_time = getTokenExpiryDate();
const current_time = Math?.round(+new Date() / 1000);
const difference_time = (token_expiry_time - current_time) * 1000;
if (difference_time > 0) {
setTimeout(() => {
if (socket?.readyState === WebSocket?.OPEN) {
socket?.close(CloseCode?.Forbidden, "Forbidden");
}
}, difference_time);
}
},
ping: (received) => {
if (!received)
// sent
timedOut = setTimeout(() => {
if (activeSocket?.readyState === WebSocket?.OPEN)
activeSocket?.close(4408, 'Request Timeout');
}, 5000); // wait 5 seconds for the pong and then close the connection
},
pong: (received) => {
if (received) clearTimeout(timedOut); // pong is received, clear connection close timeout
}
}
})
Tried to make example-event.ts work as HTTP Server. The example is a simple counter. You can subscribe to the count as an event.
It works until a client asks a second time for the value (longpoll).
EventSource ready
Emitted change 1
[binding-http] HttpServer on port 8080 received 'GET /eventsource' from [::ffff:192.168.0.5]:58268
[binding-http] HttpServer on port 8080 replied with '200' to [::ffff:192.168.0.5]:58268
[binding-http] HttpServer on port 8080 received 'GET /eventsource/events/onchange' from [::ffff:192.168.0.5]:58268
[core/exposed-thing] ExposedThing 'EventSource' subscribes to event 'onchange'
[core/content-serdes] ContentSerdes serializing to application/json
Emitted change 2
[binding-http] HttpServer on port 8080 replied with '200' to [::ffff:192.168.0.5]:58268
[binding-http] HttpServer on port 8080 closed Event connection
[core/exposed-thing] ExposedThing 'EventSource' unsubscribes from event 'onchange'
[binding-http] HttpServer on port 8080 received 'GET /eventsource/events/onchange' from [::ffff:192.168.0.5]:58268
[core/exposed-thing] ExposedThing 'EventSource' subscribes to event 'onchange'
[core/content-serdes] ContentSerdes serializing to application/json
[core/content-serdes] ContentSerdes serializing to application/json
Emitted change 3
events.js:377
throw er; // Unhandled 'error' event
^
Error [ERR_STREAM_WRITE_AFTER_END]: write after end
at new NodeError (internal/errors.js:322:7)
at writeAfterEnd (_http_outgoing.js:694:15)
at ServerResponse.end (_http_outgoing.js:815:7)
at SafeSubscriber._next (C:\xxx\node_modules\#node-wot\binding-http\dist\http-server.js:721:45)
at SafeSubscriber.__tryOrUnsub (C:\xxx\node_modules\rxjs\Subscriber.js:242:16)
at SafeSubscriber.next (C:\xxx\node_modules\rxjs\Subscriber.js:189:22)
at Subscriber._next (C:\xxx\node_modules\rxjs\Subscriber.js:129:26)
at Subscriber.next (C:\xxx\node_modules\rxjs\Subscriber.js:93:18)
at Subject.next (C:\xxx\node_modules\rxjs\Subject.js:55:25)
at Object.ExposedThing.emitEvent (C:\xxx\node_modules\#node-wot\core\dist\exposed-thing.js:53:50)
Emitted 'error' event on ServerResponse instance at:
at writeAfterEndNT (_http_outgoing.js:753:7)
at processTicksAndRejections (internal/process/task_queues.js:83:21) {
code: 'ERR_STREAM_WRITE_AFTER_END'
}
I am a little confused by all the subscribing and unsubscribing, but this is maybe because of longpoll is used.
my code:
server side:
Servient = require('#node-wot/core').Servient;
HttpServer = require('#node-wot/binding-http').HttpServer;
Helpers = require('#node-wot/core').Helpers;
// create Servient add HTTP binding with port configuration
let servient = new Servient();
servient.addServer(new HttpServer({}));
// internal state, not exposed as Property
let counter = 0;
servient.start().then((WoT) => {
WoT.produce({
title: 'EventSource',
events: {
onchange: {
data: { type: 'integer' },
},
},
})
.then((thing) => {
console.log('Produced ' + thing.getThingDescription().title);
thing.expose().then(() => {
console.info(thing.getThingDescription().title + ' ready');
setInterval(() => {
++counter;
thing.emitEvent('onchange', counter);
console.info('Emitted change ', counter);
}, 5000);
});
})
.catch((e) => {
console.log(e);
});
});
cient side:
const servient = new Wot.Core.Servient();
servient.addClientFactory(new Wot.Http.HttpClientFactory());
const helpers = new Wot.Core.Helpers(servient);
const addr ='http://192.168.0.5:8080/eventsource';
getTd(addr);
function getTd(addr) {
servient.start().then((thingFactory) => {
helpers
.fetch(addr)
.then((td) => {
thingFactory.consume(td).then((thing) => {
showEvents(thing);
});
})
.catch((error) => {
window.alert('Could not fetch TD.\n' + error);
});
});
}
function showEvents(thing) {
let td = thing.getThingDescription();
for (let evnt in td.events) {
if (td.events.hasOwnProperty(evnt)) {
document.getElementById("events").innerHTML = "waiting...";
thing
.subscribeEvent(evnt, (res) => {
document.getElementById("events").innerHTML = res;
})
.catch((err) => window.alert('error: ' + err));
}
}
}
The problem also occurs if I'm using example-event-client.ts or just send GET requests to "http://192.168.0.5:8080/eventsource/events/onchange" using the browser.
What do I have to do to make the example work?
Next.js provides serverless API routes. By creating a file under ./pages/api you can have your service running, and I want to have a Socket.io service by using this mechanism.
I have created a client:
./pages/client.js
import { useEffect } from 'react';
import io from 'socket.io-client';
export default () => {
useEffect(() => {
io('http://localhost:3000', { path: '/api/filename' });
}, []);
return <h1>Socket.io</h1>;
}
And an API route:
./pages/api/filename.js
const io = require('socket.io')({ path: '/api/filename' });
io.onconnection = () => {
console.log('onconnection');
}
io.on('connect', () => {
console.log('connect');
})
io.on('connection', () => {
console.log('connection');
})
export default (req, res) => {
console.log('endpoint');
}
But I can't get the client to connect to the Socket.io server and succesfully see any of: 'onconnection', 'connect', or 'connection' printed.
The trick is to plug 'socket.io' into the http server only once, so checking every access to the api.
Try something like this:
./pages/api/socketio.js
import { Server } from 'socket.io'
const ioHandler = (req, res) => {
if (!res.socket.server.io) {
console.log('*First use, starting socket.io')
const io = new Server(res.socket.server)
io.on('connection', socket => {
socket.broadcast.emit('a user connected')
socket.on('hello', msg => {
socket.emit('hello', 'world!')
})
})
res.socket.server.io = io
} else {
console.log('socket.io already running')
}
res.end()
}
export const config = {
api: {
bodyParser: false
}
}
export default ioHandler
./pages/socketio.jsx
import { useEffect } from 'react'
import io from 'socket.io-client'
export default () => {
useEffect(() => {
fetch('/api/socketio').finally(() => {
const socket = io()
socket.on('connect', () => {
console.log('connect')
socket.emit('hello')
})
socket.on('hello', data => {
console.log('hello', data)
})
socket.on('a user connected', () => {
console.log('a user connected')
})
socket.on('disconnect', () => {
console.log('disconnect')
})
})
}, []) // Added [] as useEffect filter so it will be executed only once, when component is mounted
return <h1>Socket.io</h1>
}
You have to have the /api/pusher/auth to authenticate with pusher on the frontend. Then you use the key you get from that to communicate with pusher. It's for security purposes. You can do it all through the frontend, but depending on your app, if you're saving data (such as messages, or chats) then probably should authenticate.
You can use custom server and attach sockets to it (just like with express) and provide needed path where socket.io will listen. How to use custom server
You can write something like this server.js
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const { Server } = require('socket.io');
const dev = process.env.NODE_ENV !== 'production';
const hostname = 'localhost';
const port = 3000;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = createServer(async (req, res) => {
try {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;
if (pathname === '/a') {
await app.render(req, res, '/a', query);
} else if (pathname === '/b') {
await app.render(req, res, '/b', query);
} else {
await handle(req, res, parsedUrl);
}
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
});
const io = new Server(server, {
path: '/socket.io' // or any other path you need
});
io.on('connection', socket => {
// your sockets here
console.log('IO_CONNECTION');
});
server.listen(port, err => {
if (err) throw err;
console.log(`> Ready on http://${hostname}:${port}`);
});
});
You would need to run your server using node server.js
I am having a bit of a problem when using crossbar.
I start running autobahn python backend and autobahn js frontend and everything seems normal until I get this error:
2016-08-01T14:12:40+0000 [Router 9051] failing WebSocket opening handshake ('WebSocket connection denied: origin 'null' not allowed')
Any idea why? and how to solve it?
EDIT:
Here is some code (I'm using WAMP) but i believe the problem isn't code related because it has worked in other networks, just not in the one im in right now. Anyways:
Frontend:
<!DOCTYPE html>
<html>
<body>
<h1>Hello WAMP/Browser - Frontend</h1>
<p>Open JavaScript console to watch output.</p>
<script>AUTOBAHN_DEBUG = true;</script>
<script src="http://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"></script>
<script>
// the URL of the WAMP Router (Crossbar.io)
//
var wsuri = "ws://192.168.4.24:8080/";
// the WAMP connection to the Router
//
var connection = new autobahn.Connection({
url: wsuri,
realm: "joyPadRealm"
});
// timers
//
var t1, t2;
// fired when connection is established and session attached
//
connection.onopen = function (session, details) {
console.log("Connected");
// SUBSCRIBE to a topic and receive events
//
function on_counter (args) {
var counter = args[0];
console.log("on_counter() event received with counter " + counter);
}
session.subscribe('com.example.oncounter', on_counter).then(
function (sub) {
console.log('subscribed to topic');
},
function (err) {
console.log('failed to subscribe to topic', err);
}
);
// PUBLISH an event every second
//
t1 = setInterval(function () {
session.publish('com.example.onhello', ['Hello from JavaScript (browser)']);
console.log("published to topic 'com.example.onhello'");
}, 1000);
// REGISTER a procedure for remote calling
//
function mul2 (args) {
var x = args[0];
var y = args[1];
console.log("mul2() called with " + x + " and " + y);
return x * y;
}
session.register('com.example.mul2', mul2).then(
function (reg) {
console.log('procedure registered');
},
function (err) {
console.log('failed to register procedure', err);
}
);
// CALL a remote procedure every second
//
var x = 0;
t2 = setInterval(function () {
session.call('com.example.add2', [x, 18]).then(
function (res) {
console.log("add2() result:", res);
},
function (err) {
console.log("add2() error:", err);
}
);
x += 3;
}, 1000);
};
// fired when connection was lost (or could not be established)
//
connection.onclose = function (reason, details) {
console.log("Connection lost: " + reason);
if (t1) {
clearInterval(t1);
t1 = null;
}
if (t2) {
clearInterval(t2);
t2 = null;
}
}
// now actually open the connection
//
connection.open();
</script>
</body>
</html>
Backend:
from twisted.internet.defer import inlineCallbacks
from twisted.logger import Logger
from os import environ
from autobahn.twisted.util import sleep
from autobahn.twisted.wamp import ApplicationSession, ApplicationRunner
from autobahn.wamp.exception import ApplicationError
class AppSession(ApplicationSession):
log = Logger()
#inlineCallbacks
def onJoin(self, details):
## SUBSCRIBE to a topic and receive events
##
def onhello(msg):
self.log.info("event for 'onhello' received: {msg}", msg=msg)
sub = yield self.subscribe(onhello, 'com.example.onhello')
self.log.info("subscribed to topic 'onhello'")
## REGISTER a procedure for remote calling
##
def add2(x, y):
self.log.info("add2() called with {x} and {y}", x=x, y=y)
return x + y
reg = yield self.register(add2, 'com.example.add2')
self.log.info("procedure add2() registered")
## PUBLISH and CALL every second .. forever
##
counter = 0
while True:
## PUBLISH an event
##
yield self.publish('com.example.oncounter', counter)
self.log.info("published to 'oncounter' with counter {counter}",
counter=counter)
counter += 1
## CALL a remote procedure
##
try:
res = yield self.call('com.example.mul2', counter, 3)
self.log.info("mul2() called with result: {result}",
result=res)
except ApplicationError as e:
## ignore errors due to the frontend not yet having
## registered the procedure we would like to call
if e.error != 'wamp.error.no_such_procedure':
raise e
yield sleep(1)
if __name__ == '__main__':
runner = ApplicationRunner(
environ.get("AUTOBAHN_DEMO_ROUTER", u"ws://192.168.4.24:8080/"),
u"joyPadRealm",
)
runner.run(AppSession)
config.json
{
"version": 2,
"controller": {},
"workers": [
{
"type": "router",
"realms": [
{
"name": "joyPadRealm",
"roles": [
{
"name": "anonymous",
"permissions": [
{
"uri": "",
"match": "prefix",
"allow": {
"call": true,
"register": true,
"publish": true,
"subscribe": true
},
"disclose": {
"caller": false,
"publisher": false
},
"cache": true
}
]
}
]
}
],
"transports": [
{
"type": "websocket",
"endpoint": {
"type": "tcp",
"port": 8080
},
"url": "ws://localhost:8080/"
}
]
}
]
}