1

I'm using SockJS + stomp client in angular(1.5.x) to establish websocket with a spring (mvc) server. All works fine except this: if I kill the server, it takes up to two minutes for the stomp client to detect connection error on the browser. Is there a way to manage a much shorter (or immediate) timeout or throw an event as soon as the server died or is disconnected?

function socketService($rootScope, $q, $log, $timeout, URL) {
    var listener = $q.defer(),
        socket = {
        client: null,
        stomp: null
    };

    var reconnect = function() {
        $log.info('Reconnecting');
        $timeout(function() {
            initialize();
        }, 2000);
    };

    var getMessage = function(data) {
        var message = JSON.parse(data), out = {};
        out.message = message;
        if (message.metadata) {
            out.time = new Date(message.metadata.timestamp);
        }
        $log.info(out);
        return out;
    };

    var startListener = function() {
        $log.info('Connected');
        socket.stomp.subscribe(URL.PROCESS_UPDATES, function(data) {
            listener.notify(getMessage(data.body));
        });

        socket.stomp.subscribe(URL.CONTAINER_UPDATES, function(data) {
            listener.notify(getMessage(data.body));
        });
        $rootScope.$broadcast('web_socket_event', 'CONNECTED');
    };

    var errorCallback = function (error) {
        // Browser gets here 2 minutes after the server is killed. Seems like might be affected by the the xhr_streaming timeout 
        $rootScope.$broadcast('web_socket_event', 'DISCONNECTED');
        reconnect();
    };

    return {
        initialize: initialize,
        receive: receive
    };
    function initialize() {
        var header = {
          'accept-version': 1.1
        };
        $log.info('Connecting');
        // custom header to specify version.
        socket.client = new SockJS(header, URL.ROOT + URL.UPDATES);

        socket.client.debug = function(){};
        socket.stomp.heartbeat.outgoing = 0;
        socket.stomp.heartbeat.incoming = 2000;
        socket.stomp = Stomp.over(socket.client);
        socket.stomp.connect({}, startListener, errorCallback);
        socket.stomp.onerror = errorCallback;
        socket.stomp.onclose = reconnect;
    };

    function receive() {
        return listener.promise;
    };
}


**// browser console:**
Opening Web Socket...
  stomp.js:145 Web Socket Opened...
  stomp.js:145 >>> CONNECT
  accept-version:1.1,1.0
  heart-beat:0,2000

  stomp.js:145 <<< CONNECTED
  version:1.1
  heart-beat:2000,0

  stomp.js:145 connected to server undefined
  stomp.js:145 check PONG every 2000ms
user2994871
  • 177
  • 2
  • 13
  • Can you post the javascript code used to create the connection, are you subscribing a callback function to the connection error event? – artemisian Feb 26 '17 at 04:54
  • @artemisian please see code above, thanks! – user2994871 Feb 26 '17 at 05:39
  • Please add `socket.client.debug = function(msg){ console.log(msg)};` and check the heartbeat negotiation (search in the console for heart-beat)? also if any message is printed when the server disconnects – artemisian Feb 26 '17 at 05:49
  • @artemisian I added that but it never gets called. Two minutes after killing the server I get a 'Whoops! Lost connection to http://localhost..." browser console info message which is invoked by stompjs:145:108. – user2994871 Feb 26 '17 at 20:07
  • what about the hearbeats negotiation. check in your developer tools in the network panel in the ws connection frames – artemisian Feb 26 '17 at 20:10
  • on chrome I get an (Opcode - 1) about a minute after initial connection is established. On the the console I get connection closed before handshake response error. Nothing appears immediately on the WS frame after killing the server. I get the same (Opcode -1) after 15:23:06.322 with length 55. – user2994871 Feb 26 '17 at 20:28
  • Once the connection is established you should see a frame similar to `a["CONNECTED\nversion:1.1\nheart-beat:0,20000\n\n\u0000"]` – artemisian Feb 26 '17 at 20:30
  • @artemisian I get CONNECT accept version: 1.1, 1.0 heart-beat: 10000, 10000 followed by CONNECTED version 1.1 heart-beat 0,0 after the connection is established. I am also using grunt-proxy connect with livereload which throws ECONNREFUSED as soon as the server is killed – user2994871 Feb 26 '17 at 20:34
  • I'll add an answer – artemisian Feb 26 '17 at 20:38
  • I've added my answer. Hope it works for you! – artemisian Feb 26 '17 at 20:56
  • @artemisian thanks! Yes looks like I might not be sending any heartbeat. Will refactor accordingly. – user2994871 Feb 26 '17 at 21:20

1 Answers1

4

I'm not an expert in WS but based on our conversation through the question comments, and my understanding of WS it's clear that your server is negotiating a connection with NO heart-beats at all: heart-beat 0,0. The first 0 is the max time (in millis) that the client should expect no packets from the server at all (when this timeout elapses with no communication at all from either side the server should send a heartbeat frame), the 2nd 0 is the equivalent but from looked from the server perspective.

You should set up your server to send a heartbeat periodically and also to expect a heartbeat from the client. This way you allow your server and client to have a better management on the WS connection resources and also you ensure that the connection doesn't get disconnected by the 'network' when applying stalled connection detection policies or by any other mechanism.

I don't know how you have set up your WS server but the below sample applies to a simple WS server in spring boot:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Component
public class WebSocketConfigurer extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        long heartbeatServer = 10000; // 10 seconds
        long heartbeatClient = 10000; // 10 seconds

        ThreadPoolTaskScheduler ts = new ThreadPoolTaskScheduler();
        ts.setPoolSize(2);
        ts.setThreadNamePrefix("wss-heartbeat-thread-");
        ts.initialize();

        config.enableSimpleBroker("/topic")
                .setHeartbeatValue(new long[]{heartbeatServer, heartbeatClient})
                .setTaskScheduler(ts);
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/my/ws/endpoint")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}
artemisian
  • 2,976
  • 1
  • 22
  • 23
  • So in order to set the client to expect heartbeat, is this something I can do via stompJS/SockJS or it would have to be via angular http protocol? – user2994871 Feb 26 '17 at 21:44
  • The client sends a heartbeat propose but the server will have the final word on the heartbeat negotiation terms. So is your spring config who will set the heartbeat timings – artemisian Feb 26 '17 at 23:03
  • 1
    So I've got all the heartbeat stuff wired up and working; however on the browser (Safari Technology Preview) console I can the following error: ' Incompatibile SockJS! Main site uses: "1.1.2", the iframe: "1.0.0" '. In ff/Chrome the error is: 'websocket connection closed before handshake response..'. This error is intermittent. Any idea? – user2994871 Mar 02 '17 at 22:16
  • Sounds like a versioning issue with your sockjs client/server, check the debug logs in your browser. In the same step of the hearbeat negotiation the versioning is also negotiated, your client will tell the server what version it accepts `accept-version:1.1,1.0` then the server will respond which one to use `version:1.1`. Make sure the server respond with one of the version proposed by the client. – artemisian Mar 02 '17 at 22:27
  • Align your versions – artemisian Mar 02 '17 at 22:34
  • I have updated the code above with browser console. I am using websocket:simple-broker to specify heartbeat in the app-context.xml like this: heartbeat="2000, 0". Are you saying I need to specify the version in there as well? – user2994871 Mar 03 '17 at 00:54
  • No, looks your client app (front-end) is using a version of the sock.js javascript library that is incompatible with the sock.js version that the server is running so they are unable to communicate in some scenarios. One of the ways to check the version that the server is using is through the logs in the browser. Ultimately you need to ensure that the sock.js version that the client is using is the same as the one the server is using. Most likely you need to change the sock.js version in the client app. – artemisian Mar 03 '17 at 00:58
  • I have tried 1.0.0, 1.1.1 and currently running with the latest 1.1.2. I get the same error 1.1.x. SockJS 1.0.0 seems to have the following bug that was resolved in 1.1.x version: https://github.com/sockjs/sockjs-client/issues/237 - which I ran into when I tried that version. – user2994871 Mar 03 '17 at 01:01
  • what spring version are you using? – artemisian Mar 03 '17 at 01:03
  • 4.2.7.RELEASE – user2994871 Mar 03 '17 at 01:08
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/137092/discussion-between-artemisian-and-user2994871). – artemisian Mar 03 '17 at 01:11
  • another possible reason for the 'Incompatibile SockJS!' error message is described here: http://stackoverflow.com/questions/41951006/uncaught-error-incompatible-sockjs-angular-2-cli/43657754#43657754 – stefan.m Apr 27 '17 at 12:33