Device Communication
- Each device needs to know if other devices are online before doing any operation
Namespace: /device
Payload Type Definition
enum Feature {
ClipboardSync = "clipboard_sync",
MouseShare = "mouse_share",
FileTransfer = "file_transfer",
}
type WebRTCInfo {
// TODO
}
type DeviceConnectPayload = {
features: Array<Feature>;
user_id: number;
device_id: number;
device_name: string;
utc_timestamp: Date;
socket_id?: string;
sync_socket_id?: string;
file_transfer_socket_id?: string;
// webrtc_info?: WebRTCInfo
};
type DeviceDisconnectPayload = {
socket_id: string; // this is enough for other devices to find the rest of the info
};
type DeviceUpdatePayload = {
user_id: number,
device_id: number,
device_name: number,
features: Array<Feature>
sync_socket_id?: string;
file_transfer_socket_id?: string;
// webrtc_info: WebRTCInfo // for example, make a new connection
}
Events
connectconnectis a built-in event of socket-io, everything happens when a device first connect to server
- Client-side
connect- Client listens on
connect. When successfully connected with server on namespace/device, client will also need to connect to/clipboard_syncand/mouse_sharenamespaces if the features are enabled - We will talk about the
connectevent later in the corresponding namespaces - Note: TODO
- Client listens on
device_connect- Emitted after
connect - When a device gets online, client emit
device_connectto server withDeviceConnectPayload - Server join socket to room
user-room/<user_id>of namespace/device - Server broadcast event
device_connectto roomuser-room/<user_id>(namespace/device) with receivedDeviceConnectPayload- Note: the broadcast event should include
socket_id(add it if not added initially), so other devices can associate socket id with device (sometimes only socket id is available)
- Note: the broadcast event should include
- Friend Status
- When a user device gets online, user's friends should all be notified so that they know the user is ready for communication
- When a device gets online, not only its fellow devices (same owner) are notified, all devices that belong to the friends of the owner should also be notified. This is why we have the
user_idfield inDeviceConnectPayload - This is done by querying all friends of the current user and broadcast the
device_connectevent to all of their user rooms in the/devicenamespace.
- Emitted after
device_disconnect- Server listens on
disconnectevent, when a device disconnects, broadcast to all 3 rooms (in different namespaces) mentioned above with eventdevice_disconnect, and payloadDeviceDisconnectPayload - When other devices receives the event, they find the device list stored locally, find the device with the socket_id, and set the device status to offline (should reflect in UI).
- Server listens on
device_update- When a device enables or disables a feature,
DeviceUpdatePayloadis send to server, and broadcast to all devices. WebRTC connects can be established or dropped depending on this update.
- When a device enables or disables a feature,
Note: From now on, we will call room id user-room/<user_id> "user room" for convenience.
Workflow
In the sequence diagram below, I illustrate the workflow using 2 devices, when I say broadcast I only draw one arrow to one device, but it's actually broadcasting to all devices in room
Now, every device knows the status of every other device.
Friends Status
We want to have a QQ-like friend status feature, so that we know whether our friends are ready to communicate.
- When a device connects to SocketIO Server, with
device_connectevent andDeviceConnectPayload - Server should find all of the user's friends, find their user rooms (
user-room/<user_id>)- If a user room exists, broadcast the
Namespace: /file_transfer
Given that all devices know the status (online or not) of all other devices, choose a file and the target device.
Payload Type Definition
type FileTransferRequestPayload = {
webrtc: {
info: string,
},
file: {
filename: string.
size: number
},
device: {
socket_id: string,
id: number,
platform: PlatformEnum // this DeviceEnum should be defined as a common varaible with all platforms
},
target: {
socket_id: string,
device_id: number
}
}
type FileTransferResponsePayload = {
accept: boolean,
message: string,
webrtc: {
info: string
},
device: {
socket_id: string,
id: number,
platform: PlatformEnum // this DeviceEnum should be defined as a common varaible with all platforms
},
source: {
socket_id: string,
device_id: number
}
}
Event
file_transfer_request- Create WebRTC connection and emit
file_transfer_requestto server withFileTransferRequestPayload - Server on
file_transfer_requestevent: emitfile_transfer_requestevent to target based on socket id specified in payload
- Create WebRTC connection and emit
file_transfer_response- When target device get
file_transfer_request, respond withfile_transfer_responseevent - Respond with
FileTransferResponsePayload - If file too large for target device platform (e.g. web and file size is 2G), or for some other reason target device canont do file transfer, set
accepttofalse- Sample reasons
- Storage not enough
- Device is on cellular data and file size exceed limit set for cellular data
- Sample reasons
- When server gets
file_transfer_responseevent, server will emitfile_transfer_responseto source device by the socket id included inFileTransferResponsePayload
- When target device get
Problem: Different Socket ID from Different Namespaces
In /device namespace, devices already exchanged socket id with each other, but notice that socket id for different namespaces are different. The /device socket id cannot be used in /file-transfer namespace. In the WebRTC version, each 2 devices have to be able to reach each other directly using socket id in the /file-transfer namespace.
What are the solutions?
One solution is simply repeat the "greeting" (share socket id, features, ...) in namespace /device in /file-transfernamespace, but doesn't this sound stupid?
The purpose of /device namespace is to get devices to know each other and keep all devices updated with other devices' pubic information. So why not use it as a central namespace to also share /file-transfer namespace socket io?
Solution: Add an optional file-transfer_socket_id field to DeviceConnectPayload and DeviceUpdatePayload.
When a device connects to namespace /file-transfer, it can emit event device_update in /device namespace to update its /file-transfer socket id. This also helps other devices to know wheter one device is ready to transfer.
Once everyone knows everyone else's /file-transfer socket id, the WebRTC connection can proceed.
Workflow
Namespace: /sync
Sync namespace will be responsible for syncing both clipboard and mouse sharing. The original plan is to use 2 namespaces and 2 separate groups of WebRTC connections. If not combined, the number of WebRTC connections will double, which is stupid design.
Since devices can enable one or both features, we need 1 user rooms for each feature.
- Room
clipboard_sync/user_room/<user_id> - Room
mouse_share/user_room/<user_id>
Payload Type Definition
Current payload design is just a draft, we have to think more carefully what to include in payload so every device knows whether it has the "focus"
type ConnectionPayload = {
features: Array<Feature>; // Feature is a enum defined earlier
};
enum UpdateType {
Clipboard = "clipboard",
MouseMove = "mouse_move",
KeyboardMove = "keyboard_move",
}
type MouseMovePayload = {
x: number;
y: number;
};
type KeyboardMovePayload = {
key: string;
};
enum ClipboardDataType {
Text = "text",
Image = "image",
}
type NewClipboardDataPayload = {
type: ClipboardDataType;
value: string;
uuid: string; // used to avoid duplicate
utc_time: Date;
};
type UpdatePayload = {
type: UpdateType;
device_id: number;
value: MouseMovePayload | KeyboardMovePayload | NewClipboardDataPayload;
};
SocketIO Version
Event
connect- When client connects to
/syncnamespace withConnectionPayload, server will join socket to the roomsclipboard_sync/user_room/<user_id>ormouse_share/user_room/<user_id>depending on features enabled. How to join room is a problem we will discuss.
- When client connects to
update- When there is an update/move in key, mouse or clipboard, emit
updateto server withUpdatePayload - Server will broadcast the update payload to all devices in the corresponding room
- When there is an update/move in key, mouse or clipboard, emit
How to decide room during broadcasting?
This is a problem because some devices may enable only one feature and some enable both. It's possible that one want mouse and keyboard share but not clipboard share. To avoid broadcasting the same payload to the same device twice, we need a solution.
The root problem is, clipboard sync is part of mouse share, otherwise we can simply use 2 separate rooms.
Solution: We can remove the clipboard sync function from mouse room. If we remove the overlap, the problem is solved.
Room Joining
if clipboard and mouse both enabled {
join socket to both clipboard and mouse room
} else {
if only clipboard enabled {
join clipboard room
} else if only mouse enabled {
join mouse room
} else {
// none enabled
}
}
Broadcasting
Pseudocode
// when a payload is received
switch payload.type {
case mouse:
broadcast to mouse room
break;
case clipboard:
broadcast to clipboard
break;
default:
Unexpected
}
Workflow
WebRTC Version
Since WebRTC uses P2P connection, we don't have the broadcasting problem above. Every connection is one-to-one, and clients can decide whether or not to send a payload. Remember that in /device namespace connection, devices share their enabled features with all connected devices.
Assumption: Mouse share is a superset of clipboard sync in terms of functionality
Analysis
If device A enables only clipboard sync and device B enables only mouse share, they don't need a connection
If a device enables both features, it will need to establish a connection with every other device in the
/syncuser room.This is like a XOR operation, features are represented in one-hot vector.
- One-Hot Vector definition
11means both enabled01means only mouse share is enabled10means only clipboard sync is enabled00means none enabled (not possible in/syncnamespace as devices who connect to the namespace enables either feature)
- The function to decide if 2 devices need a connection is expressed as
XOR(onehot(A), onehot(B)) > 0
- XOR is the idea, we don't really have to implement the bitwise XOR operation, it's too hard to understand for other readers. The following logic is the equivalent.
def need_connection(A: Device, B: Device):
return (A.mouse and B.mouse) or (A.clipboard and B.clipboard)
- One-Hot Vector definition
Now, how to establish so many connections efficiently using SocketIO?
Since in /device namespace connection stage, we know the features enabled (as well as device id and socket id) for all devices, each device can compute whether it needs a connection with another device. They will use SocketIO to share WebRTC connection info.
Problem 1: How to solve conflict?
2 devices which need a connection don't know who to start the request, they need to reach a concensus.
This is similar to Block Chain. The problem can be easily solved with public information (both device have). e.g. larger device id starts the connection (this is not a good solution, the device with largest id will start all connections, not sure this is slow, but this sounds unfair). We can use math to introduce some randomness.
For example, both devices know the device id of each other, then we can implement the following logic
def i_should_start_connection(self_device: Device, other_device: Device):
device_id_product = self_device.device_id * other_device.device_id
if self_device.device_id > other_device.device_id:
return is_odd(device_id_product)
else:
return is_even(device_id_product)- Two devices starts to gamble blindly.
- Then device with larger device id bet the product is even, the device with smaller device id bet the product is odd. Only one of them is true.
- Product introduces a little bit of randomness. Makes the "game" fair.
Another solution is simply let server decide who to start, but this may be more complicated as server needs to store the feature info for each connected device.
Problem 2: Different Socket ID from Different Namespaces
I've discussed this in /file-transfer namespace.
Same solution for /sync namespace, simply add optional sync_socket_id field to DeviceConnectPayload and DeviceUpdatePayload. Devices can update their sync socket id after connecting to /sync namespace.
This also helps devices know when a device is ready to sync.
Algorithm
- Every time a new device joins the
/devicenamespace user room (means "online"), all devices in the user room are notified with its features enabled.- Every device will start to compute whether it needs a connection with the new device using the previously described
XORlogic. - If a connection is needed, a device will compute who should start the connection, itself or the new device. The chosen requester will start the request.
- Every device will start to compute whether it needs a connection with the new device using the previously described
- Once connection is established, they will start to broadcast data to connected devices.
- The type of data to send can also be computed easily. If the payload type is enabled on the device, then send it.
- If A has a clipboard payload to send, send it if B has enabled clipboard syncing
- If A has a mouse/keyboard payload to send, send it if B has enabled mouse sharing
- The type of data to send can also be computed easily. If the payload type is enabled on the device, then send it.
Payload Type Definition
type NewWebRTCConnectionRequestPayload = {
webrtc_info: WebRTCInfo;
device: {
socket_id: string;
id: number;
platorm: PlatformEnum;
};
target: {
socket_id: string;
device_id: number;
};
};
type NewWebRTCConnectionResponsePayload = {
accept: boolean;
message: string;
webrtc_info: WebRTCInfo;
device: {
socket_id: string;
id: number;
platorm: PlatformEnum;
};
target: {
socket_id: string;
device_id: number;
};
};
Event
All there is left for SocketIO to do is helping to establish WebRTC connection. The process and payload will be pretty much the same as WebRTC-based File Transfer
Let's say A starts a connection to B.
new_webrtc_connection_request- A emits
new_webrtc_connection_requestto server withNewWebRTCConnectionRequestPayload, server will emit to target device (socket_id included in payload)
- A emits
new_webrtc_connection_response- When the target device get
new_webrtc_connection_request, respond withnew_webrtc_connection_responseevent andNewWebRTCConnectionResponsePayload
- When the target device get
- When server gets
new_webrtc_connection_responseevent, server will emitnew_webrtc_connection_responseto source device by the socket id included inNewWebRTCConnectionResponsePayload
Stranger
How to communicate with a stranger (user that is not logged in)? How should a user room be created?
Magic Wormhole Style
- Device A starts a file transfer request to server with some file metadata. Without knowing the destination.
- Server will create a 3-word passcode such as
1-apple-banana- The first number
1is a channel id to uniquely identify the current transfer. appleandbananashould be 2 easy-to-recognize words as the actual passcode.
- The first number
- Server creates a SocketIO room with
anounymous_transfer/1-apple-banana - When another user enters the passcode
1-apple-bananaon another device (B), server will first look foranounymous_transfer/1-*room to check if this file transfer event is going on.- If the room exists, then check the passcode.
- If the passcode is correct, Device A and B can now exchange their WebRTC information and start transferring.
- If the passcode is incorrect, notify both devices. If the passcode is wrong for too many times, the transfer request should be cancelled.
- If the room doesn't exist, notify the user.
- If the room exists, then check the passcode.
One thing to worry about
- What if "hackers" keep sending request to our service with all ranges of channel id causing all transfers to fail on purpose?