多人贪吃蛇游戏 Python
我打算用Python创建一个多人“贪吃蛇”游戏。在服务器上,我使用线程来处理多个客户端,但现在我不知道该怎么做才能把信息发送给所有客户端。当有人“吃到食物”时,我需要通知所有用户“食物”的新位置。
这是我的服务器代码:
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 个回答
我建议你使用一个成熟的网页服务器框架。网络问题很难诊断和解决。你现在的服务器代码可能很难满足你的需求。就我个人而言,我推荐使用Tornado,特别是像这样的简单应用。
如果你想让所有的客户端线程都能看到新的数据,那么在创建任何线程之前,你需要先建立一个全局变量。线程常用的一种方式是创建一个队列,让工作线程从中获取信息并进行处理;不过问题是,你需要确保每个线程都能看到队列里所有的内容,所以线程不能从队列中取走东西。
我的建议是:维护一个“消息”队列,这些消息包含线程需要传递给每个客户端的数据。给每条消息加上一个时间戳,这样每个线程就能知道一条消息是新还是旧。每个线程会记录它最近发送的消息的时间戳,这样它就能知道队列中的指令是否是它已经发送过的。如果每个线程把它的时间戳保存在一个主线程能看到的列表里,那么主线程就可以查看哪些指令已经确保被每个客户端看到/发送,并可以相应地删除过时的指令。
请记住,我已经有一段时间没有处理线程了,但我希望这个逻辑是合理的。正如其他人所说的,可能还有其他框架能更高效地实现你想要的结果。不过,如果你坚持使用“每个套接字都有自己的线程”的机制,这应该能帮助你开始思考这个问题。
一个简单粗暴的解决办法是把所有的客户端连接存储在一个列表里,像这样:
clientsockets = []
# ...
clientsocket, clientaddr = serversocket.accept()
clientsockets.append(clientsocket)
threading._start_new_thread(handler, (clientsocket, clientaddr))
当然,当客户端关闭连接时,记得要从列表中remove
掉这个连接。
然后,要给所有人发送消息时,可以这样做:
for socket in clientsockets:
socket.send(msg)
不过,你需要加一些错误处理,这样一个死掉的客户端就不会把整个服务器搞垮。
但是,这里有个问题:send
并不能保证会发送完整的消息。你可以用sendall
来解决这个问题,但这也不能保证是原子操作;也就是说,可能会出现一个线程发送了一部分消息,然后另一个线程又发送了自己的部分消息,最后第一个线程再发送剩下的部分。
所以,你需要加一些锁来解决这个问题。
一个更简单的办法是为每个客户端创建一个读取线程和一个写入线程,并为每个写入线程准备一个queue.Queue
。队列和套接字不同,它们是原子操作,所以你不需要担心线程安全的问题。
(你也可以只创建一个“广播”队列,但这样每个客户端就需要同时等待它的广播队列和特定的客户端队列,这样你又得写出和之前想避免的select
类似的代码了。)
在读取方面也有类似的问题。你只是调用recv
,期待能得到一条完整的消息。但是套接字是字节流,而不是消息流。没有保证你不会在一次recv
中得到半条消息或者两条消息。你需要写一些协议,把接收到的字节缓存起来,然后从缓存中分离出消息。
在这种情况下,你的消息只是文本行(据我所知),消息中没有嵌入换行符的可能性。这就让协议变得简单:每一行就是一条消息。而socket.makefile
非常适合处理这个协议。像这样:
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
你可能想考虑使用一个Client
类,把这两个处理器和三个列表封装起来,而不是把它们分开。这样,你还可以把客户端列表作为类的一个属性,而不是全局变量,但基本思路是一样的。