I will create a multiplayer "Snake Game" in Python. On the server I'm using threads to be able to handle multiple clients, but now I do not know what to do to send a socket to all clients. I'll need it to inform all users to the new position of "food" when someone "eat the food."
Here is my server code:
from socket import *
import threading, os, sys, random
#from constants import *
def isOnline(username):
return username in playerList
def getNewColor():
r = random.randrange(0, 255)
g = random.randrange(0, 255)
b = random.randrange(0, 255)
return (r, g, b)
def genFood():
x = random.randrange(1, 49)
y = random.randrange(1, 49)
return (x, y)
def handler(clientsocket, clientaddr):
print ('New Client ', clientaddr)
player = ''
points = 0
index = 0
while 1:
try:
data = clientsocket.recv(1024).decode()
if data != '':
print(data)
if data.split(' ')[0] == 'login':
if isOnline(data.split(' ')[1]):
clientsocket.send('already_on'.encode())
else:
color = getNewColor()
clientsocket.send(('success ' + str(color)).encode())
player = data.split(' ')[1]
index = playerList.index(player)
playerList.append(player)
pointList.append(0)
if player != '':
if data == 'eat':
points += 1
pointList[index] = points
foodX, foodY = genFood()
except:
print('Disconnected Client')
clientsocket.close()
if player != '':
del playerList[index]
del pointList[index]
return 0
addr = ('localhost', 50000)
serversocket = socket(AF_INET, SOCK_STREAM)
serversocket.bind(addr)
serversocket.listen(20)
playerList = []
pointList = []
while 1:
clientsocket, clientaddr = serversocket.accept()
threading._start_new_thread(handler, (clientsocket, clientaddr))
serversocket.close()
3 Answers 3
The quick&dirty solution is to store all of the client sockets in a list, like this:
clientsockets = []
# ...
clientsocket, clientaddr = serversocket.accept()
clientsockets.append(clientsocket)
threading._start_new_thread(handler, (clientsocket, clientaddr))
And of course remember to remove the socket from the list when a client closes.
Then, to send a message to everyone:
for socket in clientsockets:
socket.send(msg)
Except that you'll want some error handling there so one dead client doesn't bring the whole server down.
However, this has a problem: A send is never guaranteed to send the entire message. You can fix that by using sendall instead, but that's not guaranteed to be atomic; it's always possible that one thread will send part of a message, then another thread will send part of its message, than the first thread will send the rest of its message.
So, you will need to add some locking.
A simpler solution is to create a read thread and a write thread for each client, with a queue.Queue for each write thread. Queues, unlike sockets, are guaranteed to be atomic, so you don't need to worry about thread safety.
(You can also just create a single "broadcast" queue, but then each client needs to wait on its broadcast queue and its client-specific queue, at which point you're writing the exact same kind of select-like code you were hoping to avoid with threads.)
There's also a similar problem on the read side. You're just calling recv, expecting to get exactly one message. But sockets are byte streams, not message streams. There's no guarantee that you won't get half a message, or two messages, in a single recv. You have to write some protocol, and buffer up incoming bytes and split off the messages from the buffer.
In this case, your messages are just lines of text, with (as far as I can tell) no possibility of embedded newlines in the messages. This allows for a dead-simple protocol: each line is a message. And socket.makefile works perfectly as a handler for this protocol. Like this:
def handler(clientsocket, clientaddr):
# your existing initial setup code
with clientsocket.makefile('rb') as f:
for data in f:
try:
# your existing loop, minus the recv command
You may want to consider using a Client class to wrap up the two handlers and the three lists, instead of keeping them all separate, and then you can also make the list of clients an attribute of the class instead of a global, but the basic idea is the same.
Comments
If you want all of your client threads to be able to see new data, then you need to establish some sort of global variable before any of the threads are created. A common construct for threads is to create a queue for the worker threads to take information from and process; the problem, though, is that you need for each thread to see everything that goes into that queue, so threads can't be taking things out of it.
Here's my suggestion: maintain a queue of "messages" that include data the threads need to pass along to each client. Attach a timestamp to each of these messages, so that each thread can know when a message is new or old. Each thread will keep track of the timestamp of the most recent message it sent, so it will know if an instruction in the queue is something that it's already sent. If each thread keeps its timestamp in a list that is visible to the main thread, then the main thread can look and see which instructions are guaranteed to have been seen/sent to every client, and can remove outdated instructions accordingly.
Keep in mind I haven't had to work with threads for a little while, but I'm hoping the logic is sound. And as others have said, there may be other frameworks that will get you the results you want more efficiently. But if you're sticking with the "each socket has its own thread" mechanism, this should get you started with how to think this through.
1 Comment
I would recommend using an established webserver framework. Network issues are hard to diagnose and fix. Your server code as-is will have trouble responding to your demands. Personally I recommend Tornado for simple applications like this.
4 Comments
urllib.request or requests or whatever you prefer.) But using HTTP raises issues of its own—e.g., it's a request-response protocol, so if you want to be able to send unsolicited commands from server to client you need some kind of long-polling "idle" command. It's worth learning how to do things that way as well, but it's not an either-or thing; you'll want to learn both.
recvto contain exactly one command; it could contain half a command, or two commands. You need to write a protocol. For your use case, since your commands are all plain text with no possible embedded newlines, I'd suggest using "each message is a line" as your protocol, so you can usef = clientsocket.makefile('rb')and then justfor line in f:as your main loop.