Skip to content

Latest commit

 

History

History
158 lines (137 loc) · 8.38 KB

ws-relay.rst

File metadata and controls

158 lines (137 loc) · 8.38 KB

WebSocket Relay

Due to cross-origin request restrictions in modern web browsers, initial communication from the Javascript code running in the browser must go back to the web server hosting the Javascript, or to other explicitly named servers provided when the Javascript was requested. So, to allow game clients to find one another, I built a very simple "relay" in Python which acts as a WebSocket server and relays messages between clients. It provides the following capabilities:

  • Assigns a unique ID to new clients when they join the game
  • Notify existing game clients whenever other clients join or quit
  • Allow clients to send arbitrary messages to either all other clients (simulating multicast) or to specific other clients (used for sending WebRTC signaling messages)

A single WebSocket relay instance can do the above for an arbitrary number of parallel game instances. Games are uniquely identified by the URL used to launch them, and this can include an optional query string added to the end to create multiple game instances. For example, all of the following are valid URLs which launch the game, but each of these will have their own independent set of players:

https://yourserver.example.com/spacewar
https://yourserver.example.com/spacewar?game=1
https://yourserver.example.com/spacewar?game=2

The WebSocket relay could be used for more than just Spacewar. All that is required is that the clients of it construct a unique URL which begins with /ws-relay on the host it is installed on. This is done automatically by the ws-relay.js package I provide here. You can pass in something like the document URL of the main Javascript file using the relay and it will add the /ws-relay to the beginning of the path and convert https:// to wss:// (the secure WebSocket URL scheme) to construct a URL suitable for contacting the relay.

The messages sent to the WebSocket relay are a very simple text-based protocol of the form:

<id> <msgtype> [<optional-msg-data>]

A few message types are generated by the relay itself, and all others are left to clients of the relay to define.

Specifically, the relay generates a msgtype self message whenever a new client first connects to it, telling the client what their unique ID is. IDs are currently just integer values which count up from 1 whenever a relay is started. So, the first client to connect to a relay will get back 1 self as a message from the relay when they first connect. The second client will get back 2 self, and so forth.

Whenever a new client connects, the relay will also send out join messages to all other clients, informing them of the ID of the client which just joined and join messages to the new client for the other clients already present there. So, in the example above, client #1 would see a 2 join message and client #2 would see a 1 join message if they both connected to the same WebSocket relay URL at the same time. If a third client connected, client #1 & #2 would both see a 3 join message, and client #3 would see two messages (1 join and 2 join).

When a client disconnects, the relay automatically sends a quit message to all other connected clients. So, if client #3 leaves, both client #1 and #2 will receive a 3 quit message.

In addition to these control messages, clients of the relay can send messages to one another using any message type they want. The messages sent follow a similar format, but the <id> field in messages sent to the relay is client ID to forward the message to, or * to forward a message to all other connected clients. So, if client #1 sent 2 hello, client #2 would receive 1 hello, indicating that client #1 sent a hello to it. If client #1 sent * hello with clients #2 and #3 connected, both of them would receive a 1 hello message.

Here's an illustration of the messages in table form, with -> indicating a client sending a message and <- indicating a client receiving one:

Client 1 Client 2 Client 3
Client 1 joins.    
<- 1 self    
  Client 2 joins.  
  <- 2 self  
  <- 1 join  
<- 2 join    
    Client 3 joins.
    <- 3 self
    <- 1 join
    <- 2 join
<- 3 join <- 3 join  
-> 2 hello    
  <- 1 hello  
-> * hello    
  <- 1 hello <- 1 hello
    Client 3 quits.
<- 3 quit <- 3 quit  
  Client 2 quits.  
<- 2 quit    
Client 1 quits.    

After the message type, clients are free to add arbitrary additional data to the messages they send and it will be forwarded along with the ID and message type. So, for instance, Spacewar defined a report message which contains game state encoded in JSON format, and it can multicast that state to all other clients by sending a message of the form:

* report <JSON-report-data>

Spacewar uses this relay for sending WebRTC signaling messages as well. In that case, it defines messages of type offer and answer, each with SDP format message data. Whenever a new client connects, existing clients attempt to reach out to the new client through the relay to set up a WebRTC data channel, and that will get used for future report messages if it is successfully opened. Otherwise, the clients fall back to using the WebSocket relay for these reports.

The Python "websockets" package makes it very easy to test out the relay and even watch it in actual use with Spacewar, since report messages are relayed to all clients joining the relay. You just have to feed your "/ws-relay" URL to the websockets package and it will enter an interactive CLI where you can see incoming messages and even send your own outgoing messages. This is done with a command like:

python -m websockets wss://yourserver.example.com/ws-relay/spacewar

Note that the spacewar URL here has /ws-relay added to the beginning of the path, and has the URL scheme of https:// replaced with wss://. You can also see offer messages with SDP information related to WebRTC signaling, but WebRTC connections will not be established since there's no client associated with the Python websockets client capable of setting up a WebRTC peer connection and replying with an answer message with the corresponding SDP.