How to check is a Websocket connection is alive
Asked Answered
J

3

5

I have a websocket connection to a server:

import javax.websocket.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

@ClientEndpoint
public class WebsocketExample {

    private Session userSession;

    private void connect() {

        try {
            WebSocketContainer container = ContainerProvider.getWebSocketContainer();
            container.connectToServer(this, new URI("someaddress"));
        } catch (DeploymentException | URISyntaxException | IOException e) {
            e.printStackTrace();
        }
    }

    @OnOpen
    public void onOpen(Session userSession) {
        // Set the user session
        this.userSession = userSession;
        System.out.println("Open");
    }

    @OnClose
    public void onClose(Session userSession, CloseReason reason) {
        this.userSession = null;
        System.out.println("Close");
    }

    @OnMessage
    public void onMessage(String message) {
        // Do something with the message
        System.out.println(message);
    }
}

After some time, it seems I don't receive any more messages from the server but the onClose method has not been called.

I would like to have a sort of timer that would at least log an error (and at best try to reconnect) if I did not receive any message during the last five minutes for instance. The timer would be reset when I receive a new message.

How can I do this?

Janessajanet answered 1/5, 2018 at 13:21 Comment(3)
Why don't you ping/pong? tools.ietf.org/html/rfc6455#section-5.5.2 NOTE: A Ping frame may serve either as a keepalive or as a means to verify that the remote endpoint is still responsive. Legere
Is there a standard way to do that?Janessajanet
read the specification. That's how it's supposed to work. See how your websocket framework implemented it, or write your own. Since you didn't specificy what you use for your websocket code, what framework/library etc.. I can't google for you if it's a part of that framework.Legere
J
8

Here is what I did. I changed javax.websocket by jetty and implemented a ping call:

import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.WebSocketClient;

import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@WebSocket
public class WebsocketExample {

    private Session userSession;
    private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

    private void connect() {
        try {
            SslContextFactory sslContextFactory = new SslContextFactory();
            WebSocketClient client = new WebSocketClient(sslContextFactory);
            client.start();
            client.connect(this, new URI("Someaddress"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnWebSocketConnect
    public void onOpen(Session userSession) {
        // Set the user session
        this.userSession = userSession;
        System.out.println("Open");

        executorService.scheduleAtFixedRate(() -> {
                    try {
                        String data = "Ping";
                        ByteBuffer payload = ByteBuffer.wrap(data.getBytes());
                        userSession.getRemote().sendPing(payload);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                },
                5, 5, TimeUnit.MINUTES);
    }

    @OnWebSocketClose
    public void onClose(int code, String reason) {
        this.userSession = null;
        System.out.println("Close");
    }

    @OnWebSocketMessage
    public void onMessage(String message) {
        // Do something with the message
        System.out.println(message);
    }
}

Edit: This is just a ping example... I don't know if all servers are supposed to answer by a pong...

Edit2: Here is how to deal with the pong message. The trick was not to listen for String messages, but to Frame messages:

@OnWebSocketFrame
@SuppressWarnings("unused")
public void onFrame(Frame pong) {
    if (pong instanceof PongFrame) {
        lastPong = Instant.now();
    }
}

To manage server time out, I modified the scheduled task as follows:

scheduledFutures.add(executorService.scheduleAtFixedRate(() -> {
                    try {
                        String data = "Ping";
                        ByteBuffer payload = ByteBuffer.wrap(data.getBytes());
                        userSession.getRemote().sendPing(payload);

                        if (lastPong != null
                                && Instant.now().getEpochSecond() - lastPong.getEpochSecond() > 60) {
                            userSession.close(1000, "Timeout manually closing dead connection.");
                        }

                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                },
                10, 10, TimeUnit.SECONDS));

... and handle the reconnection in the onClose method

Janessajanet answered 1/5, 2018 at 14:7 Comment(1)
how about session.getBasicRemote().sendPing(null); ?Falsetto
F
0

You should work around this problem by implementing a heartbeat system which one side sends ping and one side answers with pong. Almost every websocket client and server (as far as I know) support this feature internally. This ping/pong frames could be sent from both sides. I usually implement it on server side because I usually know it has better chance to stay alive than clients (my opinion). If clients dont send back pong for long time, I know the connection is dead. On client side, I check the same: If server has not sent ping messages for a long time, I know connection is dead.

If ping/pong are not implemented in libraries you use (which I think javax websocket has it) you could make your own protocol for that.

Falsetto answered 12/5, 2018 at 10:17 Comment(4)
Thanks. I don't own the server, so I can't code anything on its side. When I send ping messages from my client to the server, I never get pong answers howeverJanessajanet
would you provide a code of how you send ping frames?Falsetto
It's in my answer aboveJanessajanet
Ok I checked the answer. If session.getBasicRemote().sendPing(null); didnt work then you just have to contact the backend team and check it with them.Falsetto
L
0

The accepted answer uses Jetty specific API. There's a standard API for this:

to send ping: session.getAsyncRemote().sendPing(data)

to send pong (just keep-alive, without answer) session.getAsyncRemote().sendPong(data)

to react to pongs either session.addMessageHandler(handler) where handler implements MessageHandler.Whole<PongMessage> or create a method that is annotated with @OnMessage and has PongMessage param:

@OnMessage
public void onMessage(PongMessage pong) {
    // check if the pong has the same payload as ping that was sent etc...
}

Periodic ping/keep-alive sending can be scheduled for example using ScheduledExecutorService just as the accepted answer does, but proper care of synchronization must be taken: if session.getBasicRemote() is used then all calls to the remote need to be synchronized. In case of session.getAsyncRemote() probably all containers except Tomcat handle synchronization automatically: see the discussion in this bug report.
Finally, it's important to cancel the pinging task (ScheduledFuture obtained from executor.scheduleAtFixedRate(...)) in onClose(...).

I've developed a simple WebsocketPingerService to ease up things (available in maven central). Create an instance and store it somewhere as a static var:

public Class WhicheverClassInYourApp {
    public static WebsocketPingerService pingerService = new WebsocketPingerService();

    // more code here...
}

You can configure ping interval, ping size, failure limit after which sessions should be closed, etc by passing arguments to the constructor.

After that register your endpoints for pinging in onOpen(...) and deregister in onClose(...):

@ClientEndpoint  // or @ServerEndpoint -> pinging can be done from both ends
public class WebsocketExample {
    private Session userSession;

    @OnOpen
    public void onOpen(Session userSession) {
        this.userSession = userSession;
        WhicheverClassInYourApp.pingerService.addConnection(userSession);
    }

    @OnClose
    public void onClose(Session userSession, CloseReason reason) {
        WhicheverClassInYourApp.pingerService.removeConnection(userSession);
    }

    // other methods here
}
Leopard answered 23/7, 2022 at 13:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.