Nothing Special   »   [go: up one dir, main page]

DEV Community

Fabio Peluso
Fabio Peluso

Posted on

A simple WebSocket between Java and React (with hooks)

Async comunication: the phantom menace

When you are working as a Software Engineer or developer, it's only a matter of time since you encounter a great enemy: asynchronous behaviour! You can find it in communication between client and server, or maybe it's included in the programming language or in the framework that you are using.

Websockets: a new hope

During my current project we (me and my team) faced the following problem: after a certain action of the user, the application should prevent every other action and wait for an OK/KO from the server. It was unknown how much time can take the server to send a response, but the requirement was clear: no matter how much time, the user must wait until the response or call the customer service if he thinks it's taking to long.
After a small discussion we decided to try to implement a websocket to let the client wait for a message (even forever if necessary).

What is a Websocket?

I don't want to bother you with informations that you can retrieve by yourself from more authoritative sources. In simple words Websocket is a protocol that allow a full-duplex communication over TCP, allowing both client and server to send/receive message from each other and to manage events based on the message received.

Now, the Frontend is purely React + TypeScript, while the Backend is written in Java in an OSGi framework, so it is not possible to use simple solutions like socket.io that allows deleoper to use the same tech on both FE and BE.

Let's code together - Frontend

Since I was responsible for the Frontend part, I will first describe my React code. Another requirement to keep in mind was that the Websocket is opened at the start of the application.
So I decided to use a ref hook to manage the WebSocket Object and check if it is closed or open and a boolean shouldKeepWSAlive to enable/disable a function that keep alive the connection while waiting for the response:

  const connection = useRef<WebSocket>();
  const shouldKeepWSAlive = useRef<boolean>(false);
Enter fullscreen mode Exit fullscreen mode

After that, we need to what is the event that start the websocket.
Now I had this variable called isWaitingSomething that is responsible for blocking the application as said before so I decided to use a useEffect hook to manage the opening of the Websocket (what is a useEffect?)

useEffect(() => {
    if (!(connection && 
        connection.current && 
        connection.current.readyState === 1))
    {
      connection.current = new WebSocket("ws://path-to-websocket");
      connection.current.onopen = () => {
          //do something, maybe just log that the websocket is open;
      }
      connection.current.onclose = () => {
          //do something, maybe just log that the websocket is closed;
      };
      connection.current.onmessage = (e) => {
        aFunction();
      };
    }
  }, [dependencies]);
Enter fullscreen mode Exit fullscreen mode

Just a little explanation:

  • the if statement on the top helps me to check if connection is already open;
  • if the connection is not open, the code block inside the if open a new connection;
  • onopen and onclose are default events that are triggered when the connection is started and closed;
  • onmessage is the important part: it is the event that is triggered when a message is received on the frontend;
  • aFunction() is my custom function that do the logic that I want;
  • with the correct dependencies the Websocket is opened when the application start;
  • since even the Websocket do timeout, you may need to reopen it.

However, if the server takes to much time to send a message, while waiting the Websocket can timeout and close, so I've added a simple keepAlive() function in this way:

  const keepAlive = useCallback(() => {
    if (shouldKeepWSAlive.current) {
      if (connection.current !== undefined && 
          connection.current !== null &&
          connection.current.readyState === 1) 
      {
        connection.current.send("");
      }
      setTimeout(() => {
        keepAlive();
      }, 20000);
    }
  }, []);

  useEffect(() => {
    if (isWaitingVendi) {
      shouldKeepWSAlive.current = true;
      keepAlive();
    } else {
      shouldKeepWSAlive.current = false;
    }
  }, [isWaitingVendi, keepAlive]);
Enter fullscreen mode Exit fullscreen mode

After that my Websockt was working and performing well.

Let's code together - Backend

In this section I will briefly describe the Java part of the Websocket.
The BE was managed by another team member, so I will not insert detailed explanations, but I've asked him to write a dedicated post.

We are developing in OSGi framework and we use Jetty. The list of required imports is quite long (I have hidden some...):

import com.google.gson.Gson;
import it.hiddenstuff.common.topic.TopicConstants;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
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.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Hashtable;
Enter fullscreen mode Exit fullscreen mode

and here you have the declaration of the Class with correct annotations and the constructor:

@WebSocket
public class SellWebSocket implements EventHandler {

    public SellWebSocket() {
        BundleContext bundleContext = FrameworkUtil.
            getBundle(SellWebSocket.class).getBundleContext();
        Hashtable<String, String> stringStringHashMap = new Hashtable<>();
        stringStringHashMap.
            put( EventConstants.EVENT_TOPIC , TopicConstants.TOPIC_END_SELL);
        bundleContext.
            registerService(EventHandler.class , this ,  stringStringHashMap);
    }
}
Enter fullscreen mode Exit fullscreen mode

Then you need to add some declarations to manage the session, the logs and the endpoints:

    private Session session;
    private RemoteEndpoint remote;
    private Logger log = LoggerFactory.getLogger(getClass());

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    public RemoteEndpoint getRemote() {
        return remote;
    }
Enter fullscreen mode Exit fullscreen mode

As for the Frontend, you need to listen to the events (open, close, send, receive):

    @OnWebSocketConnect
    public void onConnect(Session session) {
        setSession(session);
        this.remote = session.getRemote();
    }

    @OnWebSocketClose
    public void onClose(int statusCode, String reason) {
        this.session = null;
    }

    @OnWebSocketMessage
    public void onText(String message) {
        if (session == null) {
            log.debug("null session");
            // no connection, do nothing.
            // this is possible due to async behavior
            return;
        }
        //do something
    }

    /**
     * Called by the {@link EventAdmin} service to notify the listener of an
     * event.
     *
     * @param event The event that occurred.
     */
    @Override
    public void handleEvent(Event event) {
        //do what you need to do
    }

Enter fullscreen mode Exit fullscreen mode

Conclusions

Before that, I was not quite familiar with all this stuff, and googleing made me a little crazy since it was hard to find a solution with all the right characteristics: sometimes you find socket.io, sometimes you find old React classes while you need Hooks, and some problems like that.
Anyway I managed to pack all together coming out with this solution. Since I don't pretend to be an expert, feel free to comment and add usefull suggestions.
If instead you think this article is usefull, I'd very happy to know 😀

Top comments (0)