Python Simple Multiperson Chat Room

23 February 2020

Intro :flashlight:

Ever wondered how today’s chat apps are so sophisticated? We have all kinds of features like media transfer, multiperson chat, link previews, stickers, gifs, etc. I’m just wondering how it all started, IRCs. So I tried to make a simple multiperson chat room.

Demo

It’s quite a basic one, but it sure was satisfying to see it work. I also added nicknames support!

The main idea is pretty standard, you get a connection to the server port, you bind that client to another port to make the main port available for the other clients to connect to. After you get an input from one of the clients, broadcast it to all the other clients except the sender, and just continue doing it forever.

Server

Here’s server.py, also downloadable here.

#!/usr/bin/python

import sys
import socket
import select
import threading
from pprint import pprint # var_dump

if len(sys.argv) != 3:
    print "Usage: server.py [server_ip] [server_port]"
    print "  Sets up a basic chat server"
    print "  For localhost connections, set server_ip to 127.0.0.1"
    exit()

bind_ip = sys.argv[1]
bind_port = int(sys.argv[2])
srv_tuple = (bind_ip, bind_port)

server = socket.socket()
server.bind(srv_tuple)
server.listen(5) # 5 max clients

print "[.] Server started"
print "[.] Now listening for clients on (IP: %s | Port: %d)" % srv_tuple

def handle_client(client_socket, client_addr):
    # init / welcome
    client_socket.send("Please enter your nickname")
    nick = client_socket.recv(1048)
    connected_clients_nicks.append(nick)
    client_socket.send("Enjoy your stay %s!\n\n" % nick)
    bc("[connected]", client_socket)

    while True:
        client_socket.send("%s> " % nick)
        msg = client_socket.recv(1048)
        if len(msg):
            print "[.] %s> %s" % (nick, msg)
            bc(msg, client_socket)
        else:
            print "[-] %s disconnected" % (nick)
            bc("[disconnected]", client_socket)
            connected_clients.remove(client_socket)
            connected_clients_nicks.remove(nick)
            client_socket.close()
            return

def bc(msg, sender):
    idx = 0
    for client in connected_clients:
        if client != sender:
            try:
                client.send("\n%s> %s\n%s>" % (
                    connected_clients_nicks[connected_clients.index(sender)], 
                    msg, 
                    connected_clients_nicks[connected_clients.index(client)]
                ))
            except Exception as e:
                print "broadcast exception %s" % (e)
        idx = idx + 1

# server main thread
connected_clients = []
connected_clients_nicks = []
while True:
    # wait for incoming connection
    client,addr = server.accept()
    print "[+] Accepted connection from %s, binded to port %d" % (addr[0], addr[1])

    # add to list
    connected_clients.append(client)

    # create a new thread for this client and start it
    client = threading.Thread(target=handle_client,args=(client,addr,))
    client.start()

The prompts need a little more work too as I’m still trying to figure out the finicky whitespaces and newlines when being sent over the network because they are supposed to be the way to know if a client has disconnected or just spamming the enter key. But they work fine for now. In the end, I solved the problem by editing the client.py making sure that nothing is sent if the stdin input is nothing but whitespaces.

Client

Here’s client.py, also downloadable here.

#!/usr/bin/python

import sys
import socket
import select
import string
from pprint import pprint # var_dump

if len(sys.argv) != 3:
    print "Usage: client.py [server_ip] [server_port]"
    print "  Connects to a basic chat server"
    print "  For localhost connections, set server_ip to 127.0.0.1"
    exit()

target_host = sys.argv[1]
target_port = int(sys.argv[2])
sys.stdin.flush()

srv = socket.socket()

srv.connect((target_host,target_port))

# init / welcome
print srv.recv(1048)
mynick = string.rstrip(sys.stdin.readline())
srv.send(mynick)
print srv.recv(1048)

while True:
    read = []
    read,write,exception = select.select([sys.stdin, srv],[],[])
    
    for stream in read:
        if stream == srv:
            sys.stdout.write(string.rstrip(srv.recv(1048)))
            sys.stdout.write(" ")
            sys.stdout.flush()
        else:
            msg = string.rstrip(sys.stdin.readline())
            sys.stdin.flush()
            srv.send(msg)

Another tricky part is watching 2 input streams, one from the server and another from stdin, but we found the solution: select, which is quite hard to understand at first, but essentially we’re looping over the 2 input streams, watching for new data to come in, and doing a set of instructions on the data based on where it’s from. and now we’ve got a fully working server and client with a good disconnect behavior.

This was quite a large amount of trial and error, but we got there in the end!