All Ruby on Rails Node JS Android iOS React Native Frontend Flutter

WebSockets on Android Made Easy

After working for a while with WebSockets in our internal project I would like to share my thoughts and experience with you.

What was the problem?

In our application, we needed a solution for local communication between devices. While searching for a solution we had to take under account that one of the devices will need to work as a server. After quick research, we’ve found out the best option was Java WebSockets library. The main advantage is that it has the capability of handling both, a server and a client on the android device. The main implementation of these features required extending two classes: WebSocketClient and WebSocketServer, and overriding their functions. Rest of the logic comes with right message handling.

WebSocket Server

In order to implement sockets server, we need to implement base hook functions.

import org.java_websocket.WebSocket
import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.server.WebSocketServer
import java.net.InetSocketAddress

class CustomWebSocketServer(
   port: Int? = null
) : WebSocketServer(InetSocketAddress(port ?: PORT)) {

   override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) = Unit
   
   override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) = Unit

   override fun onMessage(conn: WebSocket?, message: String?) = Unit

   override fun onMessage(conn: WebSocket?, message: ByteBuffer?) = Unit

   override fun onStart() = Unit

   override fun onError(conn: WebSocket?, ex: Exception?) = Unit

   companion object {
      internal const val PORT = 50123
   }
}

Because we are dealing with a cross-platform application we also need to override onMessage(conn: WebSocket?, message: ByteBuffer?) because iOS sends their messages as bytes. That's the reason why we are be also sending our messages as bytes. If we need to keep devices connected to our server every single one goes through onOpen, just keep a reference to conn: WebSocket. Our project specification requires our server to communicate with all clients at a time so we won’t be needing specific references. The server is able to send broadcasts so it will come in handy.


Before we start implementing messages handling, keep in mind that if you want to keep reusing the same port you should add

isReuseAddr = true

In your init block. If you won’t add it, you either have to change your port with every launch or your server will sometimes have issues with starting on the same port.

To start the server you will have to call the start() method. This method will block the main thread. In our project, we are using RxJava to execute it on a background.

When it comes to messaging  as I mentioned earlier to support the iOS we need to override second onMessage function as well:

override fun onMessage(conn: WebSocket?, message: ByteBuffer?) {
   message ?: return
   val buffer = ByteArray(message.remaining())
   message.get(buffer)
   onMessage(conn, String(buffer, Charset.defaultCharset()))
}

Now, that we are able to also receive messages from iOS we can go into actually interacting with messages. Let’s add a high order function into constructor then:

class CustomWebSocketServer(
   port: Int? = null,
   private val onMessageReceived: (WebSocket?, String?) -> Unit
) : WebSocketServer(InetSocketAddress(port ?: PORT)) {
…
   override fun onMessage(conn: WebSocket?, message: String?) {
      onMessageReceived(conn, message)
   }
…
}

We will be back to our server in a second. It is time to do some work with the client.

WebSocket Client

import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake
import java.net.URI

class CustomWebSocketClient(
   val address: String
) : WebSocketClient(URI(address)) {
    
   override fun onOpen(handshakedata: ServerHandshake?)  = Unit

   override fun onClose(code: Int, reason: String?, remote: Boolean)  = Unit

   override fun onMessage(message: String?)  = Unit

   override fun onError(ex: Exception?)  = Unit
}

As you can see on the server super constructor all we need to do is to pass InetSocketAddress with the given port in the client we have to pass URI with the address with given pattern “ws://${ServerIP}:${WebSocketPort}”. From here all you have to do is to call connect() from a background thread because just like a server it will block the main thread. And we will need to override onMessage(bytes: ByteBuffer?) to receive messages from iOS server.

class CustomWebSocketClient(
   address: String,
   private val onMessageReceived: (String?) -> Unit
) : WebSocketClient(URI(address)) {
…
   override fun onMessage(message: String?) {
      onMessageReceived(message)
   }

   override fun onMessage(bytes: ByteBuffer?) {
      bytes ?: return
      val buffer = ByteArray(bytes.remaining())
      bytes.get(buffer)
      onMessage(String(buffer, Charset.defaultCharset()))
   }
…
}

Now that we have implemented client and server, it is finally time to send some messages.

Messaging

We have decided to handle the messages using the JSON format. To construct and manage our messages we can use the JSONObject type from the org.json package.


If we want to broadcast specific action from our server to notify all connected clients all we have to do is create a simple message and send it as follows:

val jsonObject = JSONObject().apply {
   put("notify", "remember to eat your vegetables")
}

server?.broadcast(jsonObject.toString().toByteArray(Charset.defaultCharset()))

And from the client side, we can now display a received notification:

CustomWebSocketClient(
   address =  "ws://192.160.0.98:50123",
   onMessageReceived = { message ->
      val jsonObject = JSONObject(message)
      if (jsonObject.has("notify")) {
         showSnackbar(jsonObject.getString("notify"))
      }
   }
)

And that’s all you need to implement simple sockets-based server-client communication. You can now expand your implementation with some extra features like notifying about connectivity changes or adding client-server messaging.


As the pro tip, I would like to mention the biggest disadvantage of Java WebSockets is that when your server crashes you need to create a new instance. Fortunately, on a client-side, you can just call reconnect()

Final Worlds

WebSockets, come in handy when your application requires to maintain a persistent connection between your client’s application and server. As for our needs, we managed to handle changes in music playback as well as simply notifying the client app about certain events. Let us know if you are interested in building a client-server app. We are happy to help.


Photo by rawpixel on Unsplash

We're building our future. Let's do this right - join us
READ ALSO FROM Android
Read also
Need a successful project?
Estimate project or contact us