A SCLOrkWire class creates a reliable, two-way, connection-oriented OSC network connection between two computers using OSC over UDP as the underlying transport mechanism.
The SCLOrkWire protocol is designed for relatively infreqeunt (< ~10 per second) unicast messages between computers on a relatively relaible LAN. It makes no distinction betwen computers, the connection is symmetric. Either party can initiate a connection, and either party can terminate one. SCLOrkWire offers an onMessageReceived function to users to bind a function to incoming messages, and provides a sendMsg method just like a NetAddr.
Because UDP is a *datagram* protocol it may be susceptible to packet loss, either due to underlying detection of packet corruption, which causes packets to be dropped, or for packets being lost during sending. Furthermore, UDP is connectionless, meaning that datagrams are sent one-way and have no built-in facility for automatic two-way communication. Because packet receipt is not awknowledged, the sender has no way of understanding if the recipient has actually received any of the sent packets. Lastly, because each datagram is sent individually, there's no order of receipt guarantee on the recipient either, meaning that messages sent can arrive in any order (or not at all!) on the recipient side.
Of course, TCP is usually used to provide connection-oriented network communications with drop detection and in-order arrival guarantees. While neither TCP or UDP offers any latency guarantees, UDP typically offers much better latencies due to the lack of underlying infrastructure around packet validation and in-order arrival. While TCP is the obvious right choice between the two for reliable data transmission over the open Internet, on a closed, relatively quiet LAN environment with high reliability in packet transmission a lighter-weight reliability protocol based on UDP can offer the best of both worlds.
Lastly, by keeping the protocal based in OSC SCLOrkWire maintains all of the utility of OSC in the SuperCollider and broader OSC ecosystem.
In this section we discuss the message flow between clients in different scenarios, as well as discuss fail states and recovery scenarios.
SCLOrkWires were designed with the assumption that they each are the only instance of the class listening on a given port. There may be other OSC messages incoming on that port but keeping a wire unique on a port simplified the communication protocol design. This design does present a challenge in determining in advance what port a remote client should connect to on a host machine. To solve this problem SCLOrkWire supports a class method SCLOrkWire: bind which can listen for /wireKnock messages on a predetermined port. The wire code responds by allocating a unique port for the wire endpoint on the host machine, and then initiating connection back to the knocking address at the port supplied by the knocking machine at knock time.
This facilitates a client-server type environment, where clients will send a /wireKnock to the server at a predetermined, constant port, and then expect that the server will initiate a connection request back to the client at the port supplied in the knock.
Sender | Path | Message | Discussion |
client | /wireKnock | receivePort | Server will initiate connection back to client on receivePort |
server | /wireConnectRequest | returnPort, responderId | See Connection protocol below |
... | ... | ... | .... |
If the /wireKnock request is dropped, the client will retransmit after some timeout of not seeing a connection initiation request come in from the server. See Connection for discussion of the failure recovery modes in connection initiation.
While the SCLOrkWire is designed to be bidirectional and either client and send or receive messages from the other, in connection the client takes on one of two roles, either connection initiator or responder. In the case of a knock, which is a request for a port assignment by a client, the server role will take on the role of connection initator, responding to the client knock.
To facilitate higher-level organization of SCLOrkWires, on connection initiation each client can provide a unique identifier to the other. The clients will then include their respective provided ids in all subsequent communications with each other. Incoming messages that do not have the correct peer-supplied id number associated with them will be rejected, but otherwise the SCLOrkWire protocol doesn't use these ids for any particular purpose.
The overall connection protocol is inspired by the Three-Way Handshake connection from TCP.
Sender | Path | Message | Discussion |
initiator | /wireConnectRequest | returnPort, responderId | Responder will include responderId in all responses. |
responder | /wireConnectAccept | responderId, initiatorId | Responder provides a unique Id to the initiator. |
initiator | /wireConnectConfirm | initiatorId | Upon receipt, responder will consider connection complete. |
If the /wireConnectRequest packet is dropped, the initiator will time out receiving the /wireConnectAccept response from the responder, and so will resend the request message.
If the responder's packet /wireConnectAccept is dropped, it will not receive the /wireConnectConfirm response from the initator, and so will resend the accept message after timeout. It might also receive an additonal /wireConnectRequest message from the initiator, as it times out waiting for the accept message, which will also cause the responder to retransmit the accept message.
Lasty, if the /wireConnectConfirm packet is dropped, the responder will time out waiting for it, and will resend the /wireConnectAccept message, causing the client to resend the confirm.
Each message sent has a sender-maintained serial number that is attached, along with the recipient-assigned senderId, to the message payload. While the serial numbers increase monotonically SCLOrkWire has no concept of a cumulative acknowledgment, each message must be acknowledged individually. In order to enforce in-order arrival of messages, out-of-order messages will be stored and not delivered to the client code until after the missing in-order messages are received.
Sender | Path | Message | Discussion |
sender | /wireSend | senderId, serialNumber, ..message.. | The remaining arguments are the OSC message as originally formed. |
recipient | /wireAck | recipientId, serialNumber | The serial number must be identical to the one provided in /wireSend. |
If the /wireSend packet is dropped, the sender will never receive awknowledgment, and so will resend the data after timeout. The same situation will occur if the awknowledgment packet is dropped. In both cases the recipient will respond with a /wireAck packet.
For out-of-order packets, if the recipient recieves a packet with a later serial number than the next one it is expecting, the recipient will not send a /wireAck message for those until it receives the missing in-order packet. It will buffer those messages, so when the in-order packet does arrive, the recipient will send a series of /wireAck messages in-order.
TCP has some advanced sequential acknowledgement logic here, so there's ample room for subsequent optimization work to lower retransmission rates if the strategy outlined here proves too naive.
Normal disconnection can be initiated by either client, and follows the same roles as connection, those of disconnection initiator and responder.
Sender | Path | Message | Discussion |
initiator | /wireDisconnect | senderId | Normal disconnection initiated. |
responder | /wireDisconnectConfirm | responderId | Serves as acknowledgment of disconnection. |
If the /wireDisconnect packet is dropped, the initiator not receive the completion message, and so will time out and retransmit the disconnect. If the /wireDisconnectConfirm packet is dropped, the initiator will once again retransmit the initial disconnect message, causing the recipient to retransmit the confirmation.