This document provides the specifications of the communication protocol used by the drones, the client and the servers of the network. In the following document, drones, clients and servers are collectively referred to as nodes. The specifications are often broken or incomplete, you must improve over them.
This document also establishes some technical requirements of the project.
Can be useful for understanding and for not having to change the underlining type everywhere.
type NodeId = u64;
The Network Initializer:
- reads a local Network Initialization File that encodes the network topology and the drone parameters
- checks that the initialization file adheres to the formatting and restrictions defined in the section below
- checks that the initialization file represents a bidirectional graph
- according to the network topology, defined in the initialization file, performs the following actions(in no particular order):
- spawns the node threads
- spawns the simulation controller thread
- sets up the Rust channels for communicating between nodes that are connected in the topology
- sets up the Rust channels for communication between nodes and the simulation controller
The Network Initialization File is in the .toml
format, and structured as explained below:
Any number of drones, each formatted as:
[[drone]]
id = "drone_id"
connected_node_ids = ["connected_id1", "connected_id2", "connected_id3", "..."]
pdr = "pdr"
- note that the
pdr
is defined between 0 and 1 (0.05 = 5%). - note that
connected_node_ids
cannot containdrone_id
nor repetitions
Any number of clients, each formatted as:
[[client]]
id = "client_id"
connected_drone_ids = ["connected_id1", "..."] # max 2 entries
- note that
connected_drone_ids
cannot containclient_id
nor repetitions - note that a client cannot connect to other clients or servers
- note that a client can be connected to at least one and at most two drones
Any number of servers, each formatted as:
[[server]]
id = "server_id"
connected_drone_ids = ["connected_id1", "connected_id2", "connected_id3", "..."] # at least 2 entries
- note that
connected_drone_ids
cannot containserver_id
nor repetitions - note that a server cannot connect to other clients or servers
- note that a server should be connected to at least two drones
- note that the Network Initialization File should never contain two nodes with the same
id
value
A drone is characterized by a parameter that regulates what to do when a packet is received, that thus influences the simulation. This parameter is provided in the Network Initialization File.
Packet Drop Rate: The drone drops the received packet with probability equal to the Packet Drop Rate.
The PDR can be up to 100%, and the routing algorithm of every group should find a way to eventually work around this.
Recall that there are: Content servers (that is, Text and Media servers) and Communication servers. These servers are used by clients to implement applications.
These servers exchange, respectively, Text server messages, Media server messages and Communication server messages. These are high-level messages. Recall that you must standardize and regulate their low-level counterparts (that is, fragments).
The fragments that circulate in the network are source-routed (except for the commands sent from and the events received by the Simulation Controller).
Source routing refers to a technique where the sender of a data packet specifies the route the packet takes through the network. This is in contrast with conventional routing, where routers in the network determine the path incrementally based on the packet's destination.
The consequence is that drones do not need to maintain routing tables.
When a client or server wants to send a message to another node, it performs the following steps:
-
Route Computation: The sender calculates the entire path to the destination node. This path includes the sender itself and all intermediate nodes leading to the destination.
-
Creation of the Source Routing Header: The sender constructs a header that contains:
hops
: A list of node IDs representing the route from the sender to the destination.hop_index
: An index indicating the current position in thehops
list. It starts at 1 because the first hop (hops[0]
) is the sender itself.
-
Packet Sending: The sender attaches the source routing header to the packet and sends it to the first node in the route (the node at
hops[1]
).
Note: it is not mandatory to follow this precise order.
Consider the following simplified network topology:
Suppose that client A wants to send a message to server D.
Client A:
- Computes the route: A → B → E → F → D.
- Creates a source routing header:
hops
:[A, B, E, F, D]
.hop_index
:1
.
- Sends the packet to B, the first node after itself.
At Each Node:
-
Node B:
- Receives the packet.
- Determines that the next hop is E.
- Sends the packet to E.
-
Node E:
- Receives the packet.
- Determines that the next hop is F.
- Sends the packet to F.
-
Node F:
- Receives the packet.
- Determines that the next hop is D.
- Sends the packet to D.
-
Node D:
- Receives the packet.
- Sees that there are no more hops in the route.
- Processes the packet as the final destination.
For detailed steps on how each drone processes packets, including verification, error handling, and forwarding, please refer to the Drone Protocol section.
struct SourceRoutingHeader {
// must be set to 1 initially by the sender
hop_index: usize,
// Vector of nodes with initiator and nodes to which the packet will be forwarded to.
hops: Vec<NodeId>
}
When the network is first initialized, nodes only know who their own neighbors are.
Clients and servers need to obtain an understanding of the network topology ("what nodes are there in the network and what are their types?") so that they can compute a route that packets take through the network (refer to the Source routing section for details).
To do so, they must use the Network Discovery Protocol. The Network Discovery Protocol is initiated by clients and servers and works through query flooding.
The client or server that wants to learn the topology, called the initiator, starts by flooding a query to all its immediate neighbors:
enum NodeType {Client, Drone, Server}
struct FloodRequest {
/// Unique identifier of the flood, to prevent loops.
flood_id: u64,
/// ID of client or server
initiator_id: NodeId,
/// Records the nodes that have been traversed (to track the connections).
path_trace: Vec<(NodeId, NodeType)>
}
When a neighbor node receives the flood request, it processes it based on the following rules:
-
If the flood ID has already been received:
- The drone adds itself to the
path_trace
. - The drone creates a
FloodResponse
and sends it back.
- The drone adds itself to the
-
If the flood ID has not yet been received:
- The drone adds itself to the
path_trace
. - If it has neighbors (excluding the one from which it received the
FloodRequest
):- The drone forwards the packet to its neighbors (except the one from which it received the
FloodRequest
).
- The drone forwards the packet to its neighbors (except the one from which it received the
- If it has no neighbors, then:
- The drone creates a
FloodResponse
and sends it to the node from which it received theFloodRequest
.
- The drone creates a
- The drone adds itself to the
struct FloodResponse {
flood_id: u64,
path_trace: Vec<(NodeId, NodeType)>
}
- For the discovery protocol,
Packet
s of typeFloodRequest
andFloodResponse
will be sent. - The
routing_header
ofPacket
s of typeFloodRequest
will be ignored (as the Packet is sent to all neighbors except the one from which it was received). - The
routing_header
ofPacket
s of typeFloodResponse
, on the other hand, determines the packet's path.
For every flood response the initiator receives, it updates its understanding of the graph:
- If the node receives a flood response with a path trace, it records the paths between nodes. The initiator learns not only the immediate neighbors but also the connections between nodes further out.
- Over time, as the query continues to flood, the initiator accumulates more information and can eventually reconstruct the entire graph's topology.
The flood can terminate when:
- A node receives a
FloodRequest
with a flood_id that has already been received. - A node receives a
FloodRequest
but has no neighbors to forward the request to.
Clients and servers operate with high level Message
s which are disassembled into atomically sized packets that are routed through the drone network. The Client-Server Protocol standardizes and regulates the format of these messages and their exchange.
The previously mentioned packets can be: Fragment, Ack, Nack, FloodRequest, FloodResponse.
As described in the main document, Message
s must be serialized and can be possibly fragmented, and the Fragment
s can be possibly dropped by drones.
Message
is subject to fragmentation: see the dedicated section.
Fragment
(and Fragment
only) can be dropped by drones.
#[derive(Debug)]
pub enum ServerType {
ChatServer,
TextServer,
MediaServer,
}
#[derive(Debug)]
pub struct Message {
message_data: MessageData,
routing_header: SourceRoutingHeader
}
#[derive(Debug)]
// Part to be fragmented
pub struct MessageData {
session_id: u64,
content: MessageContent
}
If a drone receives a Message and can forward it to the next hop, it also sends an Ack to the client.
pub struct Ack {
fragment_index: u64,
}
If an error occurs, then a Nack is sent. A Nack can be of type:
- ErrorInRouting: If a drone receives a Message and the next hop specified in the Source Routing Header is not a neighbor of the drone, then it sends Error to the client.
- Dropped: If a drone receives a Message that must be dropped due to the Packet Drop Rate, then it sends Dropped to the client.
Source Routing Header contains the path to the client, which can be obtained by reversing the list of hops contained in the Source Routing Header of the problematic Message.
This message cannot be dropped by drones due to Packet Drop Rate.
pub struct Nack {
fragment_index: u64,
nack_type: NackType
}
pub enum NackType {
ErrorInRouting(NodeId), // contains id of not neighbor
DestinationIsDrone,
Dropped
}
Source Routing Header contains the path to the client, which can be obtained by reversing the list of hops contained in the Source Routing Header of the problematic Message.
As described in the main document, Message fragment cannot contain dynamically-sized data structures (that is, no Vec
, no String
, etc.). Therefore, packets will contain large, fixed-size arrays instead.
// defined as atomic message exchanged by the drones.
pub struct Packet {
pack_type: PacketType,
routing_header: SourceRoutingHeader,
session_id: u64,
}
pub enum PacketType {
MsgFragment(Fragment),
Ack(Ack),
Nack(Nack),
FloodRequest(FloodRequest),
FloodResponse(FloodResponse),
}
// fragment defined as part of a message.
pub struct Fragment {
fragment_index: u64,
total_n_fragments: u64,
length: u8,
// assembler will fragment/de-fragment data into bytes.
data: [u8; 80] // usable for image with .into_bytes()
}
To reassemble fragments into a single packet, a client or server uses the fragment header as follows:
-
The client or server receives a fragment.
-
It first checks the
session_id
in the header. -
If it has not received a fragment with the same
session_id
, then it creates a vector (Vec<u8>
with capacity oftotal_n_fragments
* 80) where to copy the data of the fragments; -
It would then copy
length
elements of thedata
array at the correct offset in the vector.
Note: if there are more than one fragment,
length
must be 80 for all fragments except for the last.
If the client or server has already received a fragment with the same session_id
, then it just needs to copy the data of the fragment in the vector.
Once that the client or server has received all fragments (that is, fragment_index
0 to total_n_fragments
- 1), then it has reassembled the whole fragment.
Therefore, the packet is now a message that can be delivered.
When a drone receives a packet, it must perform the following steps:
-
Step 1: Check if
hops[hop_index]
matches the drone's ownNodeId
.- If yes, proceed to Step 2.
- If no, send a Nack with
UnexpectedRecipient
(including the drone's ownNodeId
) and terminate processing.
-
Step 2: Increment
hop_index
by 1. -
Step 3: Determine if the drone is the final destination:
- If
hop_index
equals the length ofhops
, the drone is the final destination send a Nack withDestinationIsDrone
and terminate processing. - If not, proceed to Step 4.
- If
-
Step 4: Identify the next hop using
hops[hop_index]
, let's call itnext_hop
.- If
next_hop
is not a neighbor of the drone, send a Nack withErrorInRouting
(including the problematicNodeId
ofnext_hop
) and terminate processing. - If
next_hop
is a neighbor, proceed to Step 5.
- If
-
Step 5: Proceed based on the packet type:
-
Flood Messages: If the packet is flood-related, follow the rules specified in the Network Discovery Protocol section.
-
MsgFragment
:a. Check for Packet Drop:
- Determine whether to drop the packet based on the drone's Packet Drop Rate (PDR).
b. If the packet is to be dropped:
- Send back a Nack with type
Dropped
. The Nack should have a Source Routing Header containing the reversed path from the current drone back to the sender. - Terminate processing.
c. If the packet is not to be dropped:
- Send the packet to
next_hop
using the appropriate channel.
-
Consider the following simplified network topology:
Suppose that client A wants to send a message to server D.
Client A:
- Computes the route: A → B → E → F → D.
- Creates a source routing header:
hops
:[A, B, E, F, D]
.hop_index
:1
.
- Sends the packet to B, the first node after itself.
Detailed Steps:
-
Node B:
- Receives the packet with
hop_index = 1
. - Checks:
hops[1] = B
matches its own ID. - Increments
hop_index
to2
. - Next hop is
hops[2] = E
. - Sends the packet to E.
- Receives the packet with
-
Node E:
- Receives the packet with
hop_index = 2
. - Checks:
hops[2] = E
matches its own ID. - Increments
hop_index
to3
. - Next hop is
hops[3] = F
. - Sends the packet to F.
- Receives the packet with
-
Node F:
- Receives the packet with
hop_index = 3
. - Checks:
hops[3] = F
matches its own ID. - Increments
hop_index
to4
. - Next hop is
hops[4] = D
. - Sends the packet to D.
- Receives the packet with
-
Node D:
- Receives the packet with
hop_index = 4
. - Checks:
hops[4] = D
matches its own ID. - Increments
hop_index
to5
. - Since
hop_index
equals the length ofhops
, there are no more hops. - Concludes it is the final destination and processes the packet.
- Receives the packet with
TODO
Like nodes, the Simulation Controller runs on a thread. It must retain a means of communication with all nodes of the network, even when drones go down.
The Simulation Controller can send the following commands to drones:
Crash
: This commands makes a drone crash. Upon receiving this command, the drone’s thread should return as soon as possible.
AddSender(crossbeam::Sender, dst_id)
: This command provides a node with a new crossbeam Sender to send messages to node dst_id
.
AddReceiver(mpsc::Receiver, src_id)
: This command provides a node with a new crossbeam Receiver to receive messages from node src_id
.
Spawn(id, code)
: This command adds a new drone to the network.
SetPacketDropRate(id, new_pdr)
:
The Simulation Controller can receive the following events from nodes:
Topology(node_id, list_of_connected_ids, metadata)
: This event indicates that node node_id
has been added to the network and its current neighbors are list_of_connected_ids
. It can carry metadata that could be useful to display, such as the PDR and DR of Drones.
MessageSent(node_src, node_trg, metadata)
: This event indicates that node node_src
has sent a message to node_trg
. It can carry useful metadata that could be useful display, such as the kind of message, that would allow debugging what is going on in the network.
These are the kinds of high-level messages that we expect can be exchanged between clients and servers.
Notice that these messages are not subject to the rules of fragmentation, in fact, they can exchange Strings, Vecs
and other dynamically-sized types
#[derive(Debug)]
pub enum MessageContent {
// Client -> Server
ReqServerType,
ReqFilesList,
ReqFile(u64),
ReqMedia(u64),
ReqClientList,
ReqRegistrationToChat,
ReqMessageSend { to: NodeId, message: Vec<u8> },
// Server -> Client
RespServerType(ServerType),
RespFilesList(Vec<u64>),
RespFile(Vec<u8>),
RespMedia(Vec<u8>),
ErrUnsupportedRequestType,
ErrRequestedNotFound,
RespClientList(Vec<NodeId>),
RespMessageFrom { from: NodeId, message: Vec<u8> },
ErrWrongClientId,
}
Example of new file request, with id = 8:
fn new_file_request(source_id: NodeId, session_id: u64, routing: SourceRoutingHeader) -> Message {
let content = MessageType::ReqFile(8);
Message::new(routing, source_id, session_id, content)
}