diff --git a/client_client.py b/client_client.py index b5c8ca0..e0da677 100644 --- a/client_client.py +++ b/client_client.py @@ -5,153 +5,201 @@ - When a client wants to message another client, a TCP connection is opened, data is sent and the connection is closed. """ -import socket -import threading import sys from typing import Dict, List +import socket +import struct +import config +import json +import threading +import random -def tcp_listener(port: int): - s = socket.socket() - # host = socket.gethostname() - host = '0.0.0.0' - s.bind((host, port)) - s.listen(5) - print('TCP listener started on port', port) +class Client(): + def __init__(self): + self.client_list = dict() + self.username = '' + + @staticmethod + def multicast_handler(client_port: int): + # create the datagram socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', client_port)) + # set a timeout so the socket does not block indefinitely when trying to receive data. + sock.settimeout(0.2) + + # Set the time-to-live for messages to 1 so they do not go past the local network segment. + ttl = struct.pack('b', 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - while True: - conn, address = s.accept() - data = conn.recv(1024) - if not data: - break - data = data.decode('utf8') try: - data_split = data.split() - name = data_split[0] - message = " ".join(data_split[1:]) - print(f'{name} says: {message}') - except IndexError: - print(f'{address} sends an empty message.') - conn.sendall(bytes('OK', encoding='utf8')) + # send request to the multicast group + print(f'CLIENT: Sending multicast message to {config.MULTICAST_IP}') + + message = 'SERVER DISCOVERY' + multicast_group = (config.MULTICAST_IP, config.MULTICAST_PORT) + sock.sendto(bytes(message, encoding='utf-8'), multicast_group) + finally: + sock.close() + + def tcp_handler(self, _port: int): + sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_tcp.bind(('', _port)) + sock_tcp.listen(5) + + # empty buffer + buff = b'' + while True: + print(f'CLIENT: Waiting for a TCP connection') + connection, client_address = sock_tcp.accept() + try: + print(f'CLIENT: Connection from {client_address}') + + self.username = input('=== Provide Your Nickname === ') + + connection.sendall(bytes(self.username, encoding='utf8')) + # receive the data in chunks and add to the buffer + while True: + print(f'CLIENT: Waiting for the server to send client base') + data = connection.recv(512) + buff += data + if not data: + break + break + finally: + print(f'CLIENT: Client base received') + client_list = json.loads(buff.decode('utf-8')) + self.client_list = client_list + print(f'CLIENT: Closing TCP connection') + # clean up the connection + connection.close() + break + + @staticmethod + def tcp_listener(_port: int): + s = socket.socket() + # host = socket.gethostname() + host = '0.0.0.0' + s.bind((host, _port)) + s.listen(5) + print('TCP listener started on port', _port) + + while True: + conn, address = s.accept() + data = conn.recv(1024) + if not data: + break + data = data.decode('utf8') + try: + data_split = data.split() + name = data_split[0] + message = " ".join(data_split[1:]) + print(f'{name} says: {message}') + except IndexError: + print(f'{address} sends an empty message.') + conn.sendall(bytes('OK', encoding='utf8')) + + @staticmethod + def do_list(clients: List): + print("Available clients:") + + for client in clients: + print(f'address: {client["ip"]}:{client["port"]}, nickname: {client["nickname"]}') + + @staticmethod + def do_connect(address: Dict, _username: str) -> bool: + """ + address = { + 'ip': "IPv4_ADDR" + 'port: 1234 + } + """ + msg = input('Please provide your message: ') + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((address['ip'], address['port'])) + s.sendall(bytes(f'{_username} {msg}', encoding='utf8')) + resp = s.recv(1024) -def do_list(clients: List): - print("Available clients:") - - for client in clients: - print(f'address: {client["ip"]}:{client["port"]}, nickname: {client["name"]}') + s.close() + except socket.error as e: + print('[ERR]', e) + return False + if resp.decode('utf8') == 'OK': + return True + else: + return False -def do_connect(address: Dict, username: str) -> bool: - """ - address = { - 'ip': "IPv4_ADDR" - 'port: 1234 - } - """ - msg = input('Please provide your message: ') - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect((address['ip'], address['port'])) - - s.sendall(bytes(f'{username} {msg}', encoding='utf8')) - resp = s.recv(1024) - - s.close() - except socket.error as e: - print('[ERR]', e) - return False - - if resp.decode('utf8') == 'OK': - return True - else: - return False - - -def handle_actions(clients: List, username: str): - actions = ['list', 'msg', 'exit',] - - input_split = input().split() - action = input_split[0] - - if action not in actions: - print('[ERR] Incorrect command') - return - - if action == 'list': - do_list(clients) - - if action == 'msg': - try: - client_address = input_split[1] - client = {} - # check if client_address is an IP:port string or client name - cli_filtered = list(filter(lambda c: c['name'] == client_address, clients)) - - if len(cli_filtered): # client name found -> assign client - client = cli_filtered[0] - else: - # split input to separate IP / port - client['ip'] = client_address.split(':')[0] - client['port'] = int(client_address.split(':')[1]) - - success = do_connect(client, username) - if success: - print(f'Message to {client_address} delievered.') - else: - print(f'Error sending data to {client_address}') - except (IndexError, ValueError): - print('[ERR] incorrect address / name') + def handle_actions(self, clients: List, _username: str): + actions = ['list', 'msg', 'exit'] + + input_split = input().split() + action = input_split[0] + + if action not in actions: + print('[ERR] Incorrect command') return - if action == 'exit': - print('Exiting...') - sys.exit(1) - - -# this should be replaced with a dynamic list provided by the server -client_list = [ - { - 'ip': '127.0.0.1', - 'port': 1234, - 'name': 'client1', - }, - { - 'ip': '127.0.0.1', - 'port': 6969, - 'name': 'client2', - }, - { - 'ip': '127.0.0.1', - 'port': 8888, - 'name': 'client3', - }, -] - -usage_str = """ -Commands: - list - display available clients - msg CLIENT_NAME/CLIENT_ADDRESS - connect to one of the clients specified in - wait - allow other clients to connect to you - exit - close chat client -""" + if action == 'list': + self.do_list(clients) + + if action == 'msg': + try: + client_address = input_split[1] + client = {} + # check if client_address is an IP:port string or client name + cli_filtered = list(filter(lambda c: c['name'] == client_address, clients)) + + if len(cli_filtered): # client name found -> assign client + client = cli_filtered[0] + else: + # split input to separate IP / port + client['ip'] = client_address.split(':')[0] + client['port'] = int(client_address.split(':')[1]) + + success = do_connect(client, _username) + if success: + print(f'Message to {client_address} delievered.') + else: + print(f'Error sending data to {client_address}') + except (IndexError, ValueError): + print('[ERR] incorrect address / name') + return + + if action == 'exit': + print('Exiting...') + sys.exit(1) + + + if __name__ == '__main__': - listener_port = 5000 - - while True: - try: - listener_port = int(input('TCP Listener port number (must be an integer): ')) - break - except ValueError: - print('[ERR] Incorrect port number.') - - username = input('Your Username: ') + usage_str = """ + Commands: + list - display available clients + msg CLIENT_NAME/CLIENT_ADDRESS - connect to one of the clients specified in + wait - allow other clients to connect to you + exit - close chat client + """ + + s = Client() + + port = random.randint(50_000, 65_000) + # pass selected port to the TCP thread, in order to listen on the same port + # thread in the background as daemon + th = threading.Thread(target=s.tcp_handler, args=(port,), daemon=True) + th.start() + s.multicast_handler(port) + th.join() - threading.Thread(target=tcp_listener, args=(listener_port,)).start() + print(s.client_list) + # TODO: smart port allocation + threading.Thread(target=s.tcp_listener, args=(port+1,)).start() print(usage_str) while True: - handle_actions(client_list, username) + s.handle_actions(s.client_list['clients'], s.username) diff --git a/config.py b/config.py index cfbb941..3014427 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ MULTICAST_PORT = 10001 MULTICAST_IP = '224.0.0.1' TCP_PORT = 10001 -SERVER_IP = 'localhost' \ No newline at end of file +SERVER_IP = '127.0.0.1' \ No newline at end of file diff --git a/daemon_file.py b/daemon_file.py index 2adccad..2b19dae 100644 --- a/daemon_file.py +++ b/daemon_file.py @@ -4,61 +4,71 @@ import json import struct import threading -import time import os from daemon import runner -from datetime import datetime - -client_base = { "clients": - [ - { - "address": "192.168.0.1", - "port": "2208", - "nickname": "nikita", - "online": True - }, - { - "address": "192.168.0.2", - "port": "2208", - "nickname": "pobeda", - "online": False - }, - ] -} - - -class App(): + + +client_base = {"clients": [ + { + "ip": "192.168.0.1", + "port": 2250, + "nickname": "pepka", + "online": True + } +]} + + +class App(): + def __init__(self): self.stdin_path = '/dev/null' self.stdout_path = '/dev/null' self.stderr_path = '/dev/null' - self.pidfile_path = os.path.join(os.getcwd(), 'multiproto.pid') + self.pidfile_path = os.path.join(os.getcwd(), 'multiproto.pid') self.pidfile_timeout = 5 def run(self): while True: - # logger.debug("Debug message") - # logger.info("Info message") - # logger.warn("Warning message") - # logger.error("Error message") - # logger.info(f'LOG: {datetime.now()}') + # main thread self.multicast_handler() @staticmethod - def tcp_handler(client_address): + def tcp_handler(address): + logger.info(f'Starting TCP session with {address}') # TCP socket creation sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(client_address) + sock.bind(('', config.TCP_PORT)) + sock.connect(address) # convert dict structure to bytes msg = json.dumps(client_base).encode('utf-8') try: - sock.sendall(msg) + + while True: + data = sock.recv(1024) + if not data: + break + # TODO: there might be additional authentication for the user + _nickname = data.decode('utf8') + logger.info(f'New user named: {_nickname}') + + # send client base before the update + sock.sendall(msg) + + # update client base + client_base['clients'].append( + { + "ip": address[0], + "port": address[1], + "nickname": _nickname, + "online": True + } + ) + # TODO: update client base via SCTP + finally: - print('closing socket') sock.close() - pass def multicast_handler(self): server_address = ('', config.MULTICAST_PORT) @@ -76,22 +86,14 @@ def multicast_handler(self): # Receive/respond loop while True: - print('\nwaiting to receive message') logger.info(f'Listening for multicast messages') data, address = sock.recvfrom(1024) - - print('received %s bytes from %s' % (len(data), address)) logger.info(f'Received multicast message from {address}') - print(data) - print('sending acknowledgement to', address) - logger.info(f'Starting TCP session with {address}') # call TCP session - thread = threading.Thread(target=self.tcp_handler, args=address) + thread = threading.Thread(target=self.tcp_handler, args=(address,), daemon=True) # run thread in the background as daemon - thread.daemon = True thread.start() - # sock.sendto(json.dumps(client_base).encode('utf-8'), address) if __name__ == '__main__': diff --git a/multicast_client.py b/multicast_client.py index 0b1d6af..0625d68 100644 --- a/multicast_client.py +++ b/multicast_client.py @@ -1,80 +1,72 @@ import socket import struct -import sys import config import json import threading +import random -message = 'SERVER DISCOVERY' -multicast_group = (config.MULTICAST_IP, config.MULTICAST_PORT) +def multicast_handler(client_port: int): + # create the datagram socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', client_port)) + # set a timeout so the socket does not block indefinitely when trying to receive data. + sock.settimeout(0.2) + + # Set the time-to-live for messages to 1 so they do not go past the local network segment. + ttl = struct.pack('b', 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + + try: + # send request to the multicast group + print(f'CLIENT: Sending multicast message to {config.MULTICAST_IP}') + + message = 'SERVER DISCOVERY' + multicast_group = (config.MULTICAST_IP, config.MULTICAST_PORT) + sock.sendto(bytes(message, encoding='utf-8'), multicast_group) + finally: + sock.close() -def tcp_handler(): - sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_address = (config.SERVER_IP, config.TCP_PORT) - sock_tcp.bind(server_address) - sock_tcp.listen(1) +def tcp_handler(port: int): + sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock_tcp.bind(('', port)) + sock_tcp.listen(5) + # empty buffer + buff = b'' while True: - # Wait for a connection - print('waiting for a connection') - connection, client_address = sock.accept() + print(f'CLIENT: Waiting for a TCP connection') + connection, client_address = sock_tcp.accept() try: - print(sys.stderr, 'connection from', client_address) + print(f'CLIENT: Connection from {client_address}') - # Receive the data in small chunks and retransmit it + username = input('=== Provide Your Nickname === ') + connection.sendall(bytes(username, encoding='utf8')) + # receive the data in chunks and add to the buffer while True: - data = connection.recv(16) - print('received "%s"' % data) - if data: - print('sending data back to the client') - connection.sendall(data) - else: - print('no more data from', client_address) + print(f'CLIENT: Waiting for the server to send client base') + data = connection.recv(512) + buff += data + if not data: break + break finally: - # Clean up the connection + print(f'CLIENT: Client base received') + res_dict = json.loads(buff.decode('utf-8')) + # print(res_dict) + print(f'CLIENT: Closing TCP connection') + # clean up the connection connection.close() + break -thread = threading.Thread(target=tcp_handler, args=()) -# run thread in the background as daemon -# thread.daemon = True -thread.start() - -# Create the datagram socket -sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - -# Set a timeout so the socket does not block indefinitely when trying -# to receive data. -sock.settimeout(0.2) - -# Set the time-to-live for messages to 1 so they do not go past the -# local network segment. -ttl = struct.pack('b', 1) -sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - -try: - - # Send data to the multicast group - print('sending "%s"' % message) - sent = sock.sendto(bytes(message, encoding='utf-8'), multicast_group) - - # Look for responses from all recipients - while True: - print('waiting to receive') - try: - data, server = sock.recvfrom(1024) - except socket.timeout: - print('timed out, no more responses') - break - else: - res_dict = json.loads(data.decode('utf-8')) - print(res_dict) +if __name__ == '__main__': + port = random.randint(50_000, 65_000) + # pass selected port to the TCP thread, in order to listen on the same port + # thread in the background as daemon + th = threading.Thread(target=tcp_handler, args=(client_port,), daemon=True) + th.start() + multicast_handler(port) + th.join() -finally: - print('closing socket') - sock.close() - # sock_tcp.close()