New Websocket API

A patch released in August, 2017 for Allegro CL 10.0 and 10.1 implements a websocket API which allows users to implement websocket server and client applications in Lisp. The Allegro CL websocket API is described in Websocket API in miscellaneous.htm.

In this note, we provide a very simple example using the API. The websocket module in ACL allows one to implement websocket server and client applications in Lisp (many sites on the web offer more complex demos and tutorials in its use). The websocket protocol is specified in RFC6455. In this example the server is implemented in Lisp and a client is implemented in HTML/Javascript and also in Lisp.

This example uses some Common Lisp source files and some html files. These are all fairly short and all are appended to this article, suitable for cutting and pasting. You can also download a zip file which unzips into a directory named websock/ containing all the files and also an abbreviated text file (simplews.txt) with the instructions from this article.

The example starts a server in one Lisp image. Then it communicates with that server with a browser and also with a client started in another Lisp image. When the server receives a message, its action in response to the message is determined by the on-message function specified in the call to publish-websocket (which is called by the echo-server function defined in server.cl).

The on-message in our example is:

:on-message (lambda (contract data ext)
              (declare (ignore ext))
              (cond ((equalp data "add") (setq *mode* :add))
                    ((equalp data "none") (setq *mode* nil)))
              (case *mode*
                (:add (setq data (concatenate 'string data "...added..."))))
              (websocket-send contract data))

The value of *mode* is initially nil. The server does nothing for any message until the message "add" arrives. Then it responds to that and every subseqeuent message with '[message]...added...' unless a message "none" arrives, in which case *mode* is set to nil and responses stop. This image shows an interaction.

Clicking Open opened the connection. The first two messages ('12345' and 'another) were sent and received but elicited no response. The message 'add' enabled response mode, and messages are responded to with '...added...'. Then the mesasage 'none' turns off responses and subseqeuent messages are not responded to. Clicking Close closes the connection.

This is obviously a simple example but it provides a template for more complex cases.

Here are the steps to running the example:

  1. Start a patched Allegro CL 10.0 or 10.1.
  2. Load the websocket API and (optionally) use the relevant packages:
    (require :websocket)
    (use-package :net.aserve)
    (use-package :net.aserve.client)
    
  3. Edit the host in client and browser source files if necessary. The source files assume the server Lisp, the client Lisp and the browser are all running on the same machine, which can thus be referred to as 'localhost'. If the client Lisp is on a different machine, the client Lisp files must be edited to replace 'localhost' with the actual host name. If the browser is running on a different machine, the html files must be edited to replace 'localhost' with the actual host name.
  4. Start the simple server in a Lisp image by loading the file server.cl and calling the function echo-server:
    :ld server.cl
    (echo-server) ;; add arguments ':port XXXX' if
                  ;; you want a port other than 9001
    
  5. Open a web browser on the file client.html. You should see buttons like those at the top of the illustration above.

Open a connection by clicking Open in the browser. Then send messages. As noted above, sending the message 'add' makes the server respond to messages beyond simply returning them. 'none' stops those responses. The picture above shows a typical interaction. (Occasionally Lisp will print a message about sockets being closed and thus not open-for-output but this seems to be a oddness with some browsers as the communication continues to work.)

Starting a client in a Lisp image

Start a second Lisp image for a Lisp client interaction, load client.cl and execute more forms as shown:

cl-user(5): :ld client.cl
; Loading client.cl
cl-user(6): (echo-client)  ;; add arguments ':port XXXX' if
                           ;; you want a port other than 9001
#<websocket-message-client-contract @ #x201f6c802>
cl-user(7): (echo-send "message1")
:text
cl-user(8): 
RECEIVED message1
(echo-send "message2")
:text
cl-user(9): 
RECEIVED message2
(echo-send "add")
:text
cl-user(10): 
RECEIVED add...added...
(echo-send "after browser")
:text
cl-user(11): 
RECEIVED after browser
(echo-close)
CLOSED with code 1000 
1000

In the server, you can switch to the more elaborate server by calling

(multi-server)

This more complex server maintains a separate application state for each client and illustrates how other event handlers are used. So in the Lisp client, you can do:

cl-user(18): (echo-client :url :multi)
#<websocket-message-client-contract @ #x201fdd592>
cl-user(19): (echo-send "from Lisp client")
:text
cl-user(20): 
RECEIVED from Lisp client
(echo-send "after web add")
:text
cl-user(21): 

Load client2.html into a browser and you can have an interaction as shown in the illustration:

Associated source files

A zipped directory containing all source files used in this example along with the images displayed above and a text file with example instructions can be downloaded from ftp://ftp.franz.com/pub/src/tech_corner/websocket.tar.gz. All the source files are also displayed below suitable for cutting and pasting.

The files are:

server.cl

The port keyword argument in the functions echo-server and multi-server defaults to 9001. Specify :port XXXX in the call to echo-server if you wish to use aother port.

(in-package :user)

(eval-when (compile eval load)
  (require :websocket)
  (use-package :net.aserve))

(defvar *mode* nil)

(defun echo-server (&key (port 9001) debug)
  ;; Simple server with global state.
  (start :port port)
  (setq *mode* nil)
  (publish-websocket
   :path "/simple-echo"
   :on-message (lambda (contract data ext)
		 (declare (ignore ext))
		 (cond ((equalp data "add") (setq *mode* :add))
		       ((equalp data "none") (setq *mode* nil)))
		 (case *mode*
		   (:add (setq data (concatenate 'string data "...added..."))))
		 (websocket-send contract data))
   :debug debug)
  *wserver*)


(defvar *modes* nil)

(defun multi-server (&key (port 9001) debug)
  ;; More complex server that keeps per-client state. 
  (start :port port)
  (setq *modes* (make-hash-table))
  (publish-websocket
   :path "/multi-echo"
   :on-open (lambda (contract)
	      (setf (gethash contract *modes*) (list :mode nil)))
   :on-message (lambda (contract data ext
			&aux (plist (gethash contract *modes*)))
		 (declare (ignore ext))
		 (cond ((equalp data "add") (setf (getf plist :mode) :add))
		       ((equalp data "none") (setf (getf plist :mode) nil)))
		 (case (getf plist :mode)
		   (:add (setq data (concatenate 'string data "...added..."))))
		 (websocket-send contract data))
   :on-close (lambda (contract code data)
	       (declare (ignore code data))
	       (remhash contract *modes*))
   :debug debug)
  *wserver*)

client.cl

In the function echo-client, the port keyword argument defaults to 9001 and the host is specified as 'localhost' in the bindings of 'url'. Specify a different port is the call to echo-client if desired and modify the source to specify a different host if the client Lisp will run on a different host from the server Lisp.

(in-package :user)

(eval-when (compile eval load)
  (require :websocket)
  (use-package :net.aserve)
  (use-package :net.aserve.client))

(defvar *ws* nil)
(defun echo-client (&key (url :simple) (port 9001) debug)
  (case url
    (:simple (setq url "ws://localhost:~A/simple-echo"))
    (:multi (setq url "ws://localhost:~A/multi-echo")))
  (setq *ws*
    (open-websocket (format nil url port)
		    :on-message (lambda (contract data ext)
				  (declare (ignore contract ext))
				  (format t "~&RECEIVED ~A~%" data))
		    :on-close (lambda (contract code data)
				(declare (ignore contract))
				(format t "~&CLOSED with code ~A ~A~%"
					code data))
		    :debug debug)))

(defun echo-send (text) (websocket-send *ws* text))

(defun echo-close () (close-websocket *ws* :wait t))

client.html

This file refers to 'localhost:9001'. If the browser will be running on a different host from the Lisp server, 'localhost' must be changed to the actual host. If the port used is not 9001, that value too must be edited.

<!DOCTYPE html>

<html>
  <head>
    <title>Echo Chamber</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
  </head>
  <body>
    <div>
      <input type="text" id="messageinput"/>
    </div>
    <div>
      <button type="button" onclick="openSocket();" >Open</button>
      <button type="button" onclick="send();" >Send</button>
      <button type="button" onclick="closeSocket();" >Close</button>
    </div>
    <!-- Server responses get written here -->
    <div id="messages"></div>
    
    <!-- Script to utilise the WebSocket -->
    <script type="text/javascript">
var webSocket;
var messages = document.getElementById("messages");

function openSocket(){
    // Ensures only one connection is open at a time
    if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
        writeResponse("WebSocket is already opened.");
        return;
    }
    // Create a new instance of the websocket
    webSocket = new WebSocket("ws://localhost:9001/simple-echo");
    
    /**
     * Binds functions to the listeners for the websocket.
     */
    webSocket.onopen = function(event){
        writeResponse("CONNECTED ");
    };
    
    webSocket.onmessage = function(event){
        writeResponse("RECEIVED: " + event.data);
    };
    
    webSocket.onclose = function(event){
        writeResponse("Connection closed ");
    };
}

/**
 * Sends the value of the text input to the server
 */
function send(){
    var text = document.getElementById("messageinput").value;
    webSocket.send(text);
    writeResponse("SENT " + text);
}

function closeSocket(){
    webSocket.close();
}

function writeResponse(text){
    messages.innerHTML += "<br/>" + text;
}
    </script>
  </body>
</html>

client2.html

This file refers to 'localhost:9001'. If the browser will be running on a different host from the Lisp server, 'localhost' must be changed to the actual host. If the port used is not 9001, that value too must be edited.

<!DOCTYPE html>

<html>
  <head>
    <title>Echo Chamber</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
  </head>
  <body>
    
    <div>
      <input type="text" id="messageinput"/>
    </div>
    <div>
      <button type="button" onclick="openSocket();" >Open</button>
      <button type="button" onclick="send();" >Send</button>
      <button type="button" onclick="closeSocket();" >Close</button>
    </div>
    <!-- Server responses get written here -->
    <div id="messages"></div>
    
    <!-- Script to utilise the WebSocket -->
    <script type="text/javascript">
var webSocket;
var messages = document.getElementById("messages");
      
function openSocket(){
    // Ensures only one connection is open at a time
    if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
	writeResponse("WebSocket is already opened.");
	return;
    }
    // Create a new instance of the websocket
    webSocket = new WebSocket("ws://localhost:9001/multi-echo");
      
    /**
     * Binds functions to the listeners for the websocket.
     */
    webSocket.onopen = function(event){
	writeResponse("CONNECTED ");
    };
    
    webSocket.onmessage = function(event){
	writeResponse("RECEIVED: " + event.data);
    };
    
    webSocket.onclose = function(event){
	writeResponse("Connection closed ");
    };
}

/**
 * Sends the value of the text input to the server
 */
function send(){
    var text = document.getElementById("messageinput").value;
    webSocket.send(text);
    writeResponse("SENT " + text);
}

function closeSocket(){
    webSocket.close();
}

function writeResponse(text){
    messages.innerHTML += "<br/>" + text;
}
    </script>
  </body>
</html>
Copyright © 2023 Franz Inc., All Rights Reserved | Privacy Statement Twitter