[Solved] Spidermonkey - Server connection listener fires before registation complete

I’m working on developing a traditional client/server project with Spidermonkey. In order to detect and drop dead connections, my protocol has an empty Heartbeat message:

import com.jme3.network.AbstractMessage;
import com.jme3.network.serializing.Serializable;

@Serializable
public class Heartbeat extends AbstractMessage {}

This is registered serverside along with all other message types, and the registration is propagated to the client via the Registration Service. I have a connection listener serverside that sets up a heartbeat listener when a connection is added and triggers the first heartbeat. However, 9 times out of 10 when I would try to connect the client to the server the client would disconnect with a message saying that it couldn’t deserialize a message with type -49. The server log showed that -49 was my heartbeat message. At first I thought that there was some issue with sending empty messages, but after setting a 15-second delay between when the client connects and the first heartbeat sends, the client consistently connects successfully.

From this behavior I concluded that the server connection listener was firing before the registrations propagated to the client. Since the heartbeat was being sent from a threadpool task, it was a race between when the heartbeat sent and when the registration service sent the registrations.That explains why it usually (but not always) failed to connect and why the client didn’t recognize the heartbeat.

Assuming that my diagnosis is correct, is there any way to know serverside when the registration process is complete and the connection is safe to use?

If you send a message too quickly there is a bug where they both end up in the same buffer on the reading side. The problem is that the buffer is deserialized together… so if the first message is the one that delivers the class mappings then the second message will fail to deserialize because the first one hasn’t been processed yet.

It’s not an easy bug to solve as I haven’t thought of a solution that doesn’t involve a fairly major redesign of that section of code. In the mean time, if you just delay a little before sending that first message then you will be fine. Or just make sure that particular message type is registered before anything else on both the client and the server. Like, you’d have to make sure you registered it before you touched any of the server-side services that might be registering their own, etc… because you’d need the ID to be predictable on both ends.

Note: you could also deregister the service that is automatically passing the class registrations and just do all of your class registration manually.

The down side to this is that it’s a pain to make sure that all classes are registered in exactly the same order on both client and server and then you pretty much have to avoid any of the existing service implementations because they all expect to be able to register server-side only.

And answering my answer of an answer… probably the easiest solution is manual registration but using a specific ID that you specify. The automatically generated IDs are kept separate from the ones that users specify in their @Serializable.

So if you specify an ID in your Serializable annotation and make sure to register that class on both the client and server for any messages you will send right away… then you should be able to work around the bug.

In the case of the heartbeat, it really makes more sense to delay before sending it anyway. There’s no point to immediately sending a heartbeat (the purpose of which is to detect a dropped connection) when the connection has been immediately established. I’ll make note of the manual registration though - I may need to use that at some point.

As far as the bug goes… the only thing I can think of is extending the initial connection protocol to have a service setup stage that must be passed before the connection established event is fired. That has a whole host of its own problems though (complexity, dramatic initial connection latency spike, etc).

Thanks a lot for the detailed replies.

I posted a similar question a while ago: https://hub.jmonkeyengine.org/t/how-does-network-client-know-when-it-can-send-its-first-message/38218. I can dig out my code tonight if it will help.

@Muinighin, thanks for linking that thread. I just read it again, and it makes a lot more sense now. For the moment my problem is solved, but if it’s not a bother to post some of your code I’m sure I’m not the only one who would benefit from seeing a solution that enforces safety.

Not as wordy as my usual posts but hopefully this is clear enough and as I’ve said before, I’m relatively new to Java so there may be better ways of doing things. Anyway, here’s my client network communications code.
The bits we’re particularly interested in here interested in are the java.util.concurrent classes (look them up), my ConnectionFlag class and addClientStateListener.
The client state listener is called when the connection state changes. This lock used by the listener tells the class when everything using ConnectionFlag connected.
A call to start() in turn calls connected.waitForConnected(); which doesn’t return until everything is ready. Obviously, the server doesn’t need any of this guff.
[edit: Comms is my abstract class underlying my ServerComms and ClientComms classes]

import com.jme3.network.Client;
import com.jme3.network.ClientStateListener;
import com.jme3.network.Network;
import com.jme3.network.Message;
import java.io.IOException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Network client connection using SpiderMonkey
 *
 * Incoming messages are placed in order into a message queue, and can be read
 * in order from there All classes my be @serializable and passed to the
 * constructor.
 *
 */
public class ClientComms extends Comms {

    private static final Logger logger = Logger.getLogger(ClientComms.class.getName());

    Client client;
    int port = -1;
    String ipAddress = null;
    String name = null;
    ConnectionFlag connected = new ConnectionFlag();
    /**
     *
     */
    public ClientComms() {
        initialiseComms();
    }

    /**
     * open the network connection, ready to receive messges
     *
     * @param name string identifier of connection; only used for reporting at
     * startup, non functional
     * @param ipAddress ip address of the server to connect to
     * @param port the port number to open, listening for incoming messages
     * @param classes   array of classes the server will send as messages
     *
     * @return true on success, false on failure
     */
    public boolean start(String name, String ipAddress, int port, Class <?>[] classes) {
        boolean restart = this.port != -1;
        this.classes = classes;
        try {
            client = Network.connectToServer(ipAddress, port);
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Failed to start Client for {0} on Port {1}", new Object[]{name, Integer.toString(port)});
            return (false);
        }
        if (!restart)
            client.addClientStateListener(new GameClientStateListener(connected));
//        initialiseSerialization(classes);

        client.start();
        if (!restart)
            addMessageListeners(classes);
        this.port = port;
        this.name = name;
        try {
            connected.waitForConnected();
        } catch (InterruptedException ex) {
            logger.log(Level.SEVERE, null, ex);
        }
        logger.log(Level.INFO, "Started Client for {0} on Port {1}", new Object[]{name, Integer.toString(port)});
        return (true);
    }

    /**
     * disconnect from the server; any unread messages still in the queue remain
     * available for reading
     *
     * @return true on success, false on failure
     */
    public boolean stop() {
        if (!connected.getConnected()) {
            logger.log(Level.SEVERE, "Network CLIENT already stopped!");
            return (false);
        }

        client.close();
        logger.log(Level.SEVERE, "Stopped Client");
        return (true);
    }

    /**
     * reconnect to the server, using the original settings
     *
     * @return true on success, false on failure
     */
    public boolean restart() {
        if (port == -1) {
            logger.log(Level.SEVERE, "Network CLIENT has not previously connected!");
            return (false);
        }

        if (connected.getConnected()) {
            logger.log(Level.SEVERE, "Network CLIENT is already connected!");
            return (false);
        }

        return (start(name, ipAddress, port, classes));
    }

    /**
     * send a message to the server must already be connected
     *
     * @param message
     * @return true on success, false on failure
     */
    public boolean send(Message message) {
        if (message == null) {
            logger.log(Level.SEVERE, "Client sent NULL message:\n");
            return (false);
        }
        if (message instanceof StringMessage) {
            logger.log(Level.SEVERE, "Client attempting to send StringMessage:\n {0}", ((StringMessage) message).getString());

        }
        client.send(message);
        logger.log(Level.INFO, "Client sent message:\n {0}", message);
        return (true);
    }

    /**
     * Read the next message from the message queue
     *
     * @return the IdedMessage from queue comprising the message and the
     * connection id of the sender
     */
    public IdedMessage read() {
        return (messageQueue.read());
    }

    private void addMessageListeners(Class<?>[] classes) {
        for (Class clazz: classes) {
            addMessageListener(clazz);
        }
    }

    private void addMessageListener(Class<?> clazz) {
        client.addMessageListener(new ClientListener(messageQueue), clazz);
    }
    
    private class GameClientStateListener implements ClientStateListener {
        ConnectionFlag connectedFlag;
        
        GameClientStateListener(ConnectionFlag connectedFlag) {
            this.connectedFlag = connectedFlag;
        }
        
        @Override
        public void clientConnected(Client c) {
            try {
                connectedFlag.connect();
            } catch (InterruptedException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
            System.out.println("clientConnected(" + c + ")");
        }

        @Override
        public void clientDisconnected(Client c, DisconnectInfo info) {
            try {
                connectedFlag.disconnect();
            } catch (InterruptedException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
            System.out.println("clientDisconnected(" + c + "):" + info);
        }        
    }
    
    private class ConnectionFlag {

        final Lock lock = new ReentrantLock();
        final Condition connection = lock.newCondition();
        final Condition disconnection = lock.newCondition();
        private boolean connected = false;
        
        public void waitForConnected() throws InterruptedException {
            lock.lock();
            if (!getConnected())
                connection.await();
            lock.unlock();
        }

        public void connect() throws InterruptedException {
            lock.lock();
            connection.signal();
            connected = true;
            lock.unlock();
        }

        public void disconnect() throws InterruptedException {
            lock.lock();
            connected = false;
            disconnection.signal();
            lock.unlock();
        }
        
        
        public synchronized boolean getConnected() {
            return(connected);
        }

    }
}

Easiest way… don’t even attach your network “game” app states until the connection is up.

See these examples:

Non-ES version:

ES version:

Edit: even aside from the Sim-Ethereal parts, it’s a decent baseline for setting up a networked JME application.