如何检查WebSocket连接是否仍然存活?

5

我有一个websocket连接到服务器:

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);
    }
}

在一段时间后,似乎我不再从服务器接收到任何消息,但onClose方法并未被调用。
我想要一个定时器,如果在过去的五分钟内没有收到任何消息,它至少会记录一个错误(最好尝试重新连接)。当我收到新消息时,计时器将被重置。
我该如何做到这一点?

1
为什么不进行ping/pong呢?https://tools.ietf.org/html/rfc6455#section-5.5.2 注意:Ping帧可以作为保持连接的方式,也可以用于验证远程端点是否仍然响应。 - Tschallacka
有没有标准的方法来做这件事? - Ben
阅读规范。这就是它应该工作的方式。看看你的WebSocket框架是如何实现的,或者自己编写一个。由于您没有指定用于WebSocket代码的内容,框架/库等,我无法为您搜索谷歌。 - Tschallacka
3个回答

8

这是我的做法。我把javax.websocket改成了jetty,并实现了一个ping调用:

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);
    }
}

编辑:这只是一个ping的例子...我不知道所有的服务器是否都应该用pong回答...

编辑2:以下是如何处理pong消息的方法。关键是不要监听字符串消息,而是帧消息:

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

为了管理服务器超时,我按如下方式修改了预定任务:

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));

... 并在 onClose 方法中处理重新连接


session.getBasicRemote().sendPing(null); 这个怎么样? - Sepehr GH

0
接受的答案使用了Jetty特定的API。这里有一个标准的API可以实现相同的功能:
发送ping:session.getAsyncRemote().sendPing(data) 发送pong(只是保持连接,不需要回应):session.getAsyncRemote().sendPong(data)

要对pong做出反应,可以使用session.addMessageHandler(handler),其中handler实现了MessageHandler.Whole<PongMessage>,或者创建一个带有@OnMessage注解并且具有PongMessage参数的方法:

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

定期发送 ping/keep-alive 可以使用 ScheduledExecutorService 进行调度,就像接受的答案一样,但必须注意正确的同步:如果使用 session.getBasicRemote(),则需要对与远程调用的所有调用进行同步。在使用 session.getAsyncRemote() 的情况下,除了 Tomcat 之外的所有容器可能会自动处理同步:请参阅this bug report中的讨论。
最后,在 onClose(...) 中很重要的是取消 ping 任务(从 executor.scheduleAtFixedRate(...) 获得的 ScheduledFuture)。

我开发了一个简单的 WebsocketPingerService 来简化事情(在 maven central 中可用)。创建一个实例并将其存储在某个静态变量中:

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

    // more code here...
}

你可以通过向构造函数传递参数来配置ping间隔、ping大小、失败限制等。然后在onOpen(...)中注册要进行ping的端点,在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
}

0
你应该通过实现一个心跳系统来解决这个问题,其中一方发送ping,另一方回答pong。几乎所有的websocket客户端和服务器(据我所知)都支持这个特性。这些ping/pong帧可以从两侧发送。我通常在服务器端实现它,因为我通常知道它比客户端更有可能保持活动状态(我的意见)。如果客户端长时间没有回复pong,我知道连接已经断开。在客户端上,我检查同样的事情:如果服务器长时间没有发送ping消息,我知道连接已经断开。
如果你使用的库中没有实现ping/pong(我认为javax websocket有),你可以自己制定协议。

谢谢。我不拥有服务器,所以无法在其端编写任何代码。当我从客户端向服务器发送ping消息时,我从未收到pong回答。 - Ben
你能提供一段发送ping帧的代码吗? - Sepehr GH
它在我上面的答案中。 - Ben
好的,我已经检查了答案。如果 session.getBasicRemote().sendPing(null); 不起作用,那么你只需要联系后端团队并与他们一起检查它。 - Sepehr GH

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接