Navigate to the project root directory:
cd path/to/computer-networks/
Compile the source code:
mvn compile
mvn compile assembly:single
Note: If the first build fails, try running the command again. Maven may still be downloading dependencies.
Start Router 1
cd path/to/computer-networks/
java -jar target/COMP535-1.0-SNAPSHOT-jar-with-dependencies.jar conf/router1.conf
Start Additional Routers
cd path/to/computer-networks/
java -jar target/COMP535-1.0-SNAPSHOT-jar-with-dependencies.jar conf/routerX.conf
Replace X with the router number (e.g., 2, 3, …). Each router must be started in a separate terminal.
Before executing the send command, make sure to start all routers present in the network topology.
The attach operation establishes a link between two routers. On the requesting router side, several validation checks are performed before initiating an attachment: The router verifies that at least one port is available. The router rejects attachment attempts to itself. The router checks whether a link to the destination router already exists to prevent duplicate connections.
While iterating through the port array, the requester identifies the lowest-index free port. If all ports are occupied, the attachment request is rejected.
Once a free port is found, the requester creates a HELLO packet with the link weight embedded in the packet payload. This packet is sent to the destination router via a socket connection using an ObjectOutputStream. The requester then waits for a response packet through an ObjectInputStream. A valid attachment response is identified by sospfType == 3. If the response field attachAccepted is true, the requester creates a Link object and stores it in the selected free port.
On the destination router side, a ServerSocket is continuously listening for incoming attachment requests within requestHandler(). When a packet with isAttachReq == true is received, the router first checks for available ports. If no ports are free, the request is automatically rejected.
If a port is available, the destination router prompts the user to accept or reject the request via the terminal. This is coordinated using the shared variables pendingAttach and attachDecision. Once the user provides input, the destination router constructs a response packet indicating acceptance or rejection. If accepted, a corresponding Link object is created and stored in a free port. The response packet is then sent back to the requesting router.
The start operation transitions a router from the inactive state into the network by establishing bidirectional communication with all attached neighbors using a HELLO handshake. Before initiating the process, the router verifies that it has not already been started. It then iterates through all active ports and identifies neighboring routers from each Link object.
For each neighbor, the starting router establishes a socket connection and sends a HELLO packet, setting the neighbor’s statusin the Link object to INIT.
On the receiving router side, requestHandler() processes incoming HELLO packets by locating the corresponding link using the source simulated IP address. The receiver responds with a HELLO packet and updates the sender’s status to INIT or TWO_WAY if it was already initialized.
Back on the starting router, upon receiving the response HELLO, the neighbor’s status is updated to TWO_WAY, and a final HELLO packet is sent to complete the handshake.
Once the second HELLO is received by the destination router, it also sets the link the sender router's status to TWO_WAY. At the end of the handshake, the starting router marks it's own status as TWO_WAY and closes the socket.
Once all HELLO handshakes are complete and the router's own status is set to TWO_WAY, the router builds its initial Link State Advertisement (LSA) by calling buildOwnLSA(). This method updates the router's own LSA entry in the Link State Database (LSD) with a self-link and one entry per TWO_WAY neighbor, recording each neighbor's simulated IP address, port number, and link weight. The LSA sequence number is incremented to mark this as newer information, and routers will accept any new information flooded through the network.
After building its LSA, the router constructs an LSAUPDATE packet (sospfType 4) containing a snapshot of its entire LSD and floods it to all TWO_WAY neighbors via sendLSAUpdateTo(). Each neighbor that receives this update merges it into its own LSD: for each LSA in the received packet, the neighbor checks whether it has no existing entry for that router or whether the received sequence number is higher than what it already has. If either condition holds, the LSD entry is updated. The receiving router then re-floods the updated packet to all of its own TWO_WAY neighbors except the one that sent the update, ensuring that new topology information propagates through the entire network while avoiding infinite loops.
The neighbors command lists all directly connected routers that are in the TWO_WAY state.
For each port, the router checks whether a Link object exists. If so, it determines which endpoint represents the neighboring router. If the neighbor’s status is not TWO_WAY, the router reports that no active neighbors were found for that port. Otherwise, the simulated IP address of the neighbor is printed.
The detect command displays the shortest path, along with the associated link weights, from the router executing the command to the specified destination router.
The implementation is handled by the LinkStateDatabase.getShortestPathStr() function. First, the function calls getShortestPathHashMap(), which runs Dijkstra’s algorithm on the Link State Database to compute the shortest path from the source router to all routers in the topology. The result is stored in a HashMap<String, PathPair>, where each router’s simulated IP address maps to a PathPair object containing:
- the predecessor node along the shortest path (parent)
- the total cost from the source router to that node (cost)
To reconstruct the shortest path to the destination router, the algorithm starts at the destination node and repeatedly follows the predecessor nodes stored in the hashmap. For each node, the corresponding PathPair entry provides the parent node along the path. This process continues until the source router is reached.
During this backtracking process, the link weights between nodes are determined by subtracting the total cost values stored in the hashmap. Specifically, the weight of an edge between two nodes is calculated as the difference between their accumulated shortest path costs.
The nodes and their associated link weights are then formatted into a readable string representing the path from the source router to the destination router.
Finally, the processDetect() function prints this formatted path string to the terminal so the user can visualize the shortest route through the network.
The send command transmits a message from source router to destination router, forwarding it hop-by-hop along the shortest path computed from the LSD.
When processSend() is called, it first consults the LSD to determine the next hop toward the destination by calling lsd.getNextHop(destinationIP). It then looks up the process IP and port of that next-hop router in the local ports array, constructs an application message packet (sospfType 2), and sends it over a new socket connection.
Shortest path computation is implemented in LinkStateDatabase using Dijkstra's algorithm over the weighted graph stored in the LSD. getShortestPathHashMap() initializes a map from each known router ID to a PathPair holding the best known cost and the predecessor node on that path. Routers directly reachable from the source are initialized with their link weight while all others start at infinity. The algorithm then repeatedly selects the unvisited router with the lowest cost, marks it as visited, and relaxes the edges in its LSA entry, updating costs and predecessors whenever a shorter path is found.
getNextHop() uses this map to trace the shortest path backwards from the destination to the source, returning the first hop after the source. getShortestPathStr() performs the same reconstruction to build a human-readable string of the form source -> (cost) -> intermediate -> ... -> destination. When an intermediate router receives a message packet, handleApplicationMessage() checks whether the packet's destination IP matches its own. If not, it performs the same next-hop lookup and forwards the packet onward. When the destination router receives the packet, it prints the source IP and message content to the terminal.
The connect operation establishes a link between two routers after the network is already running, so after start has been executed already. It behaves like attach followed by an immediate handshake and LSA synchronization, but uses a distinct packet type (sospfType == 5) to differentiate it from a pre-start attachment request.
On the requesting router side, the router cannot connect to itself, must have a free port, and cannot create a duplicate link. The lowest-index free port is identified before initiating the connection.
A CONNECT packet is constructed with the link weight embedded in the message field. If the destination router already exists in the local LSD, its current LSA sequence number is included in the packet so the receiving router can correctly override this. The requester waits for a reply packet (sospfType == 6) indicating acceptance or rejection, and an LSAUPDATE packet for immediate synchronization.
If accepted, the link is created with status TWO_WAY (bypassing the HELLO handshake since both routers are already started). The requester then calls buildOwnLSA() and floods an LSAUPDATE to all TWO_WAY neighbors to propagate the new topology.
The disconnect operation removes a single link identified by its port number and triggers network-wide LSA synchronization.
The neighbor at that port is identified, then the port is set to null. buildOwnLSA() is immediately called to rebuild the router's LSA without the disconnected neighbor, incrementing the sequence number so the update will be accepted by other routers.
The disconnected router's entry is also removed from the local LSD (lsd._store.remove()), since the local router no longer has direct knowledge of it. An LSAUPDATE is then flooded to all remaining TWO_WAY neighbors, and a final LSAUPDATE is also sent to the disconnected router itself so it can update its own LSD to reflect the removed link.
When a neighbor's LSA is received that no longer lists the local router as a neighbor, the corresponding port is nulled and buildOwnLSA() is called to remove that neighbor from the local router's own LSA.
The quit operation removes a router from the network before terminating the process.
The router first modifies its own LSA (concurrency handled): all link descriptions are cleared and replaced with only a self-link (a LinkDescription where linkID equals the router's own simulated IP). The sequence number is incremented so receiving routers will accept this update over any previously stored version. An LSAUPDATE packet containing this empty LSA is then sent to all non-null neighbors, ensuring that neighbors which completed the handshake but were not yet fully synchronized also receive the termination notice.
On the receiving side, processLSAUpdate() detects the quit condition via the neighborQuitted check (a neighbor's LSA whose only link is a self-reference). When this is detected, the corresponding port is nulled and buildOwnLSA() is called to remove the dead router from the local LSA. This rebuilt LSA is then propagated in the flood, ensuring all routers in the topology converge to a state that excludes the quit router.
The updateWeight operation changes the cost of a link at a specified port number and synchronizes the change across the entire network.
The router first validates that the port number is in range and has an active link. The weight field of the Link object at that port is updated, buildOwnLSA() is then called, which rebuilds the router's LSA from scratch using the current port weights (picking up the new value automatically) and increments the LSA sequence number.
An LSAUPDATE packet is then sent to all TWO_WAY neighbors. On the receiving side, processLSAUpdate() detects a weight change via the neighborWeightChanged check, comparing the weight of the link pointing to the local router in the received LSA against the weight stored in the existing LSA. If they differ, the corresponding Link object in ports[] is updated and buildOwnLSA() is called to reflect the new weight in the local router's own LSA before the update is flooded further.