WebSocketsUser Guide

Overview

WebSockets is a Fantom implementation of the W3C WebSocket API and adheres to RFC 6455.

The same WebSocket class may be used as a Fantom desktop client, a Javascript web client, or from a web server.

WebSockets does not currently support frame fragmentation or continuations.

Quick Start

This example is a simple instant messaging application called Chatbox!

It has a basic client that, using the same Fantom code, runs in a browser and as a desktop application. A BedSheet web application is used to service the WebSockets and serve the Chatbox web client.

Due to the web client being Javascript compiled from Fantom code, Chatbox must first be compiled to a pod. All the interesting WebSocket code is in the ChatboxRoutes and ChatboxClient classes.

  1. Create a text file called Chatbox.fan containing the Chatbox code.
  2. Compile the script to create a wsChatbox pod, the warnings are expected and may be ignored.
    C:\> fan Chatbox.fan -build
    
    WARN: Type 'afConcurrent::Synchronized' not available in Js
    WARN: Unknown build option -build
    compile [wsChatbox]
      Compile [wsChatbox]
        FindSourceFiles [1 files]
        CompileJs
        WritePod [file:/C:/Apps/Fantom/fan/lib/fan/wsChatbox.pod]
    BUILD SUCCESS [237ms]!
  3. Start the Chatbox server:
    C:\> fan wsChatbox -server
    
    [info] [afBedSheet] Found mod 'wsChatbox::AppModule'
    [info] [afBedSheet] Starting Bed App 'wsChatbox' on port 8069
    [info] [web] http started on port 8069
    [info] [afIoc] Adding module definitions from pod 'afWebSockets'
    [info] [afIoc] Adding module definition for wsChatbox::AppModule
    [info] [afIoc] Adding module afWebSockets::WebSocketsModule
    [info] [afIoc] Adding module afConcurrent::ConcurrentModule
    [info] [afIoc] Adding module afBedSheet::BedSheetModule
    [info] [afIoc] Adding module afIocConfig::ConfigModule
    [info] [afIoc] Adding module afIocEnv::IocEnvModule
    [info] [afIoc] Adding module afDuvet::DuvetModule
    [info] [afIoc] Adding module wsChatbox::AppModule
    [info] [afIoc] Adding module afBedSheet::BedSheetEnvModule
     ....
       ___    __                 _____        _
      / _ |  / /_____  _____    / ___/__  ___/ /_________  __ __
     / _  | / // / -_|/ _  /===/ __// _ \/ _/ __/ _  / __|/ // /
    /_/ |_|/_//_/\__|/_//_/   /_/   \_,_/__/\__/____/_/   \_, /
               Alien-Factory BedSheet v1.5.0, IoC v3.0.0 /___/
    
    IoC Registry built in 200ms and started up in 78ms
    
    Bed App 'ChatBox - A WebSocket Demo' listening on http://localhost:8069/
  4. Visit http://localhost:8069/ to load a Chatbox web client:

    Chatbox Web Client

    You may also start a Chatbox desktop client:

    C:\> fan wsChatbox -client

    Chatbox Desktop Client

Messages are sent to the server, which then broadcasts it back out all connected clients. Instant messaging!

Usage

The WebSocket class adheres to the W3C WebSocket API and may be used as a client, or on the server, and even from Javascript. It has hooks that allow you to respond to various WebSocket events:

Fantom Client

To create a Fantom WebSocket client:

webSock := WebSocket()
webSock.onMessage |MsgEvent e| { ... }
webSock.open(`ws:localhost:8069`)

// block in an event loop until the socket is closed
webSock.read

Note that WebSocket.read() enters a read event loop that blocks the current thread / Actor until the WebSocket is closed. Therefore it may be advantageous to call the read() method in an asynchronous fashion. Alien-Factory's Concurrent library is the easiest way to do this:

using afConcurrent::Synchronized

// call the blocking read() method in a background thread
safeSock := Unsafe(webSock)
Synchronized(ActorPool()).async |->| {
    safeSock.val->read
}

Javascript Client

To create a Javascript WebSocket client:

webSock := WebSocket()
webSock.onMessage |MsgEvent e| { ... }
webSock.open(`ws:localhost:8069`)

Note that due to the asynchronous of Javascript, there is no need to call the read() method.

BedSheet Server

To use in a BedSheet web application to service HTTP requests, simply create a Route handler that returns a WebSocket instance:

using afWebSockets

const class WsRoutes {
    WebSocket serviceWs() {
        webSock := WebSocket()
        webSock.onMessage |MsgEvent e| { ... }
        return webSock
    }
}

The instance will be saved in the WebSockets service so messages may be sent to the client at any time.

@Inject private const WebSockets webSockets

...
Void sayHello(Uri webSockId) {
    webSockets[webSockId].sendText("Hello Pips!")
}

The WebSocket instance will be automatically disposed of when the connection is closed, either by the client or server.

WebMod Server

To use in a WebMod, the WebSocket instance needs to be upgraded manually:

using afWebSockets

const class WsWebMod : WebMod {

    Void serviceWs() {
        webSock := WebSocket()
        webSock.onMessage |MsgEvent e| { ... }
        webSock.upgrade(req, res)
        webSock.read
    }
}

Or you could store in a WebSockets instance:

using afWebSockets
using web

const class WsWebMod : WebMod {
    const WebSockets webSockets := WebSockets(ActorPool())

    Void serviceWs() {
        webSock := WebSocket()
        webSock.onMessage |MsgEvent e| { ... }
        webSockets.service(webSock, req, res)
    }

    override Void onStop() {
        webSockets.shutdown
    }
}

Chatbox Code

A fully working client and server instant messaging program for the web and desktop!

using afWebSockets
using afIoc
using afBedSheet
using afBedSheet::Text as BsText
using afConcurrent::Synchronized
using afDuvet::DuvetModule
using afDuvet::HtmlInjector
using concurrent::ActorPool
using fwt
using build::BuildPod

class Main {
    Void main(Str[] args) {
        if (args.first == "-client")
            ChatboxClient().main
        if (args.first == "-server")
            BedSheetBuilder(AppModule#.qname).addModulesFromPod("afWebSockets").startWisp(8069)
        if (args.first == "-build")
            Builder().main
    }
}

const class AppModule {
    @Contribute { serviceType=Routes# }
    Void contributeRoutes(Configuration conf) {
        conf.add(Route(`/`,   ChatboxRoutes#indexPage))
        conf.add(Route(`/ws`, ChatboxRoutes#serviceWebSocket))
    }
}

const class ChatboxRoutes {
    @Inject private const WebSockets    webSockets
    @Inject private const HtmlInjector  htmlInjector

    new make(|This|in) { in(this) }

    BsText indexPage() {
        htmlInjector.injectFantomMethod(ChatboxClient#main)
        return BsText.fromHtml(
            "<!DOCTYPE html>
             <html>
             <head>
                 <title>ChatBox - A WebSocket Demo</title>
             </head>
             <body>
             </body>
             </html>")
    }

    WebSocket serviceWebSocket() {
        WebSocket() {
            ws := it
            onMessage = |MsgEvent me| {
                webSockets.broadcast("${ws.id} says, '${me.txt}'")
            }
        }
    }
}

@Js
class ChatboxClient {
    Void main() {
        webSock := WebSocket().open(`ws://localhost:8069/ws`)
        convBox := Text { text = "The conversation:\r\n"; multiLine = true; editable = false }
        textBox := Text { text = "Say something!" }
        sendMsg := |Event e| {
            webSock.sendText(textBox.text)
            textBox.text = ""
        }

        webSock.onMessage = |MsgEvent msgEnv| {
            convBox.text += "\r\n" + msgEnv.txt
        }

        textBox.onAction.add(sendMsg)

        window := Window {
            title = "ChatBox - A WebSocket Demo"
            InsetPane {
                EdgePane {
                    center    = convBox
                    bottom    = EdgePane {
                        center    = textBox
                        right    = Button { text = "Send"; onAction.add(sendMsg) }
                    }
                },
            },
        }

        // desktop only code
        if (Env.cur.runtime != "js") {
            // ensure event funcs are run in the UI thread
            safeFunc := Unsafe(webSock.onMessage)
            webSock.onMessage = |MsgEvent msgEnv| {
                safeMess := Unsafe(msgEnv)
                Desktop.callAsync |->| { safeFunc.val->call(safeMess.val) }
            }

            // call the blocking read() method in a background thread
            safeSock := Unsafe(webSock)
            Synchronized(ActorPool()).async |->| {
                safeSock.val->read
            }
        }

        window.open
    }
}

class Builder : BuildPod {
    new make() {
        podName = "wsChatbox"
        summary = "A WebSocket Demo"

        meta = [
            "proj.name"    : "ChatBox - A WebSocket Demo",
            "afIoc.module" : "wsChatbox::AppModule",
        ]

        depends = [
            "sys          1.0.70 - 1.0",
            "fwt          1.0.70 - 1.0",
            "web          1.0.70 - 1.0",
            "build        1.0.70 - 1.0",
            "concurrent   1.0.70 - 1.0",
            "afIoc        3.0.6  - 3.0",
            "afConcurrent 1.0.20 - 1.0",
            "afBedSheet   1.5.10 - 1.5",
            "afDuvet      1.1.8  - 1.1",
            "afWebSockets 0.2.0  - 0.1",
        ]

        srcDirs = [`Chatbox.fan`]
    }
}