Keeping Server and Client Separate September 8th, 2011
Patrick Stein

The problem

Whenever I write client-server applications, I run into the same problem trying to separate the code. To send a message from the server to the client, the server has to serialize that message and the client has to unserialize that message. The server doesn’t need to unserialize that message. The client doesn’t need to serialize that message.

It seems wrong to include both the serialization code and the unserialization code in both client and server when each side will only be using 1/2 of that code. On the other hand, it seems bad to keep the serialization and unserialization code in separate places. You don’t want one side serializing A+B+C and the other side trying to unserialize A+C+D+B.

One approach: data classes

Some projects deal with this situation by making every message have its own data class. You take all of the information that you want to be in the message and plop it into a data class. You then serialize the data class and send the resulting bytes. The other side unserializes the bytes into a data class and plucks the data out of the data class.

The advantage here is that you can have some metaprogram read the data class definition and generate a serializer or unserializer as needed. You’re only out-of-sync if one side hasn’t regenerated since the data class definition changed.

The disadvantage here is that I loathe data classes. If my top-level interface is going to be (send-login username password), then why can’t I just serialize straight from there without having to create a dippy data structure to hold my opcode and two strings?

Another approach: suck it up

Who cares if the client contains both the serialization and unserialization code? Heck, if you’re really all that concerned, then fmakunbound half the universe before you save-lisp-and-die.

Of course, unless you’re using data classes, you’re either going to have code in your client that references a bunch of functions and variables that only exist in your server or your client and server will be identical except for:

(defun main ()
  #+server (server-main)
  #-server (client-main))

Now, of course, your server is going to accidentally depend on OpenGL and OpenAL and SDL and a whole bunch of other -L’s it never actually calls. Meanwhile, your client is going to accidentally depend on Postmodern and Portable-Threads and a whole bunch of other Po-‘s it never actually calls.

Another approach: tangle and weave, baby

Another way that I’ve got around this is to use literate programming tools to let me write the serialiization and unserialization right next to each other in my document. Then, anyone going to change the serialize code would be immediately confronted with the unserialize code that goes with it.

The advantage here is that you can tangle the client code through an entirely separate path than the server code keeping only what you need in each.

The disadvantage here is that now both your client code and your server code have to be in the same document or both include the same sizable chunk of document. And, while there aren’t precisely name-capturing problems, trying to include the “serialize-and-send” chunk in your function in the client code still requires that you use the same variable names that were in that chunk.

How can Lisp make this better?

In Lisp, we can get the benefits of a data-definition language and data classes without needing the data classes. Here’s a snippet of the data definition for a simple client-server protocol.

;;;; protocol.lisp
(userial:make-enum-serializer :opcode (:ping :ping-ack))
(defmessage :ping     :uint32 ping-payload)
(defmessage :ping-ack :uint32 ping-payload)

I’ve declared there are two different types of messages, each with their own opcode. Now, I have macros for define-sender and define-handler that allow me to create functions which have no control over the actual serialization and unserialization. My functions can only manipulate the named message parameters (the value of ping-payload in this case) before serialization or after unserialization but cannot change the serialization or unserialization itself.

With this protocol, the client side has to handle ping messages by sending ping-ack messages. The define-sender macro takes the opcode of the message (used to identify the message fields), the name of the function to create, the argument list for the function (which may include declarations for some or all of the fields in the message), the form to use for the address to send the resulting message to, and any body needed to set fields in the packet based on the function arguments before the serialization. The define-handler macro takes the opcode of the message (again, used to identify the message fields), the name of the function to create, the argument list for the function, the form to use for the buffer to unserialize, and any body needed to act on the unserialized message fields.

;;;; client.lisp
(define-sender  :ping-ack send-ping-ack (ping-payload) *server-address*)
(define-handler :ping     handle-ping   (buffer) buffer
  (send-ping-ack ping-payload))

The server side has a bit more work to do because it’s going to generate the sequence numbers and track the round-trip ping times.

;;;; server.lisp

(defvar *last-ping-payload* 0)
(defvar *last-ping-time*    0)

(define-sender :ping send-ping (who) (get-address-of who)
  (incf *last-ping-payload*)
  (setf *last-ping-time*    (get-internal-real-time)
        ping-payload        *last-ping-payload*))

(define-handler :ping-ack handle-ping-ack (who buffer) buffer
  (when (= ping-payload *last-ping-payload*)
    (update-ping-time who (- (get-internal-real-time) *last-ping-time*))))

Problems with the above

It feels strange to leave compile-time artifacts like the names and types of the message fields in the code after I’ve generated the functions that I’m actually going to use. But, I guess that’s just part of Lisp development. You can’t (easily) unload a package. I can makunbound a bunch of stuff after I’m loaded if I don’t want it to be convenient to modify senders or handlers at run-time.

There is intentional name-capture going on. The names of the message fields become names in the handlers. The biggest problem with this is that the defmessage calls really have to be in the same namespace as the define-sender and define-handler calls.

I still have some work to do on my macros to support &key and &optional and &aux and &rest arguments properly. I will post those macros once I’ve worked out those kinks.

Anyone care to share how they’ve tackled client-server separation before?

Blackthorn 3D — Lisp game engine using USerial June 20th, 2011
Patrick Stein

Elliott Slaughter announced Blackthorn 3D, yesterday. Blackthorn 3D is a game engine crafted in Lisp using LispbuilderSDL and cl-opengl for graphics and usocket and userial for network play.

USerial — v0.8.2011.06.02 June 2nd, 2011
Patrick Stein

I am releasing a new version of the USerial library. New in this version:

  • Fix (make-int-serializer) to be big-endian 2’s complement
  • Add :symbol and :keyword serializers
  • Add (make-vector-serializer)
  • Add (make-key-slot-serializer) and (make-key-accessor-serializer)
  • Add (define-serializing-funcall)

Obtaining

USerial — v0.7.2011.05.24 May 24th, 2011
Patrick Stein

I am releasing a new version of my USerial library. This version cleans up many messes from earlier releases. Unfortunately, in that process, it breaks compatibility with earlier releases.

Obtaining

Getting the USerial library:

Differences

The differences between this version and earlier versions of this library include:

  • Use of ContextL layered functions instead of CLOS methods
  • Elimination of :buffer parameter in favor of using the *buffer* special variable
  • Cleaning up macros which no longer required the :buffer parameter
  • Serializers for arbitrarily large integers and unsigned integers
  • Serializer for raw sequence of bytes
  • New make-list-serializer macro

By using ContextL layered functions, one has the ability to define a serializer and/or unserializer in a particular ContextL layer. This can be used to create new versions of the serializer without losing the ability to use the older version when required.

In the process, I have created macros to assist in creating completely custom serializers. This both streamlines their definition and should allow any future modifications to the USerial library to fly under the radar. Code that before looked like this:

(defmethod serialize ((key (eql :foo)) (value foo-struct)
                      &key (buffer userial:*buffer*) &allow-other-keys)
  ... some code ...
  buffer)

(defmethod unserialize ((key (eql :foo))
                        &key (buffer userial:*buffer*) &allow-other-keys)
  (values (progn ... some code ...)
          buffer))

Should now look like this:

(define-serializer (:foo (value foo-struct))
  ... some code ...)

(define-unserializer (:foo)
  ... some code ...)

And, when you find you need to add a new version of your :foo serializer but you don’t want to lose the old one, you can add:

(contextl:deflayer new-version)

(define-serializer (:foo (value foo-struct) :layer new-version)
  ... some new code ...)

Without the :buffer parameter everywhere, code that used to look like this:

(serialize* (:string aa :uint8 bb) :buffer buf)
(buffer-rewind :buffer buf)
(unserialize-slots* (:string name :uint8 age) object :buffer buf)

Should now look like this:

(with-buffer buf
  (serialize* :string aa :uint8 bb)
  (buffer-rewind)
  (unserialize-slots* object :string name :uint age))

There are now :int and :uint serializers that encode arbitrarily large integers and unsigned integers, respectively. There is also a serializer that copies a sequence of bytes as is without any prefix or suffix. To unserialize, you either have to provide a buffer of the appropriate length with the :output parameter or provide appropriate :start and :end keywords.

(serialize :raw-bytes uchar-array &key (start 0)
                                       (end (length uchar-array)))
(unserialize :raw-bytes &key output
                             (start 0)
                             (end (length output)))

And, if you have a serialize/unserialize pair for type :foo you can use the make-list-serializer macro to create a serialize/unserialize pair for a list of items that can be serialized with the :foo serializer.

(make-list-serializer :list-of-uint8 :uint8)
(serialize :list-of-int8 '(0 1 1 2 3 5 8 13 21 34 55 89))

At the USerial home page, you can find more complete documentation.

USerial — v0.7.2011.05.24 May 24th, 2011
Patrick Stein

I am releasing a new version of my USerial library. This version cleans up many messes from earlier releases. Unfortunately, in that process, it breaks compatibility with earlier releases.

Obtaining

Getting the USerial library:

Differences

The differences between this version and earlier versions of this library include:

  • Use of ContextL layered functions instead of CLOS methods
  • Elimination of :buffer parameter in favor of using the *buffer* special variable
  • Cleaning up macros which no longer required the :buffer parameter
  • Serializers for arbitrarily large integers and unsigned integers
  • Serializer for raw sequence of bytes
  • New make-list-serializer macro

By using ContextL layered functions, one has the ability to define a serializer and/or unserializer in a particular ContextL layer. This can be used to create new versions of the serializer without losing the ability to use the older version when required.

In the process, I have created macros to assist in creating completely custom serializers. This both streamlines their definition and should allow any future modifications to the USerial library to fly under the radar. Code that before looked like this:

(defmethod serialize ((key (eql :foo)) (value foo-struct)
                      &key (buffer userial:*buffer*) &allow-other-keys)
  ... some code ...
  buffer)

(defmethod unserialize ((key (eql :foo))
                        &key (buffer userial:*buffer*) &allow-other-keys)
  (values (progn ... some code ...)
          buffer))

Should now look like this:

(define-serializer (:foo (value foo-struct))
  ... some code ...)

(define-unserializer (:foo)
  ... some code ...)

And, when you find you need to add a new version of your :foo serializer but you don’t want to lose the old one, you can add:

(contextl:deflayer new-version)

(define-serializer (:foo (value foo-struct) :layer new-version)
  ... some new code ...)

Without the :buffer parameter everywhere, code that used to look like this:

(serialize* (:string aa :uint8 bb) :buffer buf)
(buffer-rewind :buffer buf)
(unserialize-slots* (:string name :uint8 age) object :buffer buf)

Should now look like this:

(with-buffer buf
  (serialize* :string aa :uint8 bb)
  (buffer-rewind)
  (unserialize-slots* object :string name :uint age))

There are now :int and :uint serializers that encode arbitrarily large integers and unsigned integers, respectively. There is also a serializer that copies a sequence of bytes as is without any prefix or suffix. To unserialize, you either have to provide a buffer of the appropriate length with the :output parameter or provide appropriate :start and :end keywords.

(serialize :raw-bytes uchar-array &key (start 0)
                                       (end (length uchar-array)))
(unserialize :raw-bytes &key output
                             (start 0)
                             (end (length output)))

And, if you have a serialize/unserialize pair for type :foo you can use the make-list-serializer macro to create a serialize/unserialize pair for a list of items that can be serialized with the :foo serializer.

(make-list-serializer :list-of-uint8 :uint8)
(serialize :list-of-int8 '(0 1 1 2 3 5 8 13 21 34 55 89))

At the USerial home page, you can find more complete documentation.

Updates In Email

Email:

l