Simple Chat App using Python
A simple command-line, TCP — chat application, implemented in Python, utilizes socket programming and threading to establish a server, allowing multiple clients to join and send message to each other.
GitHub: https://github.com/OmerGnscr/Simple-Chat-App/
For better understanding:
Command-line: The interface where clients interact with the application by typing messages into a console.
TCP: Transmission Control Protocol is a transport protocol that ensures reliable transmission of packets. This project uses TCP to establish connections between clients and server.
Socket: A network node that serves as a endpoint for sending and receiving data in a network. Sockets are used to create connections between clients and the server in this project.
Threading: A technique in programming that allows a program to perform multiple tasks concurrently. In this project, threading is used to handle multiple client connections simultaneously.
This project also has an logging and command-execution features. Clients can run specific commands such as listing online users or clearing the chat. And, All the messages ,that are sent by clients, are logged by the server in a server.log file.
This project consists of 3 python scripts;
server.py, client.py and commands.py
Let’s breake down the server script and analyze it:
#!/usr/bin/python3
import socket
import sys
import threading
import commands
from datetime import datetime
if len(sys.argv) != 3:
print ("Usage: ./server.py <IP> <PORT>")
exit()
HOST = str(sys.argv[1])
PORT = int(sys.argv[2])
if PORT < 0 or PORT > 65535 or not isinstance(PORT, int):
print("Port number must be between 0 and 65535")
exit()
clients = []
usernames = []
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
print(f'Server listening on {HOST}:{PORT}')
def handle_client(client, username):
while True:
try:
data = client.recv(1024)
if not data:
break
message = data.decode('utf-8')
log(message, username)
if message.startswith('/'):
message = message.lower()
commands.handle_command(client, message[1:], clients, usernames)
else:
broadcast(f"{username}: {message}")
except Exception as e:
print(f"Error : {e}")
break
remove(client, username)
def broadcast(message):
for client in clients:
client.send(message.encode('utf-8'))
def remove(client, username):
if client in clients:
clients.remove(client)
if username in usernames:
usernames.remove(username)
def log(message,username):
current_datetime = datetime.now()
current_time = current_datetime.strftime("%H:%M:%S")
try:
with open("server.log", "a") as f:
f.write(f"[{current_time}] - {username}: {message}\n")
except IOError as e:
print(f"An error has occured during logging: {e}")
try:
while True:
client, addr = server.accept()
client.send("Welcome!".encode('utf-8'))
username = client.recv(1024).decode('utf-8')
clients.append(client)
usernames.append(username)
broadcast(f"{username} joined!")
print(f'User {username} connected from {addr}')
client_handler = threading.Thread(target=handle_client, args=(client, username))
client_handler.start()
except KeyboardInterrupt:
print("\nServer stopped.")
server.close()
The shebang line, in Unix-like OS ,specifies that the script should be executed using the Python 3 located at /usr/bin/python3.
Importing necessary libraries for the server scripts.
- socket is used for creating a new socket that connects to a specific IP address.
- sys is used for command line arguments.
For example$~ argv[0] argv[1] argv[2]
- threading used for allowing multiple clients to connect to the server.
- commands is a custom module for handling commands
- datetime is used for logging operations.
if len(sys.argv) != 3:
print ("Usage: ./server.py <IP> <PORT>")
exit()
HOST = str(sys.argv[1])
PORT = int(sys.argv[2])
if PORT < 0 or PORT > 65535 or not isinstance(PORT, int):
raise ValueError("Port number must be between 0 and 65535")
First if statement is used to check if the correct number of CLI arguments is used. If not, prints usage instructions and exits.
First argument (sys.argv[1]) and second argument (sys.argv[2]) in command-line is used to retrieve HOST and PORT, respectively.
Second if statement is used for checking the valid port range and if is an integer. If not, it prints out the correct usage and exits.
clients = []
usernames = []
These two lists are used to keep track of connected clients and their usernames.
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
print(f'Server listening on {HOST}:{PORT}')
A socket object, named ‘server’ is created using IPv4 (AF_INET) and TCP (SOCK_STREAM). The server then binds to given HOST and PORT.
Then, it starts to listening for incoming connections.
(Official: Enable a server to accept connections)
def handle_client(client, username):
while True:
try:
data = client.recv(1024)
if not data:
break
message = data.decode('utf-8')
log(message, username)
if message.startswith('/'):
message = message.lower()
commands.handle_command(client, message[1:], clients, usernames)
else:
broadcast(f"{username}: {message}")
except Exception as e:
print(f"Error : {e}")
break
remove(client, username)
handle_client() function is used for managing the communication for client.
In infinite loop, while True:, it listens for incoming messages from client.
recv() is used to receive data from the socket. The maximum amount of data to be received at once is specified by bufsize (1024 for this code.)
decode(’utf-8’) is used to convert a sequence of bytes (data) into a string (message).
log() is used for storing the message and the username for logging operations.
When message is received, it checks if it start with ‘/’ (is a command) or regular message.
If it is a command, it sends the message to the commands module without ‘/’ at the beginning.
If it is not a command, it broadcasts the message to all the clients.
If an error occurs, error is printed and exits the loop.
When the client disconnects, remove() function removes it from client[] and username[].
def broadcast(message):
for client in clients:
client.send(message.encode('utf-8'))
This function is used to send the message all the users connected. It simply means broadcasting.
def remove(client, username):
if client in clients:
clients.remove(client)
if username in usernames:
usernames.remove(username)
Remove() function is used for removing the client that is disconnected from the server.
def log(message,username):
current_datetime = datetime.now()
current_time = current_datetime.strftime("%H:%M:%S")
try:
with open("server.log", "a") as f:
f.write(f"[{current_time}] - {username}: {message}\n")
except IOError as e:
print(f"An error has occured during logging: {e}")
Log() function is used to storing all the message that are sent in a server.log file. This log file includes current time, username and the message.
try:
while True:
client, address = server.accept()
client.send("Welcome!".encode('utf-8'))
username = client.recv(1024).decode('utf-8')
clients.append(client)
usernames.append(username)
broadcast(f"{username} joined!")
print(f'User {username} connected from {address}')
client_handler = threading.Thread(target=handle_client, args=(client, username))
client_handler.start()
except KeyboardInterrupt:
print("\nServer stopped.")
server.close()
Accept() is used to accept a connection. The socket must be bound to an address and listening for connections.
When client connects, it sends a welcome message and waits for the username.
Then, it appends the client and username into their respective list.
It broadcasts a message to clients that the new user is joined.
A thread is created to handle communications with client.
When keyboard interrupt (CTRL+C) occurs, server stops.
Now, let’s take a look at the client script.
#!/usr/bin/python3
import socket
import threading
import sys
if len(sys.argv) != 3:
print ("Usage: ./client.py <IP> <PORT>")
exit()
HOST = str(sys.argv[1])
PORT = int(sys.argv[2])
if PORT < 0 or PORT > 65535 or not isinstance(PORT, int):
print("Port number must be between 0 and 65535")
exit()
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
client.connect((HOST, PORT))
def receive():
while True:
try:
message = client.recv(1024).decode('utf-8')
print(message)
except:
print("An error occurred!")
client.close()
break
def send():
username = input("Enter your username: ")
client.send(username.encode('utf-8'))
while True:
message = input('')
client.send(message.encode('utf-8'))
receive_thread = threading.Thread(target=receive)
receive_thread.start()
send_thread = threading.Thread(target=send)
send_thread.start()
Connect() is used to connect to a remote socket at address.
def receive():
while True:
try:
message = client.recv(1024).decode('utf-8')
print(message)
except:
print("An error occurred!")
client.close()
break
This function is continously listens for incoming messages and decode it from bytes to strings.
Then prints the ‘message’. If error occurs, it breaks the loop and closes connection.
def send():
username = input("Enter your username: ")
client.send(username.encode('utf-8'))
while True:
message = input('')
client.send(message.encode('utf-8'))
Firstly, this function asks the client for its username and sends to the server.
Then, in infinite loop, it waits for user to send the message. If user provides a message, it sends to the server.
receive_thread = threading.Thread(target=receive)
receive_thread.start()
send_thread = threading.Thread(target=send)
send_thread.start()
Two threads are created for receiving and sending the messages.
It is used for running those functions concurrently.
Lastly, here is a ‘command’ module for executing commands.
import os
def handle_command(client,message, clients, usernames):
if message == 'count':
user_count(client, clients)
elif message == 'help':
help_message(client)
elif message == 'users':
show_users(client, usernames)
elif message == 'clear':
clear_chat(client)
else:
client.send(f"Invalid command.".encode('utf-8'))
def user_count(client, clients):
client.send(f"Number of users online: {len(clients)}".encode('utf-8'))
def show_users(client, usernames):
usr = ", ".join(usernames)
client.send(f"Online users:\n{usr}".encode('utf-8'))
def clear_chat(client):
client.send(b'\033c')
def help_message(client):
help_msg = b"Commands: \n/Count - for number of users in the chat\n/Users - for online users\n/Clear - for clearing chat\n"
client.send(help_msg)
This script is just a simple module to made for users to use commands.
Count used to prompt how many users are in the server.
Help is used to print out how to use commands.
Users is used to print which users are online in the server.
Clear is used to clearing the terminal.