import socket
import threading

''' 
    Receive TCP packets using Python (multiple clients, one thread each)

	Author: Tim Pierson, Dartmouth CS60, Fall 2026
            Assisted by Microsoft Copilot
	
	run: python3 receive_tcp_multithreaded.py
	
	send data from netcat in several terminals at once (replace IP address)
	nc -t 127.0.0.1 9090
	then type messages
'''

#define host ip and port
HOST = '0.0.0.0' #'127.0.0.1' #localhost
PORT = 9090

#everything that used to be the body of the while loop now lives in here.
#one thread runs this function per connected client, so the loop below is
#free to go straight back to accept() and pick up the next client
def handle_client(client_socket, addr):
    print(f"Connected by {addr}")
    try:
        while True:
            data = client_socket.recv(1024)
            if not data: #empty bytes means the client sent FIN, not "no data yet"
                break
            print(f"Received from {addr}: {data.decode()}") #decode converts bytes to string
    except ConnectionError: #one client crashing should not take down the server
        print(f"Connection lost: {addr}")
    finally:
        client_socket.close()
        print(f"Disconnected: {addr}")

#create TCP socket listening on ip address and port
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #lets us restart right away instead of waiting out TIME_WAIT
server_socket.bind((HOST, PORT))

#listen for connections
server_socket.listen()
print(f"Server listening on {HOST}:{PORT}...")
try:
    while True:
        client_socket, addr = server_socket.accept() #accept returns a new socket to the client and the clients ip/port
        #daemon=True so ctrl-c kills the server even with clients still connected
        t = threading.Thread(target=handle_client, args=(client_socket, addr), daemon=True)
        t.start()
except KeyboardInterrupt:
    print("\nShutting down.")
finally:
    server_socket.close()