Reputation: 513
I have a rather strange and specific problem regarding websockets on iOS devices.
I'm developing a browser based web app which communicates with a web server via websockets. For the client side the browser's native websocket is used, so there is no library involved (no socket.io etc.).
On the server side I am using Node.js with the ws module as a websocket server.
On desktop browsers and Android devices everything works fine, but on iOS the connection quite often "hangs", meaning the browser doesn't respond to messages received on the websocket connection. The websocket's "onmessage" handler doesn't get fired.
However when debugging the server, I can clearly see how the message leaves the server and is sent to the browser, but when this problem occurs then on Safari nothing happens. It seems like the message is "stuck" somewhere in the device, because if I trigger another event on the GUI (e.g. moving a slider or clicking buttons) then the "onmessage" is executed immediately. As all browsers on iOS share the same backend, this happens on Safari, Chrome and Firefox.
I admit that the messages can become very big, i.e. they can become some 100kb long. I read that some websocket implementations have problems with these magnitudes, so I have tried splitting them into several chunks on application level, but so far without success.
Maybe it's worth to mention that the server OS is Windows.
This is my simple client side code (abstract):
var socket = new WebSocket("ws://myurl.com");
socket.onmessage = function(e) {
// Sometimes gets stuck before calling this handler.
// Can be resolved with triggering any event on the UI.
processData(e.data);
}
socket.onerror = function(e) {
logError(e);
}
socket.onclose = function(e) {
cleanUp();
}
The server side looks something like this:
var webServer = require("http")
.createServer()
.listen(80, "0.0.0.0", function () {
console.log("Listening on port " + 80);
});
var WebSocketServer = require("ws").Server;
var wss = new WebSocketServer({server: webServer});
wss.on("connection", function(ws) {
ws.on("message", function(message) {
processMessage(message);
});
});
Playing around with Content Security Policy as mentioned here (but it's maybe more a Meteor / Cordova issue)
Splitting the message into chunks on application level (said here)
Sending some dummy acknowledgement bytes or disabling Nagle's algorithm (as suggested here)
Wrapping the onmessage callback into an setTimeout()
(here)
Protocols WS or WSS make no difference
Running the Autobahn Test Suite. Sometimes it passes all tests, sometimes some of them fail probably due to timeout or a too long execution time
Could someone maybe give me some hints or has experience with this kind of problems?
Update 02.02.017
Also worth to mention is that my application is a 3D application rendering in a loop via requestAnimationFrame.
After many days of research I found out there seems to be a problem regarding requestAnimationFrame + WebSockets onmessage handler.
I will update my findings tomorrow.
Upvotes: 1
Views: 2856
Reputation: 513
This problem seems to arise if a 3D render loop is created via requestAnimationFrame
and at the same time the application is receiving data using Websocket's onmessage
handler.
A solution is to substitute requestAnimationFrame
with an alternating call of setTimeout
(see example below), mimicking requestAnimationFrame
with setTimeout
or using setInterval
as a workaround.
I found evidence for this bug in chromium's issue tracker (here and there), so I don't know how much this bug is related to Apple's Webkit, but at least the symptoms described there are very similar and the solutions / workarounds have helped me too.
Below I'm quoting the solution from this source and all credits go to comment #30 there:
var altframe = true, frametime = 0, lastframe = Date.now();
function Run() {
frametime = Date.now() - lastframe;
lastframe = Date.now();
if (!altframe)
setTimeout(Run,frametime);
// Hard-scheduled based on timing of last frame, must schedule before sim+render.
/*
SIM AND RENDER CODE
*/
if (altframe) {
window.requestAnimationFrame(Run);
// Normal timing, called after next paint.
altframe = false; // Alternate frame will be timed based on the next one.
}
else {
altframe = true; // Because we can't switch it above.
}
};
Run(); // As if we just had our setTimeout fire, altframe should be true
Upvotes: 3