예제 #1
0
    def send_data_message(self, ip, port, str_message):
        bytes_message = str_message.encode()

        ip_tuple = tuple([int(tok) for tok in ip.split('.')])

        header = bytearray(8)
        struct.pack_into("!B", header, 0, PKT_TYPE_DATA_MSG)
        struct.pack_into("!BBBB", header, 1, ip_tuple[0], ip_tuple[1], ip_tuple[2], ip_tuple[3])
        struct.pack_into("!H", header, 5, port)
        struct.pack_into("!B", header, 7, len(bytes_message))

        if (ip, port) in self.reachability_table:
            route_address = self.reachability_table[ip, port][1]
            utility.log_message_force(f"Routing the message {str_message} through node {route_address[0]}:"
                                      f"{route_address[1]}", self)
            self.send_message(route_address[0], route_address[1], header+bytes_message)
        else:
            utility.log_message_force(f"Received a message headed for {ip}:{port} but this node cannot reach it!", self)
예제 #2
0
    def stop_node(self):
        utility.log_message_force("Killing node, waiting for threads to finish...", self)

        # Set this flag to false, stopping all loops
        self.stopper.set()

        # Join all threads except command console handler, as this is that thread
        self.connection_handler_thread.join()
        self.message_reader_thread.join()

        # Clear event that halts keep alives so that thread can join
        self.continue_keep_alives.set()
        try:
            self.keep_alive_handler_thread.join()
        except RuntimeError:
            utility.log_message_force("Keep alive thread had not been started, no join needed.", self)

        try:
            self.update_handler_thread.join()
        except RuntimeError:
            utility.log_message_force("Update handler thread had not been started, no join needed.", self)

        # Send a message to all neighbors indicating that this node will die
        self.reachability_table_lock.acquire()
        for ip, port in self.neighbors:
            self.send_node_death_message(ip, port)
        self.reachability_table_lock.release()
예제 #3
0
    def find_awake_neighbors(self):
        # Halt infinite keep alives
        self.continue_keep_alives.clear()
        # Assume all neighbors are dead
        with self.unawakened_neighbors_lock:
            self.unawakened_neighbors = list(self.neighbors.keys())

        # Try to find neighbors until they are all alive or the maximum amount of retries is met
        current_tries = 0
        while current_tries < KEEP_ALIVE_RETRIES and self.unawakened_neighbors:
            current_tries += 1
            with self.unawakened_neighbors_lock:
                for (ip, port) in self.unawakened_neighbors:
                    utility.log_message(f"Waking {ip}:{port}", self)
                    self.send_keep_alive(ip, port)

            # Sleep for the timeout duration before trying again
            time.sleep(KEEP_ALIVE_TIMEOUT)

        with self.unawakened_neighbors_lock:
            if not self.unawakened_neighbors:
                utility.log_message("All neighbors have awakened!", self)
            else:
                unawakened_str = "Unawoken neighbors: "
                for (ip, port) in self.unawakened_neighbors:
                    # Set nodes as dead
                    with self.neighbors_lock:
                        neighbor = self.neighbors[ip, port]
                        self.neighbors[ip, port] = (neighbor[0], neighbor[1], 0, None)

                    # Add to string to inform user
                    unawakened_str += f"{ip}:{port} "
                utility.log_message_force(unawakened_str, self)

        # Continue infinite keep alives
        self.continue_keep_alives.set()
예제 #4
0
    def receive_message(self):
        # Read enough bytes for the message, a standard packet does not exceed 1500 bytes
        try:
            message, address = self.message_queue.get(block=True, timeout=SOCKET_TIMEOUT)
        except queue.Empty:
            return

        message_type = int.from_bytes(message[0:PKT_TYPE_SIZE], byteorder='big', signed=False)

        if message_type == PKT_TYPE_UPDATE:
            tuple_count = struct.unpack('!H', message[PKT_TYPE_SIZE:PKT_TYPE_SIZE + 2])[0]
            utility.log_message(f"Received a table update from {address[0]}:{address[1]} of size "
                                f"{len(message)} with {tuple_count} tuples.", self)
            # Decode the received tuples and update the reachability table if necessary
            self.decode_tuples(message[PKT_TYPE_SIZE + TUPLE_COUNT_SIZE:], address)

        elif message_type == PKT_TYPE_KEEP_ALIVE:
            utility.log_message(f"Received a keep alive from {address[0]}:{address[1]}.", self)
            self.send_ack_keep_alive(address[0], address[1])

        elif message_type == PKT_TYPE_ACK_KEEP_ALIVE:
            utility.log_message(f"Received a keep alive ack from {address[0]}:{address[1]}.", self)
            # Check if this is the first time the node has replied
            if address in self.unawakened_neighbors:
                with self.unawakened_neighbors_lock:
                    self.unawakened_neighbors.remove(address)

            with self.neighbors_lock:
                # Cancel the timer
                neighbor = self.neighbors[address]
                try:
                    neighbor[3].cancel()
                except AttributeError:
                    pass

                # If the node was thought dead re-add it to the reachability table
                with self.reachability_table_lock:
                    self.reachability_table[address] = (neighbor[0], address, neighbor[1])

                # Reset the retry number
                self.neighbors[address] = (neighbor[0], neighbor[1], KEEP_ALIVE_RETRIES, None)

        elif message_type == PKT_TYPE_FLOOD:
            hops = struct.unpack("!B", message[1:2])[0]
            utility.log_message_force(f"Received a FLOOD with {hops} hops remaining from {address[0]}:{address[1]}."
                                f"\nFlushing reachability table..."
                                f"\nWill ignore updates for {IGNORE_AFTER_FLOOD_INTERVAL} seconds.", self)

            # Continue the flood with one less hop
			if hops < 0:
				utility.log_message_force(f"Received a flood with too few hops from {address}!")
			elif hops > 255:
				utility.log_message_force(f"Received a flood with too many hops from {address}!")
		    elif hops != 0:   
				self.send_flood_message(hops - 1)
예제 #5
0
    def print_neighbors_table(self):
        utility.log_message_force("Neighbors:", self)
        self.reachability_table_lock.acquire()

        if not self.neighbors:
            utility.log_message_force("The neighbors table is empty.", self)

        else:
            for (ip, port), (mask, cost, current_retries, _) in self.neighbors.items():
                utility.log_message_force(f"Address: {ip}:{port}, mask: {mask}, cost: {cost}, current keep alive "
                                          f"retries: {current_retries}/{KEEP_ALIVE_RETRIES}", self)

        self.reachability_table_lock.release()
예제 #6
0
    def print_reachability_table(self):
        utility.log_message_force("Current reachability table:", self)
        self.reachability_table_lock.acquire()

        if not self.reachability_table:
            utility.log_message_force("The reachability table is empty.", self)
        else:
            i = 1
            for (ip, port), (mask, (ip2, port2), cost) in self.reachability_table.items():
                utility.log_message_force(f"{i} - Destiny: {ip}:{port}/{mask}, through: {ip2}:{port2}, cost: {cost}.", self)
                i += 1

        self.reachability_table_lock.release()
예제 #7
0
class UDPNode:

    def __init__(self, ip, mask, port, neighbors):
        # Simple data
        self.port = port
        self.ip = ip
        self.mask = mask
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.bind((self.ip, self.port))
        self.sock.setblocking(True)
        self.sock.settimeout(SOCKET_TIMEOUT)

        # Turns off many frequent prints, will not avoid logging
        self.print_updates = True

        # Structures
        # Reachability table: ip, port : mask, (ip, port), cost
        self.reachability_table = {}
        # Neighbors: ip, port : mask, cost,  current_retries (0 if node is dead), Timer obj
        self.neighbors = {}
        for (n_ip, n_mask, n_port), n_cost in neighbors.items():
            self.neighbors[(n_ip, n_port)] = (n_mask, n_cost, 0, None)

        # Queue to hold incoming messages
        # Will be flushed when encountering a flood
        self.message_queue = queue.Queue()

        # Used when waking nodes
        self.unawakened_neighbors = list(self.neighbors.keys())

        # Locks
        self.reachability_table_lock = threading.Lock()
        self.neighbors_lock = threading.Lock()
        self.message_queue_lock = threading.Lock()
        self.unawakened_neighbors_lock = threading.Lock()

        # Events
        self.stopper = threading.Event()
        self.ignore_updates = threading.Event()
        self.continue_keep_alives = threading.Event()

        # Threads
        self.connection_handler_thread = threading.Thread(target=self.handle_incoming_connections_loop)
        self.message_reader_thread = threading.Thread(target=self.read_messages_loop)
        self.keep_alive_handler_thread = threading.Thread(target=self.send_keep_alive_loop)
        self.update_handler_thread = threading.Thread(target=self.send_updates_loop)
        self.command_handler_thread = threading.Thread(target=self.handle_console_commands)

        # Prints identifying the node
        utility.log_message(f"Welcome to node {ip}:{port}/{mask}!", self)
        utility.log_message(f"\nThis node's neighbors:", self)
        self.print_neighbors_table()
        utility.log_message("\nAvailable commands are:", self)
        utility.log_message("    sendMessage <ip> <port> <message>", self)
        utility.log_message("    exit", self)
        utility.log_message("    change cost <neighbor ip> <neighbor port> <new cost>", self)
        utility.log_message("    printOwn", self)
        utility.log_message("    printTable", self)
        utility.log_message("    printNeighbors", self)
        utility.log_message("    prints <on|off>\n", self)

    def start_node(self):
        # Start the thread that handles incoming messages
        self.connection_handler_thread.start()

        # Start the thread that reads messages and puts them in a queue
        self.message_reader_thread.start()

        # Check for live neighbors
        self.find_awake_neighbors()

        # Start the thread that will listen and respond to console commands
        self.command_handler_thread.start()

        # Start the thread that loops to manage subsequuent keep alives
        self.keep_alive_handler_thread.start()

        # Start the thread that periodically sends updates
        self.update_handler_thread.start()

    def read_messages_loop(self):
        while not self.stopper.is_set():
            try:
                message, address = self.sock.recvfrom(BUFFER_SIZE)
            except socket.timeout:
                continue
            except ConnectionResetError:
                continue

            if self.ignore_updates.is_set():
                # Continue without putting the message in the queue if a flood occurred recently
                continue

            message_type = int.from_bytes(message[0:PKT_TYPE_SIZE], byteorder='big', signed=False)
            if message_type == PKT_TYPE_FLOOD or message_type == PKT_TYPE_DEAD:
                # Flood messages have more priority and the queue will need to be no matter what so delete it and put
                # the flood message first
                with self.message_queue_lock:
                    self.message_queue = queue.Queue()
                    self.message_queue.put((message, address))

            else:
                # All other messages have the same priority
                with self.message_queue_lock:
                    self.message_queue.put((message, address))
        utility.log_message("Finished the read messages loop!", self)

    def find_awake_neighbors(self):
        # Halt infinite keep alives
        self.continue_keep_alives.clear()
        # Assume all neighbors are dead
        with self.unawakened_neighbors_lock:
            self.unawakened_neighbors = list(self.neighbors.keys())

        # Try to find neighbors until they are all alive or the maximum amount of retries is met
        current_tries = 0
        while current_tries < KEEP_ALIVE_RETRIES and self.unawakened_neighbors:
            current_tries += 1
            with self.unawakened_neighbors_lock:
                for (ip, port) in self.unawakened_neighbors:
                    utility.log_message(f"Waking {ip}:{port}", self)
                    self.send_keep_alive(ip, port)

            # Sleep for the timeout duration before trying again
            time.sleep(KEEP_ALIVE_TIMEOUT)

        with self.unawakened_neighbors_lock:
            if not self.unawakened_neighbors:
                utility.log_message("All neighbors have awakened!", self)
            else:
                unawakened_str = "Unawoken neighbors: "
                for (ip, port) in self.unawakened_neighbors:
                    # Set nodes as dead
                    with self.neighbors_lock:
                        neighbor = self.neighbors[ip, port]
                        self.neighbors[ip, port] = (neighbor[0], neighbor[1], 0, None)

                    # Add to string to inform user
                    unawakened_str += f"{ip}:{port} "
                utility.log_message_force(unawakened_str, self)

        # Continue infinite keep alives
        self.continue_keep_alives.set()

    def send_updates_loop(self):
        self.send_update()
        while not self.stopper.wait(SEND_TABLE_UPDATE_INTERVAL):
            self.send_update()
        utility.log_message("Finished the update sending loop!", self)

    def send_update(self):
        for (ip, port) in self.neighbors:
            self.send_reachability_table(ip, port)

    def send_keep_alive_loop(self):
        while not self.stopper.wait(SEND_KEEP_ALIVE_INTERVAL):
            self.continue_keep_alives.wait()
            with self.neighbors_lock:
                for (ip, port), (mask, cost, current_retries, _) in self.neighbors.items():
                    if current_retries > 0:
                        utility.log_message(f"Sending keep alive to {ip}:{port}...", self)

                        # Create a timer to implement the timeout, will execute code to handle the timeout after it
                        # triggers
                        # If an ack is received this timer will be cancelled
                        timeout_timer = threading.Timer(KEEP_ALIVE_TIMEOUT,
                                                        self.handle_keep_alive_timeout, [], {"ip": ip, "port": port})
                        timeout_timer.start()

                        # Save the timer in that neighbor's tuple so it can be retrieved and cancelled if/when necessary
                        self.neighbors[(ip, port)] = (mask, cost, current_retries, timeout_timer)

                        self.send_keep_alive(ip, port)
        utility.log_message("Finished the send keep alive loop!", self)

    def handle_keep_alive_timeout(self, **kwargs):
        # Get the parameters from the kwargs dictionary
        ip = kwargs["ip"]
        port = kwargs["port"]

        with self.neighbors_lock:
            # Check the neighbor's retry status
            neighbor = self.neighbors[ip, port]

            if neighbor[2] == 1:
                # If decreasing the remaining retries would set it to 0, remove the entry and start a flood
                utility.log_message(f"Keep alive message to {ip}:{port} timed out! No more retries remaining, deleting "
                                    f"entry and starting flood...", self)
                self.neighbors[ip, port] = (neighbor[0], neighbor[1], neighbor[2] - 1, None)
                self.remove_reachability_table_entry(ip, port)
                self.send_flood_message(HOP_NUMBER)
            elif neighbor[2] > 0:
                # If the neighbor is not already at 0 retries, decrease the remaining retries
                self.neighbors[ip, port] = (neighbor[0], neighbor[1], neighbor[2]-1, None)
                utility.log_message(f"Keep alive message to {ip}:{port} timed out! {neighbor[2]} retries remaining...",
                                    self)

    def handle_incoming_connections_loop(self):
        while not self.stopper.is_set():
            self.receive_message()
        utility.log_message("Finished the handle incoming connections loop!", self)

    def receive_message(self):
        # Read enough bytes for the message, a standard packet does not exceed 1500 bytes
        try:
            message, address = self.message_queue.get(block=True, timeout=SOCKET_TIMEOUT)
        except queue.Empty:
            return

        message_type = int.from_bytes(message[0:PKT_TYPE_SIZE], byteorder='big', signed=False)

        if message_type == PKT_TYPE_UPDATE:
            tuple_count = struct.unpack('!H', message[PKT_TYPE_SIZE:PKT_TYPE_SIZE + 2])[0]
            utility.log_message(f"Received a table update from {address[0]}:{address[1]} of size "
                                f"{len(message)} with {tuple_count} tuples.", self)
            # Decode the received tuples and update the reachability table if necessary
            self.decode_tuples(message[PKT_TYPE_SIZE + TUPLE_COUNT_SIZE:], address)

        elif message_type == PKT_TYPE_KEEP_ALIVE:
            utility.log_message(f"Received a keep alive from {address[0]}:{address[1]}.", self)
            self.send_ack_keep_alive(address[0], address[1])

        elif message_type == PKT_TYPE_ACK_KEEP_ALIVE:
            utility.log_message(f"Received a keep alive ack from {address[0]}:{address[1]}.", self)
            # Check if this is the first time the node has replied
            if address in self.unawakened_neighbors:
                with self.unawakened_neighbors_lock:
                    self.unawakened_neighbors.remove(address)

            with self.neighbors_lock:
                # Cancel the timer
                neighbor = self.neighbors[address]
                try:
                    neighbor[3].cancel()
                except AttributeError:
                    pass

                # If the node was thought dead re-add it to the reachability table
                with self.reachability_table_lock:
                    self.reachability_table[address] = (neighbor[0], address, neighbor[1])

                # Reset the retry number
                self.neighbors[address] = (neighbor[0], neighbor[1], KEEP_ALIVE_RETRIES, None)

        elif message_type == PKT_TYPE_FLOOD:
            hops = struct.unpack("!B", message[1:2])[0]
            utility.log_message_force(f"Received a FLOOD with {hops} hops remaining from {address[0]}:{address[1]}."
                                f"\nFlushing reachability table..."
                                f"\nWill ignore updates for {IGNORE_AFTER_FLOOD_INTERVAL} seconds.", self)

            # Continue the flood with one less hop
			if hops < 0:
				utility.log_message_force(f"Received a flood with too few hops from {address}!")
			elif hops > 255:
				utility.log_message_force(f"Received a flood with too many hops from {address}!")
		    elif hops != 0:   
				self.send_flood_message(hops - 1)

        elif message_type == PKT_TYPE_DATA_MSG:
            ip_bytes = message[1:5]
            ip = f"{ip_bytes[0]}.{ip_bytes[1]}.{ip_bytes[2]}.{ip_bytes[3]}"
            port = int.from_bytes(message[5:7], byteorder='big', signed=False)
            size = int.from_bytes(message[7:8], byteorder='big', signed=False)
            str_message = message[8:].decode()

            if ip == self.ip and port == self.port:
                utility.log_message_force(f"Received the data message {str_message} from {address[0]}:{address[1]}!", self)
            else:
                utility.log_message_force(f"Received the message {str_message} headed for {ip}:{port} from "
                                    f"{address[0]}:{address[1]}! Rerouting...", self)
                self.send_data_message(ip, port, str_message)
예제 #8
0
    def handle_console_commands(self):
        while not self.stopper.is_set():
            try:
                pass
                command = input("Enter your command...\n> ")
            except EOFError:
                utility.log_message(f"EOFile while expecting user input...", self)
                continue
            command = command.strip().split(" ")

            if len(command) == 0:
                utility.log_message_force("Please enter a valid command.", self)
                continue

            elif command[0] == "sendMessage":
                if len(command) != 4:
                    utility.log_message_force("Please enter a valid command.", self)
                    continue
                else:
                    self.send_data_message(command[1], int(command[2]), command[3])

            elif command[0] == "exit" or command[0] == "deleteNode":
                self.stop_node()

            elif command[0] == "printTable":
                self.print_reachability_table()

            elif command[0] == "printOwn":
                utility.log_message_force(f"This node's information: {self.ip}:{self.port}/{self.mask}", self)

            elif command[0] == "printNeighbors":
                self.print_neighbors_table()

            elif command[0] == "changeCost":
                if len(command) != 4:
                    utility.log_message_force("Please enter a valid command.", self)
                else:
                    ip = command[1]
                    port = int(command[2])
                    new_cost = int(command[3])

                    with self.neighbors_lock:
                        if (ip, port) not in self.neighbors:
                            utility.log_message_force(f"The node {ip}:{port} is not a neighbor, try again.", self)
                        else:
                            # Change the cost
                            neighbor = self.neighbors[(ip, port)]
                            self.neighbors[(ip, port)] = (neighbor[0], new_cost, neighbor[2], neighbor[3])

                            # Notify the node
                            self.send_cost_change(ip, port, new_cost)

            elif command[0] == "prints":
                if len(command) != 2:
                    utility.log_message_force("Please enter a valid command.", self)
                elif command[1] == "on":
                    self.print_updates = True
                elif command[1] == "off":
                    self.print_updates = False
                else:
                    utility.log_message_force("Please enter a valid command.", self)

            else:
                utility.log_message_force("Unrecognized command, try again.", self)