Home > Back-end >  TomEE websocket behind an httpd proxy connection timeout
TomEE websocket behind an httpd proxy connection timeout

Time:11-23

In development I have a javascript websocket connecting directly to TomEE and the websocket stays connected with no problems.

In production with TomEE behind an httpd proxy the connection times out after about 30 seconds.

Here is the relevant part of the virtual host config

ProxyPass / ajp://127.0.0.1:8009/ secret=xxxxxxxxxxxx
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:8080/$1" [P,L]

I have tried using the reconnecting-websocket npm library but it seems to keep spawning websockets until chrome runs out of memory. The original websockets remain with status 101 rather that changing to finished.

I did read that the firewall can cause it to disconnect but I searched for firewalld and websocket and couldn't find anything

CodePudding user response:

It looks like the answer is to implement "ping pong". This prevents the firewall or proxy from terminating the connection.

If you ping a websocket (client or server) then the specification says it has to respond (pong). But Javascript websocket depends on the browser implementation so it is best to implement a 30 second ping on the server to all clients. e.g.

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value = "/websockets/admin/autoreply")
public class MyWebSocket {
    private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
    private static final Set<String> alive = Collections.synchronizedSet(new HashSet<String>());

    @OnOpen
    public void onOpen(Session session) throws IOException {
        sessions.add(session);
        alive.add(session.getId());
    }

    @OnMessage
    public void onMessage(Session session, String string) throws IOException {
//       broadcast(string);
    }

    @OnMessage
    public void onPong(Session session, PongMessage pongMessage) throws IOException {
//      System.out.println("pong");
        alive.add(session.getId());
    }

    @OnClose
    public void onClose(Session session) throws IOException {
        sessions.remove(session);
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // Do error handling here
    }

    public void broadcast(String string) {
        synchronized (sessions) {
            for (Session session : sessions) {
                broadcast(session, string);
            }
        }
    }

    private void broadcast(Session session, String string) {
        try {
            session.getBasicRemote().sendText(string);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void ping() {
        synchronized (sessions) {
            for (Session session : sessions) {
                ping(session);
            }
        }
    }

    private void ping(Session session) {
        try {
            synchronized (alive) {
                if (alive.contains(session.getId())) {
                    String data = "Ping";
                    ByteBuffer payload = ByteBuffer.wrap(data.getBytes());
                    session.getBasicRemote().sendPing(payload);
                    alive.remove(session.getId());
                } else {
                    session.close();
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

and the timer service looks like this

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.ScheduleExpression;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;

import org.apache.tomcat.websocket.server.DefaultServerEndpointConfigurator;

import tld.domain.api.websockets.MyWebSocket;

@Singleton
@Lock(LockType.READ)
@Startup
public class HeartbeatTimer {

    @Resource
    private TimerService timerService;

    @PostConstruct
    private void construct() {
        final TimerConfig heartbeat = new TimerConfig("heartbeat", false);
        timerService.createCalendarTimer(new ScheduleExpression().second("*/30").minute("*").hour("*"), heartbeat);
    }

    @Timeout
    public void timeout(Timer timer) {
        if ("heartbeat".equals(timer.getInfo())) {
//          System.out.println("Pinging...");
            try {
                DefaultServerEndpointConfigurator dsec = new DefaultServerEndpointConfigurator();
                MyWebSocket ws = dsec.getEndpointInstance(MyWebSocket.class);
                ws.ping();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }           
        }
    }
}
  • Related