Simple Chat Server: A basic chat server using sockets

Chat Server Implementation

The chat server will handle multiple clients, allowing them to send messages to each other. We will use socket for networking and select for managing multiple connections.

Server Code

import socket
import select

# Define constants for server
HOST = '127.0.0.1'  # Localhost
PORT = 12345        # Port to bind the server

# Create a socket object with IPv4 (AF_INET) and TCP (SOCK_STREAM)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the address and port
server_socket.bind((HOST, PORT))

# Enable the server to accept connections (5 is the max number of queued connections)
server_socket.listen(5)

# List of sockets for select.select()
sockets_list = [server_socket]

# Dictionary to keep track of connected clients and their addresses
clients = {}

print(f"Chat server started on {HOST}:{PORT}")

def receive_message(client_socket):
    """
    Receive messages from a client socket.
    """
    try:
        # Receive message header indicating the message length
        message_header = client_socket.recv(10)
        
        # If no data, client has closed the connection
        if not len(message_header):
            return False

        # Decode message header to get the length
        message_length = int(message_header.decode('utf-8').strip())

        # Receive the actual message
        return {'header': message_header, 'data': client_socket.recv(message_length)}

    except:
        # Something went wrong, possibly connection reset
        return False

while True:
    # Use select to wait for activity on the sockets
    read_sockets, _, exception_sockets = select.select(sockets_list, [], sockets_list)

    for notified_socket in read_sockets:
        # If the notified socket is the server socket, it means a new connection
        if notified_socket == server_socket:
            client_socket, client_address = server_socket.accept()

            user = receive_message(client_socket)
            if user is False:
                continue

            sockets_list.append(client_socket)
            clients[client_socket] = user

            print(f"Accepted new connection from {client_address[0]}:{client_address[1]} username:{user['data'].decode('utf-8')}")
        
        else:
            # For any other socket, it means an existing client is sending a message
            message = receive_message(notified_socket)

            if message is False:
                # Client disconnected, cleanup
                print(f"Closed connection from {clients[notified_socket]['data'].decode('utf-8')}")
                sockets_list.remove(notified_socket)
                del clients[notified_socket]
                continue

            user = clients[notified_socket]
            print(f"Received message from {user['data'].decode('utf-8')}: {message['data'].decode('utf-8')}")

            for client_socket in clients:
                if client_socket != notified_socket:
                    client_socket.send(user['header'] + user['data'] + message['header'] + message['data'])

    for notified_socket in exception_sockets:
        sockets_list.remove(notified_socket)
        del clients[notified_socket]

Detailed Explanation

Imports and Constants

import socket
import select

HOST = '127.0.0.1'
PORT = 12345
  • socket: Provides low-level networking interface.
  • select: Monitors multiple sockets, waiting until they become readable, writable, or have an error.

Server Socket Setup

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((HOST, PORT))
server_socket.listen(5)
sockets_list = [server_socket]
clients = {}
print(f"Chat server started on {HOST}:{PORT}")
  • socket.socket(): Creates a new socket using IPv4 and TCP.
  • bind(): Associates the socket with a specific network interface and port.
  • listen(): Enables the server to accept connections; 5 is the maximum number of queued connections.
  • sockets_list: Contains all sockets to monitor with select.
  • clients: Dictionary to store client sockets and their information.

Receiving Messages

def receive_message(client_socket):
    try:
        message_header = client_socket.recv(10)
        if not len(message_header):
            return False
        message_length = int(message_header.decode('utf-8').strip())
        return {'header': message_header, 'data': client_socket.recv(message_length)}
    except:
        return False
  • recv(10): Receives the first 10 bytes from the socket, which contain the message length.
  • message_header.decode('utf-8').strip(): Decodes the header to get the message length.
  • client_socket.recv(message_length): Receives the actual message.

Main Loop

while True:
    read_sockets, _, exception_sockets = select.select(sockets_list, [], sockets_list)

    for notified_socket in read_sockets:
        if notified_socket == server_socket:
            client_socket, client_address = server_socket.accept()
            user = receive_message(client_socket)
            if user is False:
                continue
            sockets_list.append(client_socket)
            clients[client_socket] = user
            print(f"Accepted new connection from {client_address[0]}:{client_address[1]} username:{user['data'].decode('utf-8')}")
        
        else:
            message = receive_message(notified_socket)
            if message is False:
                print(f"Closed connection from {clients[notified_socket]['data'].decode('utf-8')}")
                sockets_list.remove(notified_socket)
                del clients[notified_socket]
                continue

            user = clients[notified_socket]
            print(f"Received message from {user['data'].decode('utf-8')}: {message['data'].decode('utf-8')}")
            for client_socket in clients:
                if client_socket != notified_socket:
                    client_socket.send(user['header'] + user['data'] + message['header'] + message['data'])

    for notified_socket in exception_sockets:
        sockets_list.remove(notified_socket)
        del clients[notified_socket]
  • select.select(): Monitors sockets for readability.
  • Accept New Connections: If notified_socket is the server socket, accept new connections.
  • Receive and Broadcast Messages: If notified_socket is a client socket, receive the message and broadcast it to all other clients.
  • Exception Handling: Remove sockets that have errors.

Client Implementation

The client will connect to the server, send messages, and receive messages from other clients.

Client Code

import socket
import threading

HOST = '127.0.0.1'
PORT = 12345

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))

username = input("Enter your username: ")
username_header = f"{len(username):<10}".encode('utf-8')
client_socket.send(username_header + username.encode('utf-8'))

def receive_messages():
    while True:
        try:
            while True:
                username_header = client_socket.recv(10)
                if not len(username_header):
                    print("Connection closed by the server")
                    return
                username_length = int(username_header.decode('utf-8').strip())
                username = client_socket.recv(username_length).decode('utf-8')

                message_header = client_socket.recv(10)
                message_length = int(message_header.decode('utf-8').strip())
                message = client_socket.recv(message_length).decode('utf-8')

                print(f"{username}: {message}")
        except Exception as e:
            print("Error receiving message:", str(e))
            client_socket.close()
            break

def send_messages():
    while True:
        message = input()
        if message:
            message_header = f"{len(message):<10}".encode('utf-8')
            client_socket.send(message_header + message.encode('utf-8'))

receive_thread = threading.Thread(target=receive_messages)
receive_thread.start()

send_thread = threading.Thread(target=send_messages)
send_thread.start()

Detailed Explanation

Imports and Constants

import socket
import threading

HOST = '127.0.0.1'
PORT = 12345
  • socket: Provides low-level networking interface.
  • threading: Enables concurrent execution for sending and receiving messages.

Client Socket Setup

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))
  • socket.socket(): Creates a new socket using IPv4 and TCP.
  • connect(): Connects to the server specified by HOST and PORT.

Username Handling

username = input("Enter your username: ")
username_header = f"{len(username):<10}".encode('utf-8')
client_socket.send(username_header + username.encode('utf-8'))
  • Prompt for Username: Asks the user for a username.
  • username_header: Creates a header indicating the length of the username.
  • send(): Sends the header and the username to the server.

Receiving Messages

def receive_messages():
    while True:
        try:
            while True:
                username_header = client_socket.recv(10)
                if not len(username_header):
                    print("Connection closed by the server")
                    return
                username_length = int(username_header.decode('utf-8').strip())
                username = client_socket.recv(username_length).decode('utf-8')

                message_header = client_socket.recv(10)
                message_length = int(message_header.decode('utf-8').strip())
                message = client_socket.recv(message_length).decode('utf-8')

                print(f"{username}: {message}")
        except Exception as e:
            print("Error receiving message:", str(e))
            client_socket.close()
            break
  • Receive and Print Messages: Continuously receives and prints messages from the server.
  • recv(10): Receives the first 10 bytes, which contain the username/message length.
  • Decode and Print: Decodes and prints the username and message.

Sending Messages

def send_messages():
    while True:
        message = input()
        if message:
            message_header = f"{len(message):<10}".encode('utf-8')
            client_socket.send(message_header + message.encode('utf-8'))
  • Input and Send Messages: Continuously takes input from the user and sends it to the server.
  • message_header: Creates a header indicating the length of the message.
  • send(): Sends the header and the message to the server.

Threads

receive_thread = threading.Thread(target=receive_messages)
receive_thread.start()

send_thread = threading.Thread(target=send_messages)
send_thread.start()
  • threading.Thread: Creates threads for receiving and sending messages.
  • start(): Starts the threads.

By following this detailed implementation and explanation, you can set up a basic chat server and client using Python sockets, allowing multiple clients to communicate with each other in real time.