Skip to content
70 changes: 70 additions & 0 deletions src/client/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import socket
import threading
import sys

# Wait for incoming data from server
# .decode is used to turn the message in bytes to a string
def receive(socket, stop_event):
while not stop_event.is_set():
try:
data = b''
while True:
chunk = socket.recv(4096)
data += chunk
if len(chunk) < 4096:
break
if data and len(data) > 0:
print(str(data.decode('utf-8')))
except (socket.error, ConnectionResetError) as e:
print("You have been disconnected from the server. Error: " + str(e))
break

# Get host and port
host = input("Host: ")
port = int(input("Port: "))
name = input("Enter your name: ")

# Attempt connection to server
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
sock.sendall(str.encode(name))
except (socket.error, ConnectionRefusedError) as e:
# Create new thread to wait for data
stop_event = threading.Event()
receiveThread = threading.Thread(target=receive, args=(sock, stop_event))
receiveThread.start()
# Send data to server
try:
while True:
message = input()
if not message: # Allow clean exit on empty input
break
sock.sendall(str.encode(message))
except (socket.error, ConnectionResetError) as e:
print(f"Connection error: {e}")
finally:
print("Closing connection...")
stop_event.set() # Signal receive thread to stop
receiveThread.join()
except (socket.error, ConnectionRefusedError) as e:
print("Could not make a connection to the server. Error: " + str(e))
input("Press enter to quit")
sys.exit(0)

# Send data to server
# str.encode is used to turn the string message into bytes so it can be sent across the network
# Setup clean exit
try:
while True:
message = input()
if not message: # Allow clean exit on empty input
break
sock.sendall(str.encode(message))
except (socket.error, ConnectionResetError) as e:
print(f"Connection error: {e}")
finally:
print("Closing connection...")
stop_event.set() # Signal receive thread to stop
sock.close()
receiveThread.join()
108 changes: 108 additions & 0 deletions src/server/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import socket
import sys
import threading

# Variables for holding information about connections
connections = []
total_connections = 0
_connections_lock = threading.Lock()

# Client class, new instance created for each connected client
# Each instance has the socket and address that is associated with items
# Along with an assigned ID and a name chosen by the client
class Client(threading.Thread):
def __init__(self, socket, address, id, name, signal):
threading.Thread.__init__(self)
self.socket = socket
self.address = address
self.id = id
self.name = name
self.signal = signal

def __str__(self):
return str(self.id) + " " + str(self.address) + " " + self.name

# Attempt to get data from client
# If unable to, assume client has disconnected and remove him from server data
# If able to and we get data back, print it in the server and send it back to every
# client aside from the client that has sent it
# .decode is used to convert the byte data into a printable string
def run(self):
while self.signal:
try:
data = b''
while True:
chunk = self.socket.recv(4096)
data += chunk
if len(chunk) < 4096:
break
except (socket.error, ConnectionResetError) as e:
print("Client " + str(self.address) + " has disconnected")
self.signal = False
with _connections_lock:
connections.remove(self)
break
if data != b"":
print("ID " + str(self.id) + ": " + str(data.decode('utf-8')))
with _connections_lock:
for client in connections:
if client.id != self.id:
client.socket.sendall(data)

# Wait for new connections
def newConnections(socket):
global total_connections
while True:
try:
sock, address = socket.accept()
name = sock.recv(1024).decode('utf-8') # Receive the name from the client
client = Client(sock, address, total_connections, name, True)
with _connections_lock:
connections.append(client)
total_connections += 1
client.start()
print("New connection at ID " + str(client))
except (socket.error, ConnectionError) as e:
print(f"Error accepting connection: {e}")
continue

def main():
# Get host and port
host = input("Host: ")
if not host:
host = "localhost"
try:
port = int(input("Port: "))
if not (1024 <= port <= 65535):
raise ValueError("Port must be between 1024 and 65535")
except ValueError as e:
print(f"Invalid port: {e}")
sys.exit(1)

# Create new server socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind((host, port))
sock.listen(5)
except socket.error as e:
print(f"Failed to bind socket: {e}")
sys.exit(1)

# Create new thread to wait for connections
newConnectionsThread = threading.Thread(target=newConnections, args=(sock,))
newConnectionsThread.start()
try:
stop_event = threading.Event()
stop_event.wait() # Wait indefinitely until KeyboardInterrupt
except KeyboardInterrupt:
print("\nShutting down server...")
# Signal all clients to stop
with _connections_lock:
for client in connections:
client.signal = False
client.socket.close()
client.join()
sock.close()

if __name__ == "__main__":
main()