Search code examples
javacookieswebsocketreconnect

Delayed disconnect mechanism for WebSockets


I'm trying to add a "delayed disconnect" mechanism to a WebSocket chat I'm developing. What this means is that if the user is disconnected, but reconnects within a certain time limit - I'm going to use 30 seconds as an example - the disconnection is ignored. The reason for this is a proof-of-concept for if the user briefly loses their connection - e.g. a mobile user entering a lift.

I've decided to use cookies for this. The logic I've figured out is that when a WebSocket is opened, it also opens a HttpSession. From that, I can check to see if a cookie with a particular id exists. If it does, then they are not treated as a new user. However, for this I need to be able to set the expiry time of the cookie for 30 seconds after the socket has been closed.

I already know that Cookie.setMaxAge() would do that, but when I tried this inside the OnClose() method on the server, the server threw a NullPointerException. That's not really a surprise, since I was obviously trying to access the user session after it had been closed.

So, is there a way to do this?

Update 16th of Feb I've decided to try resetting the cookie entirely when a message is sent. This partly works, since the cookies are generated and added to the HttpSession, but upon reconnecting the server thinks the user is entirely new. So, I think my problem is that the cookie isn't being sent to the user.

Update 2 After reading this question, I've moved the cookie generation into a configuration class that is called on a successful handshake. If the request does not have a cookie, it is treated as an entirely new connection, and logs that to the System console as proof of concept. One thing I've had to do was to extend the lifetime of the cookie at the start: currently, it's 10 minutes as a ballpark figure. If I can't find out how to do exactly what I said up above, I'll go with this.

Update 19th of February I've ditched cookies altogether. See my solution.


Solution

  • I solved this by ditching cookies altogether. I've just shown the methods in the relevant classes; if this isn't enough, I will edit my answer to include the full code.

    Inside the configuration class, I get the x-forwarded-for header of the request. This matches the IP address of the client, especially since my backend server is behind a proxy. If the user's IP address is in a list of users, their connection is "refreshed"; otherwise, they are added to the list. Upon disconnection, for whatever reason, the user is marked as disconnected.

    A separate ConnectionMonitor class implements the Runnable interface and runs every 10 seconds, and checks to see if any clients have been disconnected for more than 30 seconds. If they have been, then they are removed from the list of users.

    MyConfigClass.modifyHandshake()

    @Override
    public void modifyHandshake(ServerEndpointConfig config,
                                HandshakeRequest request,
                                HandshakeResponse response)
    {
        HttpSession theSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put(HttpSession.class.getName(), theSession);
    
        String ID = request.getHeaders().get("x-forwarded-for").get(0);
    
        if (ChatroomServerEndpoint.users.containsKey(ID))
        {
    
            // if this user isn't new, add them back onto the list
            User oldUser = ChatroomServerEndpoint.users.get(ID);
            System.out.println("An old user with " + ID + " has returned.");
            ChatroomServerEndpoint.users.remove(oldUser);
            ChatroomServerEndpoint.users.put(ID, oldUser);
            oldUser.toggleConnection(true);
            System.out.println(oldUser + ", " + ChatroomServerEndpoint.users.size() );
    
        }
        else
        {
            // add a new user to the list
            System.out.println("A new user with ID " + ID + " has arrived!");
            User newUser = new User(ID);
            ChatroomServerEndpoint.users.put(ID, newUser);
            System.out.println(newUser + ", " + ChatroomServerEndpoint.users.size() );
        }
    
        // put this ID into the configuration for proof of concept
        config.getUserProperties().put("newUser", ID);
    }
    

    ConnectionMonitor.updateUsers() runs in a separate thread.

    void updateUsers()
    {
    
        for(String id : ChatroomServerEndpoint.users.keySet())
        {
            User theUser = ChatroomServerEndpoint.users.get(id);
            if (theUser.getStatus() == User.Connection.DISCONNECTED)
            {
                // get the time at which the user disconnected
                Calendar disconnectDate = theUser.getdisconnectionDate();
    
                // Calendar.getTime.getTime returns milliseconds,
                // so, multiply maxDisconnectTime by 1000 to see if the user has expired
                if (theDate.getTime().getTime() - disconnectDate.getTime().getTime() 
                        >= maxDisconnectTime * 1000 )
                {
                    System.out.println(id + " has timed out");
                    ChatroomServerEndpoint.users.remove(id);
                }
            }
        }
    }
    

    User

    public class User {
    
    
    // the ID is the user's IP address
    private String id;
    
    // connection status
    public enum Connection
    {
        CONNECTED,
        DISCONNECTED
    }
    
    private Connection status;
    
    // the time of disconnection
    private Calendar disconnectionDate;
    
    // each user needs a WebSocket Session to be able to send and receive messages
    private Session userSession;
    
    /** 
     * @return the id of this user
     */
    public String getId() {
        return id;
    }
    
    public void setId(String id) {
        this.id = id;
    }
    
    /**
     * @return connection status
     */
    public Connection getStatus() {
        return status;
    }
    
    public void setStatus(Connection status) {
        this.status = status;
    }
    
    public Calendar getdisconnectionDate() {
        return disconnectionDate;
    }
    
    public void setdisconnectionDate(Calendar disconnectionDate) {
        this.disconnectionDate = disconnectionDate;
    }
    
    /**
     * @return the userSession
     */
    public Session getUserSession() {
        return userSession;
    }
    
    /**
     * @param userSession the userSession to set
     */
    public void setUserSession(Session userSession) {
        this.userSession = userSession;
    }
    
    /**
     * @param newID the new ID of the user
     */
    public User (String newID)
    {
        this.id = newID;
        this.status = Connection.CONNECTED;
    }
    
    /**
     * Toggles the connection
     * @param toggle - if true, the user is connected
     */
    public void toggleConnection(boolean toggle)
    {
        if (toggle == false)
        {
            status = Connection.DISCONNECTED;
            disconnectionDate = Calendar.getInstance();
        }
        else
        {
            status = Connection.CONNECTED;
            disconnectionDate = Calendar.getInstance();
            disconnectionDate.add(Calendar.HOUR, 1);        // give an extra hour to prevent them being disconnected too soon
    
        }
    }
    

    }