MusicDB Websocket Protocol

There are lots of classes involved to implement the websocket server. The image below shows the whole class diagram. Only the more important methods are included in the diragram. For a full list of all methods see the related documentation of the class. As library I use Autobahn. The class WebSocketServerProtocol and WebSocketServerFactory are base classes from that library. The MusicDBWebSocketInterface implements the API for the JavaScript clients and is documented under MusicDB Websocket API.

In the following pictures the blue elements are related to the musicdb.lib.ws.websocket.WebSocket class. This class abstracts the internals of the Autobahn module and provides a low level communication websocket interface. The red elements to musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface that implements the high level API the web clients uses.

The class diagram below shows the Autobahn classes WebSocketServerProtocol and WebSocketServerFactory and how they are related to the MusicDB classes for the websocket communication. In this diagram, only the most relavant methods and attributes are mentioned.

digraph hierarchy { size="8,8" node[shape=record,style=filled,fillcolor=gray95] edge[dir=back, arrowtail=empty] wssp [label = "{WebSocketServerProtocol||}"] ws [label = "{WebSocket||+ SendPacket()\l+ BroadcastPacket()\l/- onOpen()\l/- onClose()\l/- onMessage()\l}", color=blue] mdbwsi [label = "{MusicDBWebSocketInterface||# onWSConnect()\l# onWSDisconnect()\l# onCall()\l- onStreamEvent()\l- onQueueEvent\l}", color=red] mdbwsp [label = "{MusicDBWebSocketProtoctol||}"] wssf [label = "{WebSocketServerFactory||}"] mdbwsf [label = "{MusicDBWebSocketFactory|- clients\l|+ AddToBroadcast()\l+ RemoveFromBroadcast()\l+ BroadcastPacket()\l+ CloseConnections()\l}"] mdbwss [label = "{MusicDBWebSocketServer|- factory\l- factory.protocol\l|+ Setup()\l+ Start()\l+ Stop()\l+ HandleEvents()\l}"] wssp -> ws ws -> mdbwsp mdbwsi -> mdbwsp wssf -> mdbwsf mdbwsp -> mdbwsf [style=dotted, arrowtail=open] mdbwsf -> mdbwss [arrowtail=open] }

The relation between the Socket Server, Socket Factory and Socket Protocol is a bit insane. The following code may make it a bit more understandable what the Autobahn library wants me to do:

class MusicDBWebSocketServer(object):
   def __init__(self):
      self.factory          = MusicDBWebSocketFactory()
      self.factory.protocol = MusicDBWebSocketProtocol

The methods onWSDisconnect and onWSConnect are called from musicdb.lib.ws.websocket.WebSocket and implemented in musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface. They are used to register the callback functions for the musicdb.mdbapi.stream and musicdb.mdbapi.songqueue module.

The following state machine shows how a connections is processed. The musicdb.lib.ws.websocket.WebSocket.SendPacket() and musicdb.lib.ws.websocket.WebSocket.BroadcastPacket() method can be called independent from the clients requests. Just be sure the onWSConnect method was called before. Otherwise the mechanics behind won’t work.

digraph finite_state_machine { size="8,12" node [shape = doublecircle, color=black, fontsize=10, label="Listen\non port" ] Listen_on_port; node [shape = circle, color=red, fontsize=10, label="__init__" ] CB__init__; node [shape = circle, color=blue, fontsize=10, label="__init__" ] __init__; node [shape = circle, color=black, fontsize=10, label="Connecting" ] Connecting; node [shape = circle, color=blue, fontsize=10, label="onOpen" ] onOpen; node [shape = circle, color=red, fontsize=10, label="onWSConnect" ] onWSConnect; node [shape = circle, color=blue, fontsize=10, label="onClose" ] onClose; node [shape = circle, color=red, fontsize=10, label="onWSDisconnect" ] onWSDisconnect; node [shape = circle, color=blue, fontsize=10, label="onOpenHandschakeTimeout" ] onOpenHandshakeTimeout; // node [shape = circle, color=blue, fontsize=10, label="onCloseHandschakeTimeout"] onCloseHandshakeTimeout; node [shape = circle, color=black, fontsize=10, label="Autobahn\nMagic" ] Active; node [shape = circle, color=blue, fontsize=10, label="onMessage" ] onMessage; node [shape = circle, color=red, fontsize=10, label="onCall" ] onCall; node [shape = circle, color=blue, fontsize=10, label="SendPacket" ] SendPacket; node [shape = circle, color=blue, fontsize=10, label="BroadcastPacket" ] BroadcastPacket; Listen_on_port -> __init__ [ label = "access port"]; __init__ -> CB__init__ [ label = "create derived\nclass object" ]; CB__init__ -> __init__; __init__ -> Connecting; Connecting -> onOpen [ label = "open Handshake" ]; Connecting -> onOpenHandshakeTimeout [label = "timeout" ] ; onOpenHandshakeTimeout -> onClose [ label = "close" ]; onOpen -> onWSConnect [ label = "initialize handler" ]; onWSConnect -> onOpen; onOpen -> Active [ label = "successful handshake" ]; Active -> onMessage [ label = "got data\nfrom client" ]; onMessage -> onCall; onCall -> Active [ label = "no response" ]; onCall -> SendPacket [ label = "response" ]; onCall -> BroadcastPacket [ label = "response to all" ]; SendPacket -> Active [ label = "send data\nto client" ]; BroadcastPacket -> Active [ label = "send data\nto all clients" ]; Active -> onClose [ label = "close connection" ]; onClose -> onWSDisconnect [ label = "clean up\nwhen connected" ]; onWSDisconnect -> onClose; onClose -> Listen_on_port; {rank = same; __init__; CB__init__; } {rank = same; onOpen; onWSConnect; } {rank = same; SendPacket; BroadcastPacket; } {rank = same; onClose; onWSDisconnect;} }

Websocket Server

This module provides the server infrastructure of the server.

class musicdb.lib.ws.server.MusicDBWebSocketProtocol[source]

Derived from musicdb.lib.ws.websocket.WebSocket and musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface. Connecting low level implementation with the high level implementation of MusicDBs WebSocket Interface.

This object gets always instatiated when a new client connects to the server. It does not matter if this is a websocket client or not!. To initialize code that needs a working websocket connection, use the onWSConnect callback interface.

You can and should check changes in musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface by starting the server and accessing it via nmap -p $MDBServerPort localhost or by accessing the server via https from your browser. Both are not valid websocket connections. The server should create a new connection but will not call the onWSConnect method.

class musicdb.lib.ws.server.MusicDBWebSocketServer[source]

This class implements the whole server infrastructure. Outside of the MusicDB WebSocket Interface abstraction this is the only class to use.

HandleEvents()[source]

This method handles the events inside the Autobahn internal event loop. It should be called in a loop as long as the server shall work.

Returns

Nothing

Example

while True:
    server.HandleEvents()

    if shutdown:
        server.Stop()
        break

    time.sleep(.1)  # Avoid high CPU load
Setup(address, port, cert, key)[source]

This method does the server setup.

It configures a TLS encrypted connection and creates the event loop.

Parameters
  • address (str) – address to bind to

  • port (int) – port to bind to

  • cert (str) – Path to an SSL certificate

  • key (str) – Path to the key for the certificate

Returns

True on success, otherwise False

Start()[source]

This method starts the server.

Returns

True on success, otherwise False

Example

server = MusicDBWebSocketServer()

retval = tlswsserver.Setup("127.0.0.1", 9000, "/etc/ssl/test/test.cert", "/etc/ssl/test/test.key")
if retval == False:
    print("Setup for TLS-Server failed!")
    exit(1)

retval = tlswsserver.Start()
if retval == False:
    print("Starting TLS-Server failed!")
    exit(1)
Stop()[source]

This methos halts the server. It closes the connection and the event loop.

Returns

True

Websocket Protocol

This module provides low level classes for the WebSocket communication to the WebUI.

Most interesting are the following methods:

class musicdb.lib.ws.websocket.MusicDBWebSocketFactory[source]

Derived from WebSocketServerFactory. Implements some basic configuration and a broadcasting infrastructure to send packets to all connected clients.

AddToBroadcast(client)[source]

This method registers a new client. The client must be of the low level class musicdb.lib.ws.websocket.WebSocket or a derived class.

Parameters

client – WebSocket connection handler

Returns

Nothing

BroadcastPacket(packet)[source]

This method broadcasts a packet to all connected clients.

The method value in the packet gets forced to "broadcast".

Parameters

packet – A packet dictionary that shall be send to all clients

Returns

Nothing

CloseConnections()[source]

This method initiates a closing handshake to all connections with error code 1000 and reason "Server shutdown"

Returns

Nothing

RemoveFromBroadcast(client)[source]

Removes a client connection handler from the broadcast-list.

Parameters

client – WebSocket connection handler

Returns

Nothing

class musicdb.lib.ws.websocket.WebSocket[source]

Derived from WebSocketServerProtocol. This class provides the low level WebSocket interface for MusicDBs WebSocket Interface. It manages the packet handling and handles callback routines.

BeautifyValues(packet, affectkey, old, new)[source]

This method can be used to beautify values inside nested dictionaries. Use-cases are replacing Division Slashes by normal slashes or hyphens by n-dashes in song names.

It recursively scans through nested dictionaries and lists (packet) to find a specific key (affectkey). The content behind that key, in case it is of type string, gets scanned for old substring. This substring then gets replaced by the substirng new

For easy to read code, the method returns the reference to packet. Keep in mind that the content of packet will be changed even if the return value gets not assigned.

The core algorithm was copied from sotapme @ stackoverflow <https://stackoverflow.com/questions/14882138/replace-value-in-json-file-for-key-which-can-be-nested-by-n-levels/14882688>:

Parameters
  • packet – A nested dictionary/list

  • affectkey (str) – A key to search for

  • old (str) – A sub string to search for, inside the value referenced by affectkey

  • new (str) – A sub string to replace old

Returns

A reference to packet

Raises

TypeError – If affectkey, old or new are not of type string

Example

Replace divison slash (U+2215) by slash and hyphen by n-dash

packet  = self.BeautifyValues(packet, "name", "∕",   "/");
packet  = self.BeautifyValues(packet, "name", " - ", " – ");
BroadcastPacket(packet)[source]

This method works line SendPacket() only that the packet gets send to all clients. It uses the broadcasting method from musicdb.lib.ws.websocket.MusicDBWebSocketFactory.BroadcastPacket() This method should be used with care because it can cause high traffic.

Parameters

packet – A packet dictionary that will be send to all clients

Returns

Nothing

SendPacket(packet)[source]

This method sends a packet via to the connected client. The format of the packet is described in the MusicDB Websocket API documentation.

The abstract process of sending the packet is shown in the following code:

#packet  = self.BeautifyValues(packet, "name", "∕", "/");
rawdata = json.dumps(packet)        # Python Dict to JSON string
rawdata = rawdata.encode("utf-8")   # Encode as UTF-8
self.sendMessage(rawdata, False)    # isBinary = False

There is a race condition allowing calling SendPacket before the connection process is complete. To prevent problems, this method returns False if the connection is not established yet. Further more the state of the connection gets checked. If the connection state is not OPEN, False gets returned, too.

All values behind the name key of the packet get beautified. It is assumed that those values are used to display information to the user.

Parameters

packet – A packet dictionary that will be send to the client

Returns

True on success, otherwise False

Raises
  • TypeError – If packet is not of type dict

  • RuntimeError – If Autobahns WebSocketServerProtocol class did not set an internal state. Should never happen, but happed once :)

Example

Response to an album request by a client.

response    = {}
response["method"]      = "response"
response["fncname"]     = "GetAlbums"
response["fncsig"]      = request["fncsig"]
response["arguments"]   = albums
response["pass"]        = request["pass"]
self.SendPacket(response)
onClose(wasClean, code, reason)[source]

This method gets called by WebSocketServerProtocol implementation. It removes this connection to the broadcasting infrastructure of musicdb.lib.ws.websocket.MusicDBWebSocketFactory.

This method calls an onWSDisconnect(wasClean:bool, closecode:int, closereason:str) Method that must be implemented by the programmer who uses this class. The onWSDisconnect method gets only called when there was a successful connection before!

onCloseHandshakeTimeout()[source]

We expected the peer to respond to us initiating a close handshake. It didn’t respond (in time self.closeHandshakeTimeout) with a close response frame though. So we drop the connection, but set self.wasClean = False.

onConnect(request)[source]

Just prints the IP address of the connecting client. See for details. ConnectionRequest in the Autobahn documentation for details.

Returns

Nothing

onMessage(payload, isBinary)[source]

This method gets called by WebSocketServerProtocol implementation. It handles the decoding of a received packet and provides the packet format as described in the MusicDB Websocket API documentation.

The decoding process can be abstracted as listed in the following code:

# Check if payload is text
if isBinary == True:
    return None

# Create packet
rawdata = payload.decode("utf-8")
packet  = json.loads(rawdata)

# Provide packet to high level interface
self.onCall(packet)
Parameters
  • payload – The payload of a WebSocket message received from a client

  • isBinaryTrue if binary data got received, False`` when text. This implementation allows only text.

Returns

None

onOpen()[source]

This method gets called by WebSocketServerProtocol implementation. It registers this connection to the broadcasting infrastructure of musicdb.lib.ws.websocket.MusicDBWebSocketFactory.

This method calls an onWSConnect() Method that must be implemented by the programmer who uses this class.

onOpenHandshakeTimeout()[source]

We expected the peer to complete the opening handshake with to us. It didn’t do so (in time self.openHandshakeTimeout). So we drop the connection, but set self.wasClean = False.