Q:

Keeping track of clients across multiple servers

I've been designing a chat system for fun over the past few months and I can't find much on load balancing..
So far my architecture consists of a WebSocket server, though the websocket layer will be left out of this topic for simplicity; a MySQL database to store user accounts and chat information; a php-orientated website running on nginx.
I have considered using memcached to keep a list of chats with a reference to each connected client, but I am unsure of how to go about a messaging/queue system to tell other connected clients when a message is sent or a user has joined/quit (Redis?).
Ultimately, this concurrency issue has other potential flaws i.e, should I abstract the processing layer from the socket layer, and in the processing layer, not worry about whether the other clients disconnect during the processing? Should I let the socket layer handle that?
With my memcached example, I could store all relevant client information within that ramdisk and request/update it as I see fit.. Would that be an acceptable way of going about it?
Preferably, I would like to read on some material and figure out how to do this on my own rather than just getting the answer from somebody here and I would like to be able to use this as a lesson on scalability in the future if I were to design something like this again.

A:

Here's a test server I made
import multiprocessing
import socket, select
import redis, json

'''
'' The basic idea of this server is to have cores-1 processes running to munch data
'' and one "master" process handling all of the client connections and what not.
''
'' Scalability is simple, treat any other process as another server and there will be
'' no cross-coding required '''

'''
'' Sending data
''  master_pipe.send( {"__id__": "SEND_DATA", "fileno": "417", "data": "3025561204"} )
''
'' Closing a socket
''  master_pipe.send( {"__id__": "CLOSE_SOCKET", "fileno": 417} ) '''
def Worker(worker_index, worker_queue, master_pipe):
        memory = redis.StrictRedis(host = "127.0.0.1", port = 6379)

    #try:
        for client_id, *args in iter(worker_queue.get, None):
            client = json.loads(memory.get("server0:client-" + str(client_id)).decode("utf-8"))
            if not client:
                continue

            #print("NOPE", args)

            if args[0][:5] == "join:":
                client["chat"] = str(args[0][5:])
                memory.set("server0:client-" + str(client_id), json.dumps(client).encode("utf-8", "ignore"))
                memory.lpush("chat:" + str(args[0][5:]), client["__id__"])

            elif args[0][:7] == "online:":  
                #print(client)
                if "chat" in client:
                    print(memory.lrange("chat:" + client["chat"], 0, -1))

    #except Exception as e:
    #   #print(e)

def Master(master_pipe, workers):
    memory = redis.Redis(host = "127.0.0.1", port = 6379)
    memory.delete("clients")

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    server.bind(("0.0.0.0", 7777))
    server.listen(socket.SOMAXCONN)

    epoll = select.epoll()
    epoll.register(server.fileno(), select.EPOLLIN)
    epoll.register(master_pipe.fileno(), select.EPOLLIN)

    sockets, i = {}, 0
    while True:
        for fileno, e_bits in epoll.poll():
            try:
                if fileno == server.fileno():
                    sock, (addr, port) = server.accept()
                    sockets[sock.fileno()] = sock
                    epoll.register(sock.fileno(), select.EPOLLIN | select.EPOLLHUP)

                    client_object = {"__id__": sock.fileno()}
                    memory.set("server0:client-" + str(sock.fileno()), json.dumps(client_object).encode("utf-8", "ignore"))

                elif fileno == master_pipe.fileno():
                    print(master_pipe.recv())

                elif e_bits & select.EPOLLIN:
                    recv = sockets[fileno].recv(1024).decode("utf-8", "ignore").rstrip("\r\n")
                    if not recv:
                        raise socket.error
                    if recv == "asdasdasd":
                        print(len(sockets))
                    #print(recv)
                    workers[i % len(workers)].put( (fileno, recv, ) )

            except socket.error:
                sockets[fileno].close()
                del sockets[fileno]

                client = json.loads(memory.get("server0:client-" + str(fileno)).decode("utf-8"))
                #print(client)

                if client:
                    if "chat" in client:
                        memory.lrem("chat:" + client["chat"], client["__id__"])

                    memory.delete("server0:client-" + str(fileno))

            finally:
                i += 1

if __name__ == "__main__":
    workers = []
    master_pipe, worker_pipe = multiprocessing.Pipe()

    for i in range( max(1, multiprocessing.cpu_count() - 1) ):
        workers.append(multiprocessing.Queue())

        p = multiprocessing.Process(target = Worker, args = (i, workers[-1], worker_pipe, ))
        p.daemon = True
        p.start()

    Master(master_pipe, workers)

