Hello, i am trying to implement a simple multi-client chat using threads.
This is my code:
Server:
import datetime
import socket
import threading
import random
import datetime
client_list = []
def handle_client_chat(client_socket):
while True:
data = client_socket.recv(1024).decode()
time = datetime.datetime.now()
current_time = f"{time.year}-{time.month}-{time.day} {time.hour}:{time.minute}:{time.second}"
msg = current_time + data
port = client_socket.getpeername()[1]
msg = f"{current_time} + {port} : {data}"
for client in client_list:
print(current_time)
client.send(msg.encode())
if data == "EXIT":
break
client_socket.close()
client_list.remove(client_socket)
def main():
server_socket = socket.socket()
server_socket.bind(("0.0.0.0", 852))
server_socket.listen()
thread_list = []
print("Server is on")
while True:
(client_socket, client_address) = server_socket.accept()
print(f"Connected to client ")
client_list.append(client_socket)
print(client_list)
thread = threading.Thread(target=handle_client_chat, args=(client_socket, ))
thread.start()
thread_list.append(thread)
if __name__ == '__main__':
main()
Client Code:
# This is a sample Python script.
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
import threading
import socket
import msvcrt
def receive_messages(client_socket):
while True:
data = client_socket.recv(1024).decode()
print(data)
def send_messages(client_socket):
while True:
client_input = input()
client_socket.send(client_input.encode())
if client_input == "EXIT":
break
def main():
# Use a breakpoint in the code line below to debug your script.
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 852))
recv_thread = threading.Thread(target=receive_messages, args=(client_socket, ))
send_thread = threading.Thread(target=send_messages, args=(client_socket, ))
recv_thread.start()
send_thread.start()
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
main()
The problem is if for instance client 1 is typing a message and receives at that time a message from client2, it will print the message of client2 on top of the message client1 is currently typing, making it difficult for client1 to complete his message.
How can this be fixed?
Thank you
That’s a tricky one. You basically have two choices here: Either your code is really simple and easy, but has messy edge cases; or your code is much more complex, but it looks better to the user. Obviously if you were trying to sell this to people, you’d choose the second option!
You don’t currently have any kind of protocol. Whatever bytes come from the socket, you decode and emit. That’s probably going to work on localhost, since you’re sending small amounts of information and it’s going to arrive in a few microseconds; but in general, this has a lot of ways that it can break. Instead, I would recommend designing your code (both server and client) to always send a message followed by an “end of message” marker (such as b"\n"), and to always receive data until this marker before outputting anything.
Once that’s sorted out (it’ll be a little bit of work to set that up properly - for example, you’ll need proper buffered input everywhere - but believe me, it’s worth it), you can then look at ways to improve the display. But that’s where things get trickier. Your two threads don’t know about each other. In theory, what you could do would be to have your input() function understand that it’s just been interrupted with an output message, and clear itself off. I don’t know if you can do that with Python’s default input() function though, so you may need to reimplement that yourself.
One small thing you could do, though, would be to purge the half-written message from the screen. Try printing out "\x1b[1K\r" immediately before the message, and see whether that’s an improvement. It won’t look perfect, but maybe it’d be better.
Another option to consider would be using Tkinter to manage your user interface. More work, more things to learn, but a better result. This would most likely NOT work properly with threads, though, so you would have to first switch from threads to async I/O (which, for something like this, is probably better anyway).
Excellent idea! That will immediately solve your current issues - but it will bring its own new issues. Have you ever used tkinter before? If not, I recommend setting your current project aside and making a non-networked and single-threaded “stub” project to get to know tkinter before merging the two. Have something that’s like a solo chat program - you can type messages and they appear to yourself, but there’s no server and nobody else to chat with. [1] That’ll get you familiar with tkinter and what it requires. OTOH, if you’ve already used tkinter, you can skip this step.
In general, GUI toolkits don’t like threads. [2] So usually what you have to do is have one thread dedicated to the GUI event loop, and then other threads for other tasks like sending and receiving messages. Those other threads aren’t allowed to directly work with the GUI though, they have to send a message to the main thread and ask it to do the GUI work. It’s a bit of hassle.
But there’s an alternative: Asynchronous I/O. In Python, you have a module called asyncio, although a lot of the concepts are broader and more general than just that module. The basic idea is that you have a single loop that goes “Lemme know when there’s ANY interesting thing - a button click, a key press, a message from the server, room to send a message to the server, ANYTHING”. If done right, this can be far more scaleable than threads, although it’s limited to a single CPU core (which won’t be an issue for a chat program).
You might think that this is unrealistic, but I have a Twitch channel bot that manages a web server (for its config interface), multiple IRC connections, a GUI (for admin management), outgoing API requests, timer ticks, websockets, all kinds of things - all through async I/O. For something like you’re doing, it would be a wonderfully clean way to organize everything.
When a client receives a message while they’re typing, the message output interrupts the input prompt. To fix this, you can use a library like curses or readline to handle better input and output separation, or you can implement a custom terminal-based interface.
import threading
import socket
import sys
def receive_messages(client_socket):
while True:
try:
data = client_socket.recv(1024).decode()
if not data:
break
sys.stdout.write("\r" + data + "\n> ") # Print the received message
sys.stdout.flush() # Refresh to display the new message
except:
break
def send_messages(client_socket):
while True:
try:
sys.stdout.write("> ") # Display prompt for input
sys.stdout.flush()
client_input = input() # Get user input
client_socket.send(client_input.encode())
if client_input.upper() == "EXIT":
break
except:
break
client_socket.close()
def main():
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 852))
recv_thread = threading.Thread(target=receive_messages, args=(client_socket,))
send_thread = threading.Thread(target=send_messages, args=(client_socket,))
recv_thread.daemon = True # Exit thread when main program ends
recv_thread.start()
send_thread.start()
send_thread.join() # Wait for the send thread to finish
if __name__ == '__main__':
main()