Making a multi-thread server for a chat room, destination client sockets selection

Hello to the community! I’m new here and with a bit of dispair mixed to curiosity.
I was asked by one of my professors at university to make, or at least try to, a small server that can provide a simple chat room service. What I’m interested in, specifically, is if there’s a way, a funcion or a keyword, that allows me to detect what was the last client to have written a message and so to exclude it from the recipients.

The way I’m asking this question may seem like I’m new to programming, and in fact it’s true.
I have had a look at the documentation of the modules I’m using, socket and threading, but, maybe because of my limited experience, I could not find anything that was fitting for my purposes.

Thank you :v:

That’s becauseneither socket nor threading track clients of higher level
things (the chat stuff). But a socket knows what it talks to at an IP
level.

If you’re just exchanging messages (receive message, copy to every
other socket) you’ve got two approaches:

The easy one:

Keep an array of the active sockets, copy to every socket if that socket
is not the socket you received on. That sounds like like your
requirement, but: every connection is a “socket” object. You can see if
a reference to a socket is a particular reference with Python’s “is”
operator:

my_socket = ... the socket you're using when you receive the message 
...
for client_socket in the_client_sockets:
    if client_socket is not my_socket:
        ... copy the message to client_socket ...

That works if your chat server is using stream (usually TCP
connections), where there’s a stable “connection” socket for each
client. Just record the socket in “the_client_sockets” when the server
accepts a new connection.

The trickier one:

If you’re using datagrams (usually UDP packets) where you have a server
socket accepting UDP datagrams from clients there’s no connection;
instead you probably receive messages with socket.recvmsg(), which
returns a tuple of values:

https://docs.python.org/3/library/socket.html#socket.socket.recvmsg

From the docs: “The return value is a 4-tuple: (data, ancdata,
msg_flags, address).” The “address” is the client address - keep a list
of these and use it to decide which client to skip when copying the
message around.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Thank you :smiley: . I’ll post some update eventually.

Cheers,
Lorenzo

@cameron I have a couple of questions. Fisrt, I’m going to use TCP. These variables, the_client_sockets and client_socket are handled by the server, right? And what about my_socket? Is it identified as one of the threads opened by the server? Is it a variable that’s going to be in the client’s script?

@cameron I have a couple of questions. Fisrt, I’m going to use TCP.

That’s pretty normal.

These variables, the_client_sockets and client_socket are handled by
the server, right?

Yes, but they’re just made up variable names. You need to create and
populate them yourself. But the work all happens in the server.

And what about my_socket? Is it identified as one of the threads opened
by the server?

Yes, but again it is a made up name; you’ll have to obtain it in your
connection handler.

Is it a variable that’s going to be in the client’s script?

No. The client connects, but doesn’t know about any other clients. (Or
more pedanticly, it needn’t: serious chat services often do things like
let you know which other clients are online, etc, but you don’t want any
of that complication yet.)

If you’re using TCP and you need the server process to track all the
client connections you want to use a ThreadingTCPServer:

https://docs.python.org/3/library/socketserver.html#socketserver.ThreadingTCPServer

Instructions are at the link above. You want to subclass
StreamRequestHandler and have your .handle method record the client
socket in the_client_sockets, which you need to create. (An empty global
list or set would do.)

Because you’re sharing the the_client_sockets, you want the threading
server instead of the forking server because forked servers have their
own memory and are not sharing information (without a lot of extra
work).

Note that the handler class, in addition to recording itself (and/or the
client socket) in the_client_sockets, also probably wants a distinct
send_message() method to write a message back to the client. That’s what
allows you to write to other clients from a particular client’s handler.

Cheers,
Cameron Simpson cs@cskk.id.au

Here’s the code I’ve come up with so far:

import socket
import threading
#import time
import copy

s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_address=("192.168.1.8", 22000)
s.bind(server_address)
s.listen(1)

connected_clients=[] #port numbers!!
#msg_count=0
ls_list=[]

#def time_ser(hr, scs):
    #hr=time.localtime()[3]
    #scs=time.localtime()[5]
    #h_ser=hr+scs/3600

def handle_client(c):
    while True:
        data=c.recv(1024)
        if data.decode()=="quit":
            c.close()
        else:
        #msg_count+=1
        #msg_env=[client_address[1], data, time_ser] #enveloped
            ls_list.append(client_address[1])

            if len(ls_list)>5:
                del ls_list[0:5]
            else:
            
                last_sending=sorted(ls_list, reverse=True)[0]
        
                for i in connected_clients:
                    if i is not last_sending:
                
 while True:
     connection, client_address=s.accept()
     connected_clients.append(client_address[1])
     t=threading.Thread(target=handle_client, args=(connection, ))
     t.start()

I have some things to consider.
First is that I’m in some way stuck because I have seen videos about the copy module and it’s methods but I don’t have any ideas about how to actually copy the message to the clients which are not last_sending.
Then, I’ve figured out why do I need the for loop to determine who’s last_sending but it seems more reasonable to me to use i instead of client_address[1], because that’s something that changes every time and should be an integer, being my port number.

Here’s the code I’ve come up with so far:

Thanks. Some comments:

s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address=(“192.168.1.8”, 22000)

This is a LAN address. So clients could connect from other hosts on your
LAN. So saving just the port number is not precise enough.

If you bind to 127.0.0.1 instead, only local clients can connect.
Alternatively, save the whole address instead of just the port.

connected_clients= #port numbers!!
#msg_count=0
ls_list=

def handle_client(c):
while True:
data=c.recv(1024)
if data.decode()==“quit”:
c.close()
else:
ls_list.append(client_address[1])
if len(ls_list)>5:
del ls_list[0:5]

Why limit the list to 5? In fact, why delete the leading 5?

If you’re concerned about clients disconnecting, why not remove a client
at the end of handle_client? Then you could have your loop exit on EOF
from the client (probably len(data)==0, or “not data” as we tend to
write that). You can use “break” to exit the loop if this happens.

           else:
               last_sending=sorted(ls_list, reverse=True)[0]

This isn’t the most recent sender, it is the highest numbered port.

               for i in connected_clients:
                   if i is not last_sending:

You want != here, not “is not”. “==” and “!=” test value equality. “is”
and “is not” tend “this is the same object”. Python happens to make
singletons for small integers, so if 5==5 then also “5 is 5”. But for
large integers this isn’t the case, and it is a bad practice anyway. You
can about the value, not whether the same object is used to represent
it. So test the value (“!=”).

while True:
    connection, client_address=s.accept()
    connected_clients.append(client_address[1])

As mentioned, why not save the whole address instead of just the port?

    t=threading.Thread(target=handle_client, args=(connection, ))
    t.start()

Superficially fine, if incomplete.

I have some things to consider.
First is that I’m in some way stuck because I have seen videos about the copy module and it’s methods but I don’t have any ideas about how to actually copy the message to the clients which are not last_sending.

Forget the copy module. It is all about copying data structures. “Copy
the message to a client” just means “write the message to the client
socket”.

Then, I’ve figured out why do I need the for loop to determine who’s last_sending but it seems more reasonable to me to use i instead of client_address[1], because that’s something that changes every time and should be an integer, being my port number.

Why do you care who’s last sending? I thought you only cared whether a
client was the “current” client (which just received a message).

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like