James Williams
LinkedInMastodonGithub

Making a Chat Server with Ratpack

In this post, we're going to use Ratpack and WebSocket4J to create a simple chat website and server. I used the method from here to get Ratpack installed into a local Maven store for use with Grape and I pulled in a couple other dependencies that I needed. WebSocket4J also had to be added manually to Maven.

The routing part of the application is fairly simple, the only new addition was setting the 'public' property which we will explain a bit later. Our sole route renders the index.html template. Our WebSocket code is where it gets a little bit interesting. We start a ServerSocket on port 5555 that will initially accept all connections. If the client is trying to connect on any other enpoint but "/chat", the socket is closed. Each client has a ChatServer thread spun up for it.

@Grapes([
    @Grab(group='com.bleedingwolf', module='Ratpack', version='0.2'),
    @Grab(group='org.mortbay.jetty', module='jetty', version='6.1.25'),
        @Grab(group='org.websocket4j', module='WebSocket4J', version='1.3'),    
        @Grab(group='org.json', module='json', version='20090211'),
    @Grab(group='net.sf.mime-util', module='mime-util', version='1.2'),
    @GrabConfig(systemClassLoader=true)
])
def app = Ratpack.app {
    set 'public', 'public'
    get("/") {
        render 'index.html'
    }
}

WebServerSocket socket = new WebServerSocket(5555);
try {
    while (true) {

        WebSocket ws = socket.accept();
        System.out.println("GET " + ws.getRequestUri());
        if (ws.getRequestUri().equals("/chat"))
            (new ChatServer(ws)).start()
        else {
            System.out.println("Unsupported Request-URI");
            try {
                ws.close();
            } catch (IOException e) {
            }
        }
    }

} finally {
    socket.close();
}

This listing below shows the code for the Chat Server. When initialized, it kicks off a new thread and adds the socket to the list of sockets to communicate with. It has one main function that does work called processMessage. processMessage acts on messages with the operation code "broadcast" and sends them to all the other sockets.

import org.json.*
import websocket4j.server.WebSocket
import websocket4j.server.WebServerSocket

public class ChatServer extends Thread {    
  static ArrayList <WebSocket> sockets = []   
  WebSocket ws

  public ChatServer(WebSocket ws) {     
    this.ws = ws      
    sockets.add(ws)   
  }

  def processMessage = { msg ->
    cleanupConnections()
    // convert to JSON object
    def json = new JSONObject(msg)        
    def op = json.getString('operation')      
    if (op == 'broadcast')            
      processBroadcast(json)    
  }

  def cleanupConnections = {        
    def newList = []      
    sockets.each {            
      if (!it.isClosed()) {         
        newList.add(it)           
      }     
    }     
    println "cleaning up connections"       
    sockets = newList 
  }

  def processBroadcast = { json ->       
    for (s in sockets) {          
      s.sendMessage(json.toString())        
    } 
  }

  public void handleConnection() {        
    try {            
      while (true) {                
        String message = ws.getMessage();                
        processMessage(message)                
        if (message.equals("exit"))                    
          break;            
      }        
    } catch (IOException e) {}        
    finally {            
      try {           
        ws.close();            
      } catch (IOException e) { }        
    }    
  }    

  public void run() {        
    handleConnection();    
  }
}

Lastly, we have our HTML code. One thing that you might notice at first glance is that some $ symbols are escaped. The reason for that is both Groovy (which is evaluating the template) and Zepto/JQuery use the $ symbol. Unescaped, Groovy will try to evaluate the contents, otherwise it treats it as text. The same is the case for \ literals like \n. I've colored the extra \'s in red.

I personally like the feel of CoffeeScript so that's what the interaction code is in. When the page loads, it attempts to connect to the chat server on localhost:5555/chat. If the session is new and the user hasn't entered his or her name, there is a popup prompt. When the user clicks send, the message is sent to the server and distributed to all the clients.

<html>
    <head>
        <title>Chat</title>
        <script src="/js/json2.js"></script>
        <script src="/js/zepto.min.js"></script>  

        <script src="/js/coffee-script.js"></script>  
        <script type="text/coffeescript">
            socket = null
            if window.name is ""
                name = prompt 'What is your name'
                window.name = name

            connectToServer = ->
                socket = new WebSocket "ws://localhost:5555/chat"
                socket.onmessage = (event) ->
                    obj = JSON.parse(event.data)
                    console.log(obj)

                    val = $('#chat').get(0).value
                    val += obj.data.sender + ' said:' + obj.data.message + '\n'
                    $('#chat').get(0).value = val
                window.socket = socket

            sendMessage = ->
                msg = $("#message").get(0).value
                packet = {}
                packet.data = {'message':msg, 'sender':window.name}
                packet.operation = 'broadcast'
                json = JSON.stringify(packet)
                console.log(json)
                socket.send(json)

            window.connectToServer = connectToServer
            window.sendMessage = sendMessage
        </script>
    </head>
    <body onload="connectToServer()">
            <h1>Chat</h1>
            <textarea id='chat'readonly rows='10' columns='50' style='width:200px;height:550px'></textarea>
            <br/>
            <input type='text' columns='40' id='message'/>
            <input type='button' value='Send' onclick="sendMessage()"/>
    </body>
</html>

Static files: If you set the application config property "public" to anything, it will attempt to serve what it thinks are files out of that directory. There is currently a bug on trunk for static file serving, you can get the fix on my git fork. Zepto.js is a lightweight JQuery-like library. Here are the links for the other JavaScript libraries I used: json.js and coffee-script.js. All of these files would go in the public directory we specified in the app declaration.