babaliaris
babaliaris

Reputation: 703

Python Sockets, Advanced Chat Box

I want to create a server that handles a lot of clients at the same time (handles: receiving data from clients and sending data to all clients at the same time!!!)

Actually i'm trying to create a chat box. The program will work like this:

1) There's going to be a server which handles the clients.

2) More than one clients can join the server.

3) Clients send messages (Strings) to the server.

4) The server receive's the message from a client and then it send's it to all the clients except the one he got it from.

And this is how the clients will communicate each other. No private messages available. When someone hits the enter all the clients will see the message on their screen.

The client module is easy to make, because the client communicates only with one socket (The Server).

The server module from the other hand is really complicated, i don't know how to do it (I also know about threads).

This is my atempt:

import socket, threading


class Server:

def __init__(self, ip = "", port = 5050):

    '''Server Constructor. If __init__ return None, then you can use
       self.error to print the specified error message.'''

    #Error message.
    self.error  = ""

    #Creating a socket object.
    self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    #Trying to bind it.
    try:
        self.server.bind( (ip, port) )
        pass


    #Failed, because socket has been shuted down.
    except OSError :
        self.error = "The server socket has been shuted down."
        return None

    #Failed, because socket has been forcibly reseted.
    except ConnectionResetError:
         self.error = "The server socket has been forcibly reseted."
         return None


    #Start Listening.
    self.server.listen()


    #_____Other Variables_____#

    #A flag to know when to shut down thread loops.
    self.running = True

    #Store clients here.
    self.clients = []

    #_____Other Variables_____#


    #Start accepting clients.
    thread = threading.thread(target = self.acceptClients)
    thread.start()

    #Start handling the client.
    self.clientHandler()





#Accept Clients.
def acceptClients(self):

    while self.running:
        self.clients.append( self.server.accept() )

    #Close the server.
    self.server.close()




#Handle clients.
def clientHandler(self):

    while self.running:

        for client in self.clients:

            sock = client[0]
            addr = client[1]

            #Receive at most 1 mb of data.
            #The problem is that recv will block the loop!!!
            data = sock.recv(1024 ** 2)

As you can see, i accept clients using a thread so the server.accept() won't block the program. And then i store the clients into a list.

But the problem is on the clientHandler. How am i going to recv from all clients at the same time? The first recv will block the loop!!!

I also tried to start new threads (clientHandlers) for every new client but the problem was the synchronization.

And what about the send? The server must send data to all the clients, so the clientHandler is not yet finished. But if i mix the methods recv and send then the problem become's more complicated.

So what is the proper and best way to do this? I'd like to give me an example too.

Upvotes: 1

Views: 707

Answers (2)

babaliaris
babaliaris

Reputation: 703

This my final program and works like a charm.

Server.py

import socket, select

class Server:

def __init__(self, ip = "", port = 5050):

    '''Server Constructor. If __init__ return None, then you can use
       self.error to print the specified error message.'''

    #Error message.
    self.error  = ""

    #Creating a socket object.
    self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    #Trying to bind it.
    try:
        self.server.bind( (ip, port) )
        pass


    #Failed, because socket has been shuted down.
    except OSError :
        self.error = "The server socket has been shuted down."

    #Failed, because socket has been forcibly reseted.
    except ConnectionResetError:
         self.error = "The server socket has been forcibly reseted."


    #Start Listening.
    self.server.listen()


    #_____Other Variables_____#

    #A flag to know when to shut down thread loops.
    self.running = True

    #Store clients here.
    self.sockets   = [self.server]

    #_____Other Variables_____#


    #Start Handling the sockets.
    self.handleSockets()






#Handle Sockets.
def handleSockets(self):

    while True:
        r, w, x = select.select(self.sockets, [], self.sockets)

        #If server is ready to accept.
        if self.server in r:

            client, address = self.server.accept()
            self.sockets.append(client)


        #Elif a client send data.
        elif len(r) > 0:

            #Receive data.
            try:
                data = r[0].recv( 1024 )


            #If the client disconnects suddenly.
            except ConnectionResetError:
                r[0].close()
                self.sockets.remove( r[0] )
                print("A user has been disconnected forcible.")
                continue

            #Connection has been closed or lost.
            if len(data) == 0:

                r[0].close()
                self.sockets.remove( r[0] )
                print("A user has been disconnected.")


            #Else send the data to all users.
            else:

                #For all sockets except server.
                for client in self.sockets[1:]:

                    #Do not send to yourself.
                    if client != r[0]:
                        client.send(data)


server = Server()
print("Errors:",server.error)

Client.py

import socket, threading

from tkinter import *

class Client:

def __init__(self, ip = "192.168.1.3", port = 5050):

    '''Client Constructor. If __init__ return None, then you can use
       self.error to print the specified error message.'''

    #Error message.
    self.error  = ""

    #Creating a socket object.
    self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    #Trying to bind it.
    try:
        self.server.connect( (ip, port) )
        pass


    #Failed, because socket has been shuted down.
    except OSError :
        self.error = "The client socket has been shuted down."
        return

    #Failed, because socket has been forcibly reseted.
    except ConnectionResetError:
         self.error = "The client socket has been forcibly reseted."
         return

    #Failed, because socket has been forcibly reseted.
    except ConnectionRefusedError:
         self.error = "The server socket refuses the connection."
         return


    #_____Other Variables_____#

    #A flag to know when to shut down thread loops.
    self.running = True

    #_____Other Variables_____#



#Start the GUI Interface.
def startGUI(self):

    #Initialiazing tk.
    screen = Tk()
    screen.geometry("200x100")

    #Tk variable.
    self.msg = StringVar()

    #Creating widgets.
    entry  = Entry( textvariable = self.msg )
    button = Button( text = "Send", command = self.sendMSG )

    #Packing widgets.
    entry.pack()
    button.pack()

    screen.mainloop()



#Send the message.
def sendMSG(self):
    self.server.send( str.encode( self.msg.get() ) )



#Receive message.
def recvMSG(self):

    while self.running:

        data = self.server.recv(1024)

        print( bytes.decode(data) )



#New client.
main = Client()
print("Errors:", main.error)



#Start a thread with the recvMSG method.
thread = threading.Thread(target = main.recvMSG)
thread.start()

#Start the gui.
main.startGUI()

#Close the connection when the program terminates and stop threads.
main.running = False
main.server.close()

The program works fine exactly as i wanted.

But i have some more questions.

r, w, x = select.select(self.sockets, [], self.sockets)

r is a list which contains all the ready sockets. But i did not undesrand what w and x are.

The first parameter is the sockets list, the second the accepted clients and the third parameter what is it? Why am i giving again the sockets list?

Upvotes: 0

Serge Ballesta
Serge Ballesta

Reputation: 148890

Multithreading is great when the different clients are independant of each other: you write your code as if only one client existed and you start a thread for each client.

But here, what comes from one client must be send to the others. One thread per client will certainly lead to a synchronization nightmare. So let's call select to the rescue! select.select allows to poll a list of sockets and returns as soon as one is ready. Here you can just build a list containing the listening socket and all the accepted ones (that part is initially empty...):

  • when the listening socket is ready for read, accept a new socket and add it to the list
  • when another socket is ready for read, read some data from it. If you read 0 bytes, its peer has been shut down or closed: close it and remove it from the list
  • if you have read something from one accepted socket, loop on the list, skipping the listening socket and the one from which you have read and send data to any other one

Code could be (more or less):

    main = socket.socket()  # create the listening socket
    main.bind((addr, port))
    main.listen(5)
    socks = [main]   # initialize the list and optionaly count the accepted sockets

    count = 0
    while True:
        r, w, x = select.select(socks, [], socks)
        if main in r:     # a new client
            s, addr = main.accept()
            if count == mx:  # reject (optionaly) if max number of clients reached
                s.close()
            else:
                socks.append(s) # appends the new socket to the list
        elif len(r) > 0:
            data = r[0].recv(1024)  # an accepted socket is ready: read
            if len(data) == 0:      # nothing to read: close it
                r[0].close()
                socks.remove(r[0])
            else:
                for s in socks[1:]:  # send the data to any other socket
                    if s != r[0]:
                        s.send(data)
        elif main in x:  # close if exceptional condition met (optional)
            break
        elif len(x) > 0:
            x[0].close()
            socks.remove(x[0])
    # if the loop ends, close everything
    for s in socks[1:]:
        s.close()
    main.close()

You will certainly need to implement a mechanism to ask the server to stop, and to test all that but it should be a starting place

Upvotes: 1

Related Questions