trinopoty / socket.io-server-java

Socket.IO Server Library for Java
Other
183 stars 50 forks source link

Infinite service requests resulting in TcpSocketLink OutOfMemory #6

Closed fracturedexistence closed 4 years ago

fracturedexistence commented 4 years ago

I'm testing out your socket.io solution...

I've got 1 Test user, using chrome browser (latest version) + socket.io (latest JS version) Our Resin4 server: Using your "socket.io-client" solution, this server sends out notifications to our test user via the socket server

wsclient.js - connection setup $(function () {

// make connection

let socket = io.connect();

console.log("Websocket connected");

...

I've tested this setup with node.js+socket-io server.js (a more complete script than the code below) and it works perfectly.

Now, as a test, I decided to disable node.js and try your socket.io-server solution by following your documentation (as best I could), I've modified it slightly, but I get the same quirks whether I follow your instructions explicitly or not. I start my Resin4 server (same server mentioned above, but including the following servlet) It runs ok for a few seconds handling the socket.io-client(java) connection ok, but then it then has major issues (described further below). Note: I also tried let socket = io.connect('ws://localhost:8080/socket.io/?user=test'); with same results

package servlet;

import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import io.socket.emitter.Emitter; import io.socket.engineio.server.EngineIoServer; import io.socket.socketio.server.SocketIoNamespace; import io.socket.socketio.server.SocketIoServer; import io.socket.socketio.server.SocketIoSocket;

@WebServlet("/socket.io/*") public class SocketIoServlet extends HttpServlet {

/**
 * 
 */
private static final long serialVersionUID = 1L;

private final EngineIoServer mEngineIoServer = new EngineIoServer();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[WEBSOCKET] SOCKET.IO: doGet");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[WEBSOCKET] SOCKET.IO: doPost");
}

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    mEngineIoServer.handleRequest(request, response);

    SocketIoServer mSocketIoServer = new SocketIoServer(mEngineIoServer);  // << this seems to result in less repeated iterations
    SocketIoNamespace namespace = mSocketIoServer.namespace("/");

    namespace.on("connection", new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            SocketIoSocket socket = (SocketIoSocket) args[0];
            System.out.println("[SOCKET.IO] Connection. Args=" + args.length+" Socket id=" + socket.getId());

            socket.on("message", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("notify", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("ping", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Ping");
                }
            });
            socket.on("pong", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Pong");
                }
            });
            socket.on("error", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Error");
                }
            });

        }
    });
}

public void init() throws ServletException {
    System.out.println("[WEBSOCKET] SOCKET.IO: Init");

}

public void destroy() {
    System.out.println("[WEBSOCKET] SOCKET.IO: DESTROYED");
}

}

The log file reports...

[SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ [SOCKET.IO] Connection. Args=1 Socket id=N9k8mvJ

(repeats forever(?))

WarningService: Shutdown: TcpSocketLink OutOfMemory [20-06-01 13:11:54.045] {EventThread} Task threw exception java.lang.OutOfMemoryError: GC overhead limit exceeded at io.socket.engineio.client.Transport.onError(Transport.java:63) at io.socket.engineio.client.transports.PollingXHR.access$100(PollingXHR.java:26) at io.socket.engineio.client.transports.PollingXHR$6$1.run(PollingXHR.java:140) at io.socket.thread.EventThread$2.run(EventThread.java:80) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

[20-06-01 13:11:54.045] {resin-port-8080-38} WarningService: Shutdown: TcpSocketLink OutOfMemory Exception in thread "EventThread" java.lang.OutOfMemoryError: GC overhead limit exceeded at io.socket.engineio.client.Transport.onError(Transport.java:63) at io.socket.engineio.client.transports.PollingXHR.access$100(PollingXHR.java:26) at io.socket.engineio.client.transports.PollingXHR$6$1.run(PollingXHR.java:140) at io.socket.thread.EventThread$2.run(EventThread.java:80) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)


It looks like the listener is refusing the connection

Is there something I'm missing?? Is there something your documentation is missing? Do you have a demonstration example of -say- a simple echo server that you could share? Some help would be appreciated

trinopoty commented 4 years ago

The first problem I can see is that you are creating SocketIoServer objects inside the handler. It should be moved to the same place as EngineIoServer object creation. Also, you have not setup websocket handling, at least in the code that you have posted. Please note that the websocket handler and the HTTP handler both need the same object of EngineIoServer and SocketIoServer to function properly. I would highly suggest enabling async support in your servlet as without it, the server is flooded by HTTP requests and the way Java Servlets are implemented, there's no way for EngineIoServer to enable it on a per request basis.

trinopoty commented 4 years ago

SocketIoServer class is somewhat memory intensive and creating a new object for every request both strains the memory and messes up the state of EngineIoServer. It also effects the client ID generation as you can see from the logs. Other than that, can you also post the browser network log and check if there are 400 error codes?

fracturedexistence commented 4 years ago

Hi Thanks for replying.

Yes, I did mention that I tried following the instructions verbatum in your documentation, but I had changed it because it was defective. Ie. was way worse!

So I've changed it back to the original, and added listeners for engineioserver, but as I said, the results are worse. The site dies within seconds because now it's overrun with requests from one single browser.

There are no more files. This is the only file I've implemented because I was of the impression (from your docs) that it was a straight portal of (Node.js) socket.io

package servlet;

import java.io.IOException;

import javax.servlet.ServletException;

import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;

import io.socket.emitter.Emitter; import io.socket.engineio.server.EngineIoServer; import io.socket.engineio.server.EngineIoSocket; import io.socket.socketio.server.SocketIoNamespace; import io.socket.socketio.server.SocketIoServer; import io.socket.socketio.server.SocketIoSocket;

@WebServlet("/socket.io/*") public class SocketIoServlet extends HttpServlet {

/**
 * 
 */
private static final long serialVersionUID = 1L;

private final EngineIoServer mEngineIoServer = new EngineIoServer();
private final SocketIoServer mSocketIoServer = new SocketIoServer(mEngineIoServer);

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[WEBSOCKET] SOCKET.IO: doGet");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[WEBSOCKET] SOCKET.IO: doPost");
}

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    mEngineIoServer.handleRequest(request, response);

    mEngineIoServer.on("connection", new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            EngineIoSocket socket = (EngineIoSocket) args[0];
            System.out.println("[ENGINE.IO] Connection. Args=" + args.length+" Socket id=" + socket.getId());

            socket.on("message", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[ENGINE.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("notify", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[ENGINE.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("ping", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[ENGINE.IO] Ping");
                }
            });
            socket.on("pong", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[ENGINE.IO] Pong");
                }
            });
            socket.on("error", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[ENGINE.IO] Error");
                }
            });
        }           
    });

    SocketIoNamespace namespace = mSocketIoServer.namespace("/");

    namespace.on("connection", new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            SocketIoSocket socket = (SocketIoSocket) args[0];
            System.out.println("[SOCKET.IO] Connection. Args=" + args.length+" Socket id=" + socket.getId());

            socket.on("message", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("notify", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("ping", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Ping");
                }
            });
            socket.on("pong", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Pong");
                }
            });
            socket.on("error", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Error");
                }
            });

        }
    });
}

public void init() throws ServletException {
    System.out.println("[WEBSOCKET] SOCKET.IO: Init");

}

public void destroy() {
    System.out.println("[WEBSOCKET] SOCKET.IO: DESTROYED");
}

}

This time it just goes to town with repeated requests on engine.io instead [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf [ENGINE.IO] Connection. Args=1 Socket id=N9lWJaf ...

RE: "you have not setup websocket handling"

Now I'm really confused, isn't that what socketioserver and engineioserver are? Handlers for socket.io and http connections?

I'm starting to tear my hair out over this. Do I have to set up another websocket handler? is it EngineIoEndpoint? or EngineIoHandler? that you mentoned in the appendix part of your docs? And how do they marry up with my servlet? Sorry, but I'm just confused, If you could elaborate more, it would be helpful

Mel

trinopoty commented 4 years ago

Socket.io supports and prefers websocket but falls back to HTTP if it's not available. It's a bit more tedious to setup websocket in Java but you can follow this to setup websocket support. Also, enable async support on your servlet for better performance.

Move the namespace.on("connection",... event listener outside of your service method and into your constructor. Currently, it's being called on every request and putting a lot of stress on your memory and CPU.

trinopoty commented 4 years ago

I'd suggest moving your EngineIoServer and SocketIoServer objects into a static context so you can access them from both the HTTP and WebSocket handlers.

trinopoty commented 4 years ago

Don't use mEngineIoServer.on(...) to register any event listeners, it'll interfere with SocketIoServer operations. Only use mSocketIoServer.on(...).

fracturedexistence commented 4 years ago

Hi

Ah... Something I missed in your documents were the separation of the 2 servlets So I've now created SocketIoServlet and EngineIoServlet object classes

@WebServlet(value = "/engine.io/*", asyncSupported = true) public class EngineIoServlet extends HttpServlet

@WebServlet(value = "/socket.io/*", asyncSupported = true) public class SocketIoServlet extends HttpServlet

Now... "moving your EngineIoServer and SocketIoServer objects into a static context so you can access them from both the HTTP and WebSocket handlers" is a little abstract. I have a GlobalServlet that handles our shared pools, I guess I could add these declarations in there, and that should satisfy that criteria... (I assume) public static EngineIoServer mEngineIoServer = new EngineIoServer(); public static SocketIoServer mSocketIoServer = new SocketIoServer(mEngineIoServer);

So I've done that now.

This has instantly reduced the number of calls down to a few dozen rather than a zillion (which is promising). Although, shortly after processing the first notifications, it seems to disconnect and refuse all reconnection attempts. 0:=io.socket.engineio.client.EngineIOException: xhr poll error

RE:Don't use mEngineIoServer.on(...) Ok, understood, despite your documentation saying "Attach a listener to the connection event of EngineIoServer to listen for new connections.".

RE:Only use mSocketIoServer.on(...) There is no "mSocketIoServer.on(...)" method, The only way I could see it being achieved is by using "namespace.on(...)"

I'll post the revised code shortly, just to be sure that I'm doing it right

fracturedexistence commented 4 years ago

I won't post my GlobalServlet file because it contains a lot of business sensitive info. But the following static objects are declared in there

public static EngineIoServer wsEngineIoServer = new EngineIoServer(); public static SocketIoServer wsSocketIoServer = new SocketIoServer(wsEngineIoServer);

SocketIoServlet.java package servlet;

import java.io.IOException;

import javax.servlet.ServletException;

import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;

import servlet.GlobalServlet;

import io.socket.emitter.Emitter; import io.socket.engineio.server.EngineIoServer; import io.socket.socketio.server.SocketIoNamespace; import io.socket.socketio.server.SocketIoServer; import io.socket.socketio.server.SocketIoSocket;

@WebServlet(value = "/socket.io/*", asyncSupported = true) public class SocketIoServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

public SocketIoServlet()
{
    EngineIoServer mEngineIoServer = GlobalServlet.wsEngineIoServer;
    SocketIoServer mSocketIoServer = GlobalServlet.wsSocketIoServer;

    SocketIoNamespace namespace = mSocketIoServer.namespace("/");
    namespace.on("connection", new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            SocketIoSocket socket = (SocketIoSocket) args[0];
            System.out.println("[SOCKET.IO] Connection. Args=" + args.length+" Socket id=" + socket.getId());

            socket.on("message", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("notify", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    try {
                        System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                        System.out.println(args[0]);
                    } catch (Exception e) {
                        System.out.println("Error");
                    }
                }
            });
            socket.on("ping", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Ping");
                }
            });
            socket.on("pong", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Pong");
                }
            });
            socket.on("error", new Emitter.Listener() {
                @Override
                public void call(Object... args) {
                    System.out.println("[SOCKET.IO] Error");
                }
            });

        }
    });     
}

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    EngineIoServer mEngineIoServer = GlobalServlet.wsEngineIoServer;
    SocketIoServer mSocketIoServer = GlobalServlet.wsSocketIoServer;

    System.out.println("[SOCKET.IO] SERVICE CALL");
    mEngineIoServer.handleRequest(request, response);
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[SOCKET.IO] SOCKET.IO: doGet");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[SOCKET.IO] SOCKET.IO: doPost");
}

public void init() throws ServletException {
    System.out.println("[WEBSOCKET] SOCKET.IO: Init");
}

public void destroy() {
    System.out.println("[WEBSOCKET] SOCKET.IO: DESTROYED");
}   

}

EngineIoServlet.java @WebServlet(value = "/engine.io/*", asyncSupported = true) public class EngineIoServlet extends HttpServlet {

/**
 * 
 */
private static final long serialVersionUID = 1L;

// private final static EngineIoServer mEngineIoServer = new EngineIoServer();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[WEBSOCKET] SOCKET.IO: doGet");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("[WEBSOCKET] SOCKET.IO: doPost");
}

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    EngineIoServer mEngineIoServer = GlobalServlet.wsEngineIoServer;
    System.out.println("[ENGINE.IO] SERVICE CALL");
    mEngineIoServer.handleRequest(request, response);
}

public void init() throws ServletException {
    System.out.println("[WEBSOCKET] SOCKET.IO: Init");

}

public void destroy() {
    System.out.println("[WEBSOCKET] SOCKET.IO: DESTROYED");
}

}

Logs...

Server sends a message successfully.

[SOCKET.IO] SERVICE CALL [SOCKET.IO] Message. Args=1 Received message: {"command":"initialise","system_name":"RESIN","message":"Hello"}

But then ... 02-06-20 14:34:01 WEBSOCKET ERROR 0:=io.socket.engineio.client.EngineIOException: xhr poll error

02-06-20 14:34:01 WEBSOCKET DISCONNECT 0:=transport error

[SOCKET.IO] SERVICE CALL unable to connect to port.
[SOCKET.IO] SERVICE CALL [SOCKET.IO] SERVICE CALL [SOCKET.IO] Connection. Args=1 Socket id=N9ouC12 02-06-20 14:34:02 WEBSOCKET RECONNECT 0:=1

[SOCKET.IO] SERVICE CALL [SOCKET.IO] SERVICE CALL 02-06-20 14:34:02 WEBSOCKET CONNECT

[SOCKET.IO] SERVICE CALL [SOCKET.IO] SERVICE CALL [SOCKET.IO] SERVICE CALL [SOCKET.IO] Connection. Args=1 Socket id=N9ouC13 [SOCKET.IO] SERVICE CALL [SOCKET.IO] SERVICE CALL [SOCKET.IO] SERVICE CALL 02-06-20 14:34:12 WEBSOCKET ERROR 0:=io.socket.engineio.client.EngineIOException: xhr poll error

02-06-20 14:34:12 WEBSOCKET DISCONNECT 0:=transport error

trinopoty commented 4 years ago

Remove the EngineIoServlet class as it's not needed. The engine.io documentation will ask you to use mEngineIoServlet.on(...) because that's the defined API. SocketIoServer extends EngineIoServer and handles all connections and raw messages. Attaching event listeners on EngineIoServer messes with the control/data flow possibly causing random errors. Yes, I meant namespace.on(...) instead of mSocketIoServer.on(...).

Can you also post your websocket handler code?

fracturedexistence commented 4 years ago

I'm still unsure of how to implement the websocket handler, currently it has fallen back on polling.

How and where do I implement EngineIoEndpoint? I stored it in our src folder but Resin doesn't know what to do with it, so it just sits there doing nothing

trinopoty commented 4 years ago

I'm not familiar with Resin but if it follows the specification properly, you can add the websocket handler by adding the EngineIoEndpoint class and the ApplicationServerConfig class and it should work.

public final class ApplicationServerConfig implements ServerApplicationConfig {

    @Override
    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses) {
        final HashSet<ServerEndpointConfig> result = new HashSet<>();
        result.add(ServerEndpointConfig.Builder
                .create(EngineIoEndpoint.class, "/socket.io/")
                .build());

        return result;
    }

    @Override
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
        return null;
    }
}
fracturedexistence commented 4 years ago

Thanks. I've removed EngineIoServlet. Yeah, I saved EngineIoEndpoint and ApplicationServerConfig to src, and nothing happened. Zilch

RE: websocket handler code? Do you mean the socketio-client java code?

trinopoty commented 4 years ago

So it looks like the websocket implementation for Resin is a bit weird. I found some documentation about getting websocket to work here. By the looks of it, you'll need to modify SocketIoServlet.service(..) to detect if transport query parameter is set to websocket and then inject the WebSocketListener. BTW, you'll also need to write a class that implements WebSocketListener that passes data to EngineIoServer by emiting the message event.

trinopoty commented 4 years ago

If you can get it working, can you please post some of the code so I can include it in the documentation?

fracturedexistence commented 4 years ago

Oh.. yeah those pages are terribly outdated, the examples are defective, and they show no interest in helping their customers to get websockets working. I had words to them about that. Grr.

But i'll read through it and see if I can jig up something that works. And yes, absolutely, I'd be happy to share my code

fracturedexistence commented 4 years ago

Just a question though...

In your documentation, you write... "Handling WebSocket connections involves creating an instance of EngineIoWebSocket and passing it in a call to handleWebSocket method of EngineIoServer."

My question is... EngineIoEndpoint endpoint = new EngineIoEndpoint(); <--- where should this go?

Should it be a) Instantiated in SocketIoServlet.service b) Instantiated in SocketIoServlet.constructor; or c) Instantiated independently during server startup ?

Does it handle the handshake and websocket upgrade process or are we supposed to write those?

trinopoty commented 4 years ago

EngineIoEndpoint is instantiated by the server (I know for a fact that Tomcat does) every time a websocket connection is established. The server is responsible for the websocket handshake and passes the opened connection to a new instance of EngineIoEndpoint which then encapsulates it and passes it to EngineIoServer.

fracturedexistence commented 4 years ago

Ok, I think I've managed to get something working, The "Network" tab is showing a solid websocket connection, and so far the system is holding together without any hiccups.

General... Request URL: ws://localhost:8080/socket.io/?EIO=3&transport=websocket&sid=N9tq38t Request Method: GET Status Code: 101 Switching Protocols

Response... HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Server: Resin/4.0.63 Sec-WebSocket-Accept: cFWvOrwkd2wtBXUbBsQwy8dOQ6Q= Content-Length: 0 Date: Wed, 03 Jun 2020 01:49:20 GMT

Request Headers... GET ws://localhost:8080/socket.io/?EIO=3&transport=websocket&sid=N9tq38t HTTP/1.1 Host: localhost:8080 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)......... Upgrade: websocket Origin: http://localhost:8080 Sec-WebSocket-Version: 13 Accept-Encoding: gzip, deflate, br Accept-Language: en-NZ,en-GB;q=0.9,en-US;q=0.8,en;q=0.7,pl;q=0.6 Cookie: ###REDACTED### Sec-WebSocket-Key: rPA9lAPeoonCrvGys5UeaQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

fracturedexistence commented 4 years ago

SocketIoServlet.java


package servlet;

import GlobalServlet;

import java.io.IOException;
import java.util.Enumeration;

import javax.servlet.ServletException;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.caucho.websocket.WebSocketListener;
import com.caucho.websocket.WebSocketServletRequest;

import io.socket.emitter.Emitter;
import io.socket.engineio.server.EngineIoServer;
import io.socket.socketio.server.SocketIoNamespace;
import io.socket.socketio.server.SocketIoServer;
import io.socket.socketio.server.SocketIoSocket;

@WebServlet(value = "/socket.io/*", asyncSupported = true)
public class SocketIoServlet extends HttpServlet {
    EngineIoServer mEngineIoServer = GlobalServlet.wsEngineIoServer;
    SocketIoServer mSocketIoServer = GlobalServlet.wsSocketIoServer;

    private static final long serialVersionUID = 1L;

    public SocketIoServlet()
    {
        SocketIoNamespace namespace = mSocketIoServer.namespace("/");
        namespace.on("connection", new Emitter.Listener() {
            @Override
            public void call(Object... args) {
                SocketIoSocket socket = (SocketIoSocket) args[0];
                System.out.println("[SOCKET.IO] Connection. Args=" + args.length+" Socket id=" + socket.getId());

                socket.on("message", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        socket.send("message", args);

                        try {
                            System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                            System.out.println(args[0]);
                        } catch (Exception e) {
                            System.out.println("Error");
                        }
                    }
                });
                socket.on("notify", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        socket.send("message", args);
                        try {
                            System.out.println("[SOCKET.IO] Message. Args=" + args.length);
                            System.out.println(args[0]);
                        } catch (Exception e) {
                            System.out.println("Error");
                        }
                    }
                });
                socket.on("ping", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        socket.send("pong", args);
                        System.out.println("[SOCKET.IO] Ping");
                    }
                });
                socket.on("pong", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        System.out.println("[SOCKET.IO] Pong");
                    }
                });
                socket.on("error", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        System.out.println("[SOCKET.IO] Error");
                    }
                });
            }
        });
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("[SOCKET.IO] SERVICE CALL. HttpSession="+request.getSession().getId()+" QueryString="+request.getQueryString());

        String header = request.getHeader("upgrade");
        String transport = request.getParameter("transport");

//testing: have a look at headers
//      Enumeration headers = request.getHeaderNames();
//      while (headers.hasMoreElements()) {
//          String headername = headers.nextElement().toString();
//          System.out.println("Header: " + headername + "=" + request.getHeader(headername));
//      }

        WebSocketListener listener;

        if (!("websocket".equals(header) || transport.equalsIgnoreCase("websocket") )) {

            System.out.println("----------[HTTP]----------");

            mEngineIoServer.handleRequest(request, response);

        } else {

            System.out.println("----------[WEBSOCKET]----------");

            listener = new MyListener();
            WebSocketServletRequest wsReq = (WebSocketServletRequest) request;

            System.out.println("Starting listener...");
            wsReq.startWebSocket(listener);
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("[SOCKET.IO] SOCKET.IO: doGet");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("[SOCKET.IO] SOCKET.IO: doPost");
    }

    public void init() throws ServletException {
        System.out.println("[WEBSOCKET] SOCKET.IO: Init");
    }

    public void destroy() {
        System.out.println("[WEBSOCKET] SOCKET.IO: DESTROYED");
    }

}

MyListener.java

package servlet;

import GlobalServlet;

import com.caucho.websocket.WebSocketContext;
import com.caucho.websocket.WebSocketListener;

import java.io.*;
import java.nio.CharBuffer;
import java.util.logging.Logger;

public class MyListener implements WebSocketListener {
    private static final Logger log = Logger.getLogger(MyListener.class.getName());

    // Instantiate EngineIoEndpoint during creation of this Listener
    private EngineIoEndpoint endpoint = new EngineIoEndpoint();

    public void onStart(WebSocketContext context) throws IOException {
        log.info("[LISTENER] onStart");
    }

    public void onReadBinary(WebSocketContext context, InputStream in) throws IOException {
        log.info("[LISTENER] onReadBinary: NOT IMPLEMENTED!!!");
    }

    public void onReadText(WebSocketContext context, Reader r) throws IOException {
        StringBuilder sb = new StringBuilder();
        CharBuffer cb = CharBuffer.allocate(100);
        while(r.read(cb)>=0)
        {
            cb.flip();
            sb.append(cb.toString());
        }

        log.info("[LISTENER] onReadText: " + sb.toString());
        GlobalServlet.wsEngineIoServer.emit("message", sb.toString());
        log.info("[LISTENER] EMIT TO ENGINE.IO DONE");
    }

    public void onClose(WebSocketContext context) throws IOException {
        log.info("[LISTENER] onClose");
    }

    public void onDisconnect(WebSocketContext context) throws IOException {
        log.info("[LISTENER] onDisconnect");
    }

    public void onTimeout(WebSocketContext context) throws IOException {
        log.info("[LISTENER] onTimeout");
    }
}

GlobalServlet.java

public class GlobalServlet extends HttpServlet 
{
    // This is where we store our shared pools and global data, this object persists as long as the server is running

    public static EngineIoServer wsEngineIoServer = new EngineIoServer();
    public static SocketIoServer wsSocketIoServer = new SocketIoServer(wsEngineIoServer);
}

web.xml

   <servlet servlet-name='GlobalServlet'
            servlet-class='servlet.GlobalServlet' 
            load-on-startup="1">
   </servlet>

   <servlet>
     <servlet-name>SocketIoServlet</servlet-name>
     <servlet-class>servlet.SocketIoServlet</servlet-class>
   </servlet>
   <servlet-mapping url-pattern='/socket.io' servlet-name='SocketIoServlet'/>
fracturedexistence commented 4 years ago

Woops, I spoke too soon. My websockets seem to constantly be in 'pending' state, and data isn't being passed to the browser.

fracturedexistence commented 4 years ago

It seems my attempt to instantiate that 'endpoint' might be the issue, I don't know how else I can get it to work.

You mention "The endpoint can be registered by annotation" What exactly is the annotation that should be used?

trinopoty commented 4 years ago

Have a look at the oracle documentation here. I haven't ever used the annotation before though.

fracturedexistence commented 4 years ago

Thanks. I've tried that already, no effect.

In fact annotations don't seem to work full stop, I'm unsure why and quite frankly I'm getting sick of trying to fix it.

So... after a week of trying to get this working, I give up.

I might have to re-evaluate my choice of server engine at some point, because this is just a nightmare. So, yeah, I'm done

fracturedexistence commented 4 years ago

Update: I've switched to Tomcat 9, and it has solved quite a few issues, but has exposed a few new issues.

Earlier, you told me to delete the "Engine.io servlet" because it wasn't needed.

However, your EngineIoEndpoint references that servlet! Thus: create(EngineIoEndpoint.class, "/engine.io/")

Now I'm confused again. You're creating a connection to a "not needed" class?

or should that actually be: .create(EngineIoEndpoint.class, "/socket.io/")? Note: I've tried changing to this, but it results in a lot of "xhr poll errors"

fracturedexistence commented 4 years ago

I've tried

    result.add(ServerEndpointConfig.Builder
            .create(EngineIoEndpoint.class, "/engine.io/")
            .build());

and

    result.add(ServerEndpointConfig.Builder
            .create(EngineIoEndpoint.class, "/socket.io/")
            .build());

and neither of them result in any events being processed.

Also, the "/socket.io" url path is not functioning at all under tomcat. I tried using the annotation method

@WebServlet(value = "/socket.io/*", asyncSupported = true)

and I also tried defining a servlet-mapping in web.xml,

   <servlet>
     <servlet-name>SocketIoServlet</servlet-name>
     <servlet-class>servlet.SocketIoServlet</servlet-class>
   </servlet>
   <servlet-mapping><servlet-name>SocketIoServlet</servlet-name><url-pattern>/socket.io</url-pattern></servlet-mapping>

but neither method results in a functional path

Admittedly, I'm new to tomcat's quirks, so could you could give me a pointer to get that working?

fracturedexistence commented 4 years ago

Incidentally, I also tried newer annotation methods @WebServlet(asyncSupported = true, displayName="SocketIoServlet", urlPatterns = "/socket.io/*")

Still no luck, this is getting silly now

trinopoty commented 4 years ago

EngineIoServer is provides the underlying transport that SocketIoServer works on top of. All the connections (HTTP, Websocket) are handled by EngineIoServer. The documentation for engine.io uses the /engine.io/ endpoints but for socket.io, you need to replace those with /socket.io/. Also, the instances of EngineIoServer and SocketIoServer must be the same wherever they're used for it to work properly.

As for tomcat, the servlet mappings are processed in the order they appear in web.xml. @WebServlet annotation is much more unpredictable in that matter. More specialized endpoints must go above more generalized endpoints. So, if you put /* above /socket.io/*, your requests will never reach the socket.io servlet.

If HTTP is working properly, I'd suggest setting some breakpoints in EngineIoEndpoint.onOpen to try to debug it. Also, check your browser logs and browser console and those should have some issues if anything isn't working.

You need only one servlet mapped to /socket.io/* and one websocket endpoint mapped to /socket.io/ that calls EngineIoServer.

fracturedexistence commented 4 years ago

Thank you so much for those tips, I'll try them out tomorrow :-D

fracturedexistence commented 4 years ago

Ok, I discovered that SocketIoServlet was definitely initialising, but not serving any http/websocket requests. After a bit of mucking round, I found a solution. Note: I'm using Eclipse+Tomcat9 (latest releases as of June 2020) as my IDE. In order to get SocketIoServlet working as a servlet, I had to change the class definition slightly.

@WebServlet(
        urlPatterns = { "/socket.io/*" },
        asyncSupported = true
        )
public class SocketIoServlet extends HttpServlet implements Servlet {
  ...
}

So it seems that "implements Servlet" and url-pattern "/socket.io/*" (not "/socket.io") were absolutely necessary. This might be a quirk with Tomcat 9 (or even Eclipse perhaps?), I haven't had time to test it. But I think it's important to note if you ever want to create a troubleshooting section.

Now,

I've altered EngineIoEndpoint to grab the static instance of EngineIoServer from my Global Servlet, as follows:

private EngineIoServer mEngineIoServer = GlobalServlet.wsEngineIoServer

And I've changed one line in ApplicationServerConfig as follows (as you instructed)

result.add(ServerEndpointConfig.Builder
                .create(EngineIoEndpoint.class, "/socket.io/")
                .build());

and using an implementation of your SocketIoClient, I'm connecting to ws://mydomain.local:8181/socket.io/?blahblah=1 Apparently it doesn't like empty querystrings and throws null pointer exceptions (hence the blahblah=1)

But still I'm getting the following results in the logs (some of the output is just me printing variable values):

Jun 16, 2020 11:22:01 AM org.apache.catalina.core.ApplicationContext log
INFO: default: DefaultServlet.serveResource:  Serving resource '/socket.io/' headers and data
Updated global counters. Total=21758, recent login count=2
SESSION: init: -1
[SOCKET.IO-CLIENT]  CONNECT ERROR
0:=io.socket.engineio.client.EngineIOException: xhr poll error

After a couple of hours of tinkering I finally managed to get something working.

After selecting "Run as" on SocketIoServlet, Endpoint finally started working. Weird, I would have expected that file to just automatically compile, but nope it needed some manual encouragement. I don't like that 'feature' at all.

Now I'm back to the old problem of the browser connecting by polling, instead of connecting by websocket. But on a positive note, I'm now seeing messages being passed through endpoint.

******************************
SOCKET.IO Service call
******************************
[SOCKET.IO-SERVER] SERVICE CALL. HttpSession=3B54F1CC7C926430D927EAFD79C519B2 QueryString=EIO=3&transport=polling&t=NAwhWnI&sid=NAwejBb
Request.Protocol=HTTP/1.1
Request.Method=POST
Request.ContentType=text/plain;charset=UTF-8
Header: host=mydomain.local:8181
Header: connection=keep-alive
Header: content-length=3
Header: accept=*/*
Header: dnt=1
Header: user-agent=Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Header: content-type=text/plain;charset=UTF-8
Header: origin=http://mydomain.local:8181
Header: referer=http://mydomain.local:8181/messages/
Header: accept-encoding=gzip, deflate
Header: accept-language=en-GB,en-US;q=0.9,en;q=0.8
Header: cookie=#REDACTED#
----------[FIN]----------

******************************
SOCKET.IO Service call
******************************
[SOCKET.IO-SERVER] SERVICE CALL. HttpSession=3B54F1CC7C926430D927EAFD79C519B2 QueryString=EIO=3&transport=polling&t=NAwhWnm&sid=NAwejBb
Request.Protocol=HTTP/1.1
Request.Method=GET
Request.ContentType=null
Header: host=mydomain.local:8181
Header: connection=keep-alive
Header: accept=*/*
Header: user-agent=Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Header: dnt=1
Header: referer=http://mydomain.local:8181/messages/
Header: accept-encoding=gzip, deflate
Header: accept-language=en-GB,en-US;q=0.9,en;q=0.8
Header: cookie=#REDACTED#
----------[FIN]----------
[ENDPOINT] Message (string)
fracturedexistence commented 4 years ago

I've gotten to this point, websocket just says "pending"

network1

General Request URL: ws://mydomain.local:8181/socket.io/?EIO=3&transport=websocket&sid=NAxJjkR Request Method: GET Status Code: 101

Response Headers Connection: upgrade Date: Tue, 16 Jun 2020 04:07:02 GMT Sec-WebSocket-Accept: W6pJnRJWtUhcTGXVyE4R9HnweHI= Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15 Upgrade: websocket

Request Headers Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cache-Control: no-cache Connection: Upgrade Host: mydomain.local:8181 Origin: http://mydomain.local:8181 Pragma: no-cache Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: y5QacU5bDP4lhBQZqCDWbQ== Sec-WebSocket-Version: 13 Upgrade: websocket User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36

Query string Parameters EIO: 3 transport: websocket sid: NAxJjkR

trinopoty commented 4 years ago

I've discovered this quirk with Chrome's Network tab. All websocket connections appear as 'Pending' because websocket is a persistent connection and the 'Time' can only show up when a connection has completed and closed which does not happen for websocket.

Click on the websocket request to bring up the headers information and select the 'Messages' tab and make sure messages are being passed around. At the very least, you will see heartbeat messages like this: Screenshot from 2020-06-16 11-53-24-1

Note that the '/equipment' is my application specific message.

fracturedexistence commented 4 years ago

Ok good to know. Unfortunately, the only Websocket messages are:

2probe | 1 | 16:53:22.175 |   3probe | 1 | 16:53:22.214 |   2 | 1 | 16:55:31.541 |   3 | 1 | 16:55:31.544 |   2 | 1 | 16:55:57.537 |   3 | 1 | 16:55:57.567 |   2 | 1 | 16:56:22.569 |   3 | 1 | 16:56:22.570 |   2 | 1 | 16:56:47.571 |   3 | 1 | 16:56:47.573 |   2 | 1 | 16:57:12.575 |  

The messages are still sending/receiving via the http polling method

:-(

trinopoty commented 4 years ago

Those messages are the keep-alive messages sent by the client. If that's working, normal messages should also work. Even after establishing a websocket connection, a few polling messages are sent. You can test if websocket is working properly by sending periodic messages after say every 10 seconds. It should start sending by websocket after the first or second one.

fracturedexistence commented 4 years ago

Thanks, I'm aware of all that.

After a day (or so) of checking, rechecking, triple checking, etc etc etc I've discovered the problem is with Chrome!!! It was only showing heartbeats and probes in the websocket entry. Nothing else.

I decided to use Firefox instead, and discovered that messages ARE in fact being sent via websocket.

So... yeah. So that's rather crappy. But the problem is solved now. Thank you for your help.

Next problem on the todo list is: how to handle push messages while the user is clicking between pages. The only half decent solution I can think of is to create a "Service Worker" that will receive messages and queue them for delivery to the browser once the websocket is reestablished. Unfortunately this method would exclude a chunk of my customer base who use old browsers.

trinopoty commented 4 years ago

Weird. It works fine on my Chrome. Could be an issue with the client library. socket.io-client version 2.3.0 is what I'm using on my projects and it works on both Chrome and Firefox. As for the page refresh issue, I have always used socket.io with Angular so I don't have any experience or advice for your use case. I'm closing this issue. Feel free to re-open it or create a new issue if you encounter any more problems.