Chat rooms are a popular way for people to communicate and interact with each other in real time over the Internet. In this article, we will explore how to build a simple chat room using Python, specifically focusing on the socket and threading modules. We will examine the provided code that implements a server and client for the chat room and explain each of the individual functions involved.
Introduction to Socket and Threading
Socket
Sockets are endpoints for communication between two machines over a network. They allow programs to send and receive data across the network. In our chat room implementation, we will use sockets to establish connections between the server and clients.
Python's
socket
module provides a low-level interface for socket programming. It offers various functions and methods to create and manipulate sockets. We will utilize thesocket.socket
class to create sockets and perform socket-related operations.Threading
Threading is a technique that allows concurrent execution of multiple threads within a single program. Threads are lightweight and independent units of execution that can perform tasks simultaneously. In our chat room implementation, we will use threading to handle multiple client connections and enable concurrent communication.
Python's
threading
module provides a high-level interface for creating and managing threads. It offers theThread
class, which allows us to define thread functions and start them concurrently.
Server Implementation
Initialization
The
Server
class initializes various attributes such ashost
,port
,server
socket, a list of disconnected keywords, an empty list of clients, an empty list of nicknames, and a logger for logging server events.
import socket
import threading
import structlog
class Server:
def __init__(self):
self.host = socket.gethostbyname(socket.gethostname())
self.port = 5051
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.DISCONNETED = ['q', 'exit', 'close', 'disconnect']
self.clients = []
self.nicknames = []
self.logger = structlog.get_logger()
Broadcasting Messages
The
broadcast
method sends a message to all clients in the chat room except the client who sent the message. Thebroadcast
method iterates over the list of connected clients and sends the message to each client except the client who sent the message.
def broadcast(self, msg, client=None):
for _client in self.clients:
if _client != client:
_client.send(msg)
Starting the Server
The
start_server
method first binds the server socket to the specified host and port using thebind
method. Then it starts listening for incoming connections using thelisten
method. Finally, it calls thelisten
method to handle incoming requests.
def start_server(self):
self.server.bind((self.host, self.port))
self.server.listen()
self.logger.info(f'Server is listening on {self.host}:{self.port}')
self.listen()
Handling Client Connections
The
handle_client
method runs in a loop and continuously receives messages from the client using therecv
method. If the received message is one of the disconnected keywords, it sets theis_connected
flag toFalse
and breaks out of the loop.Otherwise, it broadcasts the received message to all other clients using the
broadcast
method. Additionally, it logs the total number of active connections using thethreading.activeCount()
function.In case of any exceptions during message reception, the client is removed from the list of clients, the socket is closed, and the associated nickname is removed from the list of nicknames. Finally, the method breaks out of the loop.
def handle_client(self, client, addr):
is_connected = True
while is_connected:
try:
msg = client.recv(1024)
if msg in self.DISCONNETED:
is_connected = False
break
self.broadcast(msg, client)
self.logger.info("Total Connections: {}".format(threading.activeCount() - 1))
except:
index = self.clients.index(client)
self.clients.remove(client)
client.close()
nickname = self.nicknames[index]
self.broadcast('{} left!'.format(nickname).encode('ascii'), client)
self.nicknames.remove(nickname)
break
Listening for Connections
The
listen
method runs in an infinite loop, accepting incoming client connections using theaccept
method of the server socket.Once a connection is established, it sends the message 'GET_NAME' to the client using the
send
method to request the nickname of the user. Then it receives the nickname from the client using therecv
method and decodes it.The received nickname is added to the list of nicknames, and the client socket is added to the list of clients. A welcome message is broadcasted to all clients to notify that a new user has joined. Additionally, a connection message is sent to the newly connected client.
Finally, a new thread is started using the
threading.Thread
class, which calls thehandle_client
method to handle the client's messages.
def listen(self):
while True:
client, addr = self.server.accept()
client.send('GET_NAME'.encode('ascii'))
self.nickname = client.recv(1024).decode('ascii')
self.nicknames.append(self.nickname)
self.clients.append(client)
self.logger.info("Welcome {} to the chatroom".format(self.nickname))
self.broadcast("{} joined!".format(self.nickname).encode('ascii'), client)
client.send("{} connected to server!".format(self.nickname).encode('ascii'))
self.logger.info("Starting thread for {}".format(self.nickname))
threading.Thread(target=self.handle_client, args=(client, addr)).start()
Client Implementation
Now let's move on to the client-side implementation of our chat room.
Initialization
The
Client
class initializes various attributes such as the client's nickname (name
), the host IP address, the port number, the client socket (client
), and a logger for logging client events.
import socket
import threading
import structlog
class Client:
def __init__(self, nickname):
self.name = nickname
self.host = socket.gethostbyname(socket.gethostname())
self.port = 5051
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.connect((self.host, self.port))
self.logger = structlog.get_logger()
Receiving Messages
The
receive
method runs in a loop and continuously receives messages from the server using therecv
method. If the received message is 'GET_NAME', it sends the client's nickname to the server using thesend
method.Otherwise, it prints the received message to the console. If any exceptions occur during message reception, the client socket is closed, and the method breaks out of the loop.
def receive(self):
while True:
try:
message = self.client.recv(1024).decode('ascii')
if message == 'GET_NAME':
self.client.send(self.name.encode('ascii'))
else:
print(message)
except:
print("An error occurred!")
self.client.close()
break
Writing Messages
The
write
method runs in a loop and prompts the user to input a message. It constructs a message string using the client's nickname and the input message. Then it sends the message to the server using thesend
method.
def write(self):
while True:
message = '{}: {}'.format(self.name, input(''))
self.client.send(message.encode('ascii'))
Connecting to the Server
The
connect_to_server
method starts two threads, one for receiving messages (receive
) and one for writing messages (write
), enabling concurrent communication between the client and the server.
def connect_to_server(self):
receive_thread = threading.Thread(target=self.receive)
receive_thread.start()
write_thread = threading.Thread(target=self.write)
write_thread.start()
Putting It All Together
In the above code, the user is prompted to enter their name, which is then used to create a
Client
instance. Theconnect_to_server
method is called on the client instance to establish a connection with the server and start the message-receiving and writing threads.
if __name__ == '__main__':
name = input("Enter your name: ")
client = Client(name)
client.connect_to_server()
Note:- We need to run multiple instances of the `client.py` in different terminals.
Output
Starting the server.
Connecting clients.
Client - A
Client - B
Sending Messages.