Introduction

In this blog article I’d like to describe the building of a simple chat server using Kitura and Kitura-WebSocket. The server will support multiple users connected to a single chat. There is no storage of the messages exchanged, any messages sent are only received by those users connected at that moment in time. Slack Technologies don’t worry, this won’t compete with your great offerings.

This blog article is a follow-on to the blog article Working with WebSockets in a Kitura based server

A pre-built version of this chat server with its Web based UI can be found in the repository Kitura-Chat-Server.

While the chat server serves a Web based User Interface, I will not be discussing it in detail in this blog article. This article is focused on the server side of things. Having said that, I will point out that the Web based UI uses browser native WebSocket APIs for its communication with the server. Its look and feel can be seen in the following screen captures (they can be clicked to see an enlarged image):

Initial screen as a user connects to the chat server
Initial screen as a user connects to the chat server
Only one user connected to the chat server
Only one user connected to the chat server
Two users in the chat
Two users in the chat
Here you can see the icon next to a user's display name when that user is typing
Here you can see the icon next to a user’s display name when that user is typing
Some messages exchanged in the chat
Some messages exchanged in the chat
Some messages sent and a user leaves the chat
A user leaves the chat

Messages exchanged between the server and the clients

WebSockets are to be used as the communications transport. However, the use of WebSockets in no way forces a particular message format on the application. In this chat server I’m going to use a set of text messages. Simply due to the fact that text messages are easier to work with in JavaScript in the client.

Each message has two or three parts separated by colons (:). The parts are:

  1. A single character message type.
  2. The display name of the user involved with the message.
  3. The text of a message typed by a user.

The types of the messages are:

c
Sent to a newly connected client, telling the client about each existing participant in the chat.
C
Sent to all connected clients participating in the chat, telling them that a new participant has joined the chat.
D
Sent to all connected clients participating in the chat, telling them that a participant has left the chat.
M
Sent to all connected clients participating in the chat, telling them that a participant has sent a message.
S
Sent to all connected clients participating in the chat, telling them that a participant has stopped typing.
T
Sent to all connected clients participating in the chat, telling them that a participant has started typing.

Most of these messages are sent by a client and forwarded by the server to all of the participants in the chat. The ones that originate from the server are participant left the chat (D) and existing participant of the chat (c).

Writing the server

With all of that background, let’s start building the server.

First set up the directory structure by running:

mkdir ChatServer
cd ChatServer
swift package init
rm Sources/ChatServer.swift
mkdir Sources/ChatServer
mkdir -p public/images
mkdir public/templates

The directory structure of public and below it are used for files of the Web based UI.

The Package.swift file

Edit the Package.swift file and make its contents similar to the following:

import PackageDescription

let package = Package(
    name: "ChatServer",
    dependencies: [
        .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 5),
        .Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", majorVersion: 1, minor: 5),
        .Package(url: "https://github.com/IBM-Swift/Kitura-WebSocket", majorVersion: 0, minor: 5)
    ]
)

The main.swift file

In the directory Sources/ChatServer create the file main.swift and start editing it.

Let’s start by adding the ordinary stuff:

import Foundation

import Kitura
import KituraWebSocket

import HeliumLogger

// Using an implementation for a Logger
HeliumLogger.use(.info)

// All Web apps need a router to define routes
let router = Router()

// Serve the files in the public directory for the web client
router.all("/", middleware: StaticFileServer())

We’ve imported the packages we’ll use in main.swift, setup the HeliumLogger to be the logger, created the Router object, and setup the Router object to serve the static files for the Web UI.

Next we’ll register our WebSocketService implementation, an instance of the ChatService class, with Kitura-WebSocket on the path “kitura-chat”:

WebSocket.register(service: ChatService(), onPath: "kitura-chat")

Next we’ll add code to figure out what port the server should listen on. This is needed if you want to do a cloud deployment.

// Figure out what port we should listen on
let envVars = ProcessInfo.processInfo.environment
let portString = envVars["PORT"] ?? envVars["CF_INSTANCE_PORT"] ??  envVars["VCAP_APP_PORT"] ?? "8090"
let port = Int(portString) ?? 8090

Lastly we’ll create the HTTP Server instance, and start the server running:

// Add HTTP Server to listen on the appropriate port
Kitura.addHTTPServer(onPort: port, with: router)

// Start the framework - the servers added until now will start listening
Kitura.run()

The ChatService class

Now we’ll write the WebSocketService protocol implementation for our very simple chat server.

In the directory Sources/ChatServer create the file ChatService.swift and start editing it.

Let’s start by adding the ordinary stuff and the shell for the ChatService class:

import Dispatch
import Foundation

import KituraWebSocket

class ChatService: WebSocketService {
    
    private let connectionsLock = DispatchSemaphore(value: 1)
    
    private var connections = [String: (String, WebSocketConnection)]()
    
    private enum MessageType: Character {
        case clientInChat = "c"
        case connected = "C"
        case disconnected = "D"
        case sentMessage = "M"
        case stoppedTyping = "S"
        case startedTyping = "T"
    }
}

We’ve added two fields and an enum:

connectionsLock
Used to manage multi-thread access to the thread unsafe connections Dictionary.
connections
A Dictionary that maintains information about the connections of the various clients connected to the server. The key used is the unique identifier of the connection’s WebSocketConnection object. The value is a tuple containing the client’s display name and WebSocketConnection object.
MessageType
An enum of the various supported message type values.

The WebSocketService protocol has four functions that we need to implement. We’ll add them to the ChatService class one by one:

The connected function is invoked when a client connects to the server. We ignore this event, as clients send us an application level “connected” (C) message, when they connect to the server.

    public func connected(connection: WebSocketConnection) {}

The disconnected function is invoked when a client disconnects from the server. When this happens we remove the disconnecting client’s information from the connections Dictionary and send a disconnected (D) message to all of the remaining clients.

Note: The code is wrapped with calls to lockConnectionsLock and unlockConnectionsLock. Those helper functions, respectively, lock and unlock access to the connections Dictionary.

    public func disconnected(connection: WebSocketConnection, reason: WebSocketCloseReasonCode) {
        lockConnectionsLock()
        if let disconnectedConnectionData = connections.removeValue(forKey: connection.id) {
            for (_, (_, from)) in connections {
                from.send(message: "\(MessageType.disconnected.rawValue):" + disconnectedConnectionData.0)
            }
        }
        unlockConnectionsLock()
    }

The received function with a message parameter of type Data, is invoked when a client sent the server a binary message. As this chat server only accepts text messages, we close the connection with a reason code of .invalidDataContents with an appropriate description. This is done using the invalidData helper function, which additionally notifies the other clients that this client has disconnected.

    public func received(message: Data, from: WebSocketConnection) {
        invalidData(from: from, description: "Kitura-Chat-Server only accepts text messages")
    }

The received function with a message parameter of type String, is invoked when a client sent the server a text message. In this function the main processing of this class occurs. To make it easier to explain this function, we’ll add the code to it in pieces.

    public func received(message: String, from: WebSocketConnection) {
        
    } 

The first piece of the received function makes sure the message is at least two characters long, uses the first character as the message type, and saves for later use the displayName sent in the incoming message.

        guard message.characters.count > 1 else { return }
        
        guard let messageType = message.characters.first else { return }
        
        let displayName = String(message.characters.dropFirst(2))
        

The second piece of the received function, handles the message (M), started typing (T), and stopped typing (S) messages. In all three cases, if the connection of the client who sent the message is in the set of connections known by the service, the message is echoed to all of the clients using the echo helper function.

        if messageType == MessageType.sentMessage.rawValue || messageType == MessageType.startedTyping.rawValue ||
                       messageType == MessageType.stoppedTyping.rawValue {
            lockConnectionsLock()
            let connectionInfo = connections[from.id]
            unlockConnectionsLock()
            
            if  connectionInfo != nil {
                echo(message: message)
            }
        }

The third piece of the received function, handles the connected (C) message. The code validates the displayName, sends the set of client displayNames to the newly connected client, adds the connection of the newly connected client to the set of connections, and sends the received connected message to all of the clients via the echo helper function.

        else if messageType == MessageType.connected.rawValue {
            guard displayName.characters.count > 0 else {
                from.close(reason: .invalidDataContents, description: "Connect message must have client's name")
                return
            }
            
            lockConnectionsLock()
            for (_, (clientName, _)) in connections {
                from.send(message: "\(MessageType.clientInChat.rawValue):" + clientName)
            }
            
            connections[from.id] = (displayName, from)
            unlockConnectionsLock()
            
            echo(message: message)
        }

The last piece of the received function, causes the client connection to be closed as the client sent an invalid message. The invalidData helper function is used to do this.

        else {
            invalidData(from: from, description: "First character of the message must be a C, M, S, or T")
        }

The last piece of the ChatService class is a set of helper functions used in the rest of the code for purposes of reuse and readability.

    private func echo(message: String) {
        lockConnectionsLock()
        for (_, (_, connection)) in connections {
            connection.send(message: message)
        }
        unlockConnectionsLock()
    }
    
    private func invalidData(from: WebSocketConnection, description: String) {
        from.close(reason: .invalidDataContents, description: description)
        lockConnectionsLock()
        let connectionInfo = connections.removeValue(forKey: from.id)
        unlockConnectionsLock()
        
        if let (clientName, _) = connectionInfo {
            echo(message: "\(MessageType.disconnected.rawValue):\(clientName)")
        }
    }
    
    private func lockConnectionsLock() {
        _ = connectionsLock.wait(timeout: DispatchTime.distantFuture)
    }
    
    private func unlockConnectionsLock() {
        connectionsLock.signal()
    }

The Web based client for the Chat server

As I said in the beginning of the blog, I’m not going to discuss the Web based client. However, as a server without a client is useless, I will point out that you can get the client from the public directory of the Kitura-Chat-Server repository.

Running the server

The server can be run in several ways, among those many ways are:

  1. Locally
  2. In the cloud. I’ll describe doing this with Bluemix, IBM’s cloud offering.
  3. In a Docker container. I’ll describe this using IBM’s Bluemix Container Service.

Running the server locally

  1. To build the server run:
    swift build
    
  2. To execute the server run:
    .build/debug/ChatServer
    

Once the server starts you can access the UI from a browser by going to the URL
http://hostname:8090, where hostname is the host your server is running on.

Running on Bluemix using the Runtime for Swift

Bluemix is a hosting platform from IBM that makes it easy to deploy your app to the cloud. On Bluemix one can deploy Swift servers in several ways. In this section I will describe deploying on Bluemix using the Runtime for Swift Cloud Foundry build pack. In the next section I will describe deploying on Bluemix using a Docker Container.

To run the ChatServer on Bluemix:

  1. If needed:
    1. Get an account for Bluemix
    2. Download and install the Cloud Foundry tools
  2. Create a manifest.yml file in the root of your repository with the following contents:
    applications:
    - name: ChatServer
      memory: 256M
      host: my-chat-server
    
    
  3. Create a file named Procfile in the root of your repository with the following contents:
    web: ChatServer
    
    
  4. Login to Bluemix, by running:
    cf api https://api.ng.bluemix.net
    cf login
    

    Be sure to run this in the root of your repository.

  5. Run
    cf push
    

    Note: This step will take 3-5 minutes

    You will see output from the deployment as it proceeds, when it is successful you will see:

    1 of 1 instances running
    App started

    In the deployment output you will also see the URL to access your instance of ChatServer.

Deploying as a Docker container on the IBM Bluemix Container Service

The IBM Bluemix Container Service enables you to run applications as Docker containers in the cloud. The container to be run can be built on the cloud as well.

To build and run the ChatServer:

  1. If needed:
    1. Get an account for Bluemix
    2. Download and install the Cloud Foundry tools
    3. Install the CloudFoundry plugin for the IBM Bluemix Container Service by running:
      cf install-plugin https://static-ice.ng.bluemix.net/ibm-containers-mac
      
    4. Set a namespace for your account (note that this can’t be changed once set), running:
      cf ic namespace set namespacename
      

      Where: namespacename is the name space name you have chosen.

  2. Login to Bluemix by running:
    cf api https://api.ng.bluemix.net
    cf login
    cf ic login
    

    Be sure to run this in the directory where the manifest.yml file is located.

  3. Build the container for the ChatServer, by running the command:
    cf ic build -t registry.ng.bluemix.net/namespacename/chat-server:latest --force-rm .
    

    Where: namespacename is the name space name you chose for your account.

  4. Create a container group to run your container, by running the command:
    cf ic group create -m 128 -desired 1 --name chat-server -p 8090 -n hostname -d mybluemix.net registry.ng.bluemix.net/namespacename/chat-server
    

    Where:

    • namespacename is the name space name you chose for your account
    • hostname is the virtual host name you want for your container group

    Once the container in the container group has started you can point a browser at http://hostname.mybluemix.net, where hostname is the host name you specified in the previous command.

Working with the Kitura-Chat-Server’s UI

If you took the Web based UI from Kitura-Chat-Server, here are some directions on how to work with it.

When the Web UI first loads, it asks for a display name that will be used as your identity in
the chat.

As other users join the chat you will see their display names on the left side of the screen.
To send messages, simply type in the input area in the bottom of the screen and press enter.
In the area on the left with the display names of the other users, you will see from time to time
an icon next to one or more of the display names indicating that that user is typing a message
to be sent. The icons will disappear when the users have sent their messages or have paused typing
for a while.

Messages sent by users are shown in the upper area on the right. Each message is displayed with
an indication of who sent it and when it was sent.

Next steps

In this blog I described in detail the building of a simple Chat Server using Kitura and Kitura-WebSocket. A pre-built version of this chat server with its Web based UI can be found in the repository Kitura-Chat-Server.

While the Chat Server is rather simplistic, it is, nonetheless, a multi-user multi-threaded server with users connecting and disconnecting all of the time with messages flowing to and from the server asynchronously.

I hope you’ll run Kitura-Chat-Server with its lovely Web based UI.

Feel free to fork Kitura-Chat-Server, improve it, and submit Pull Requests with your improvements, We’d love to get them.

1 comment on"Writing a WebSocket based chat server using Kitura"

  1. wow, amazing project ! Thanks

Join The Discussion

Your email address will not be published. Required fields are marked *