《Python多人游戏项目实战》第五节 断线重连
目录
5.1 模拟弱网状态
5.2 断线重连
5.3 优化玩家名称显示
5.4 完整代码下载地址
导致客户端和服务端断开连接的原因可能有以下三种:
- 服务端主动关闭连接。
- 客户端窗口关闭,玩家退出游戏。
- 客户端所在网络不给力(也叫做弱网),导致延迟或者丢包,严重时掉线。
前两点是正常的断线情况,我们主要来简单了解下针对第三种情况的应对措施,运行结果如下:
注:本节代码是在第三节代码的基础上添加的断线重连功能。
本项目结构显示如下(和第三节中的项目结构一样):
├── SimHei.ttf # 字体文件
├── client.py # 客户端代码
├── pics # 图片文件夹
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ └── 6.png
├── player.py # 包含Player类
└── server.py # 服务端代码
在player.py中我们新导入了以下模块或库:
import uuid
5.1 模拟弱网状态
当客户端处在弱网状态下的时候,客户端界面会出现卡顿现象(人物移动卡顿,聊天延迟等),严重的话就掉线。我们在GameWindow类中添加一些代码来模拟这种情况:
# client.py
class GameWindow:
def __init__(self):
...
self.port = 5000
self.host = "127.0.0.1"
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.is_connected = False # 1
self.connect()
...
def connect(self):
self.sock.connect((self.host, self.port))
self.player.id = self.sock.recv(2048).decode("utf-8")
self.is_connected = True # 2
def send_player_data(self):
if not self.is_connected: # 3
print("和服务端断开连接")
self.player.message = ""
return pickle.dumps({})
if randint(0, 1000) < 20: # 4
print("断线")
self.sock.close()
self.is_connected = False
self.player.message = ""
return pickle.dumps({})
elif randint(0, 100) < 10: # 5
print("卡顿")
time.sleep(1)
data = {
"id": self.player.id,
"player": self.player
}
self.sock.send(pickle.dumps(data))
self.player.message = ""
return self.sock.recv(2048)
...
代码解释如下:
1. is_connected变量用来表示客户端和服务端是否连接成功。
2. 如果成功连接,就将is_connected值设为True。
3. 如果客户端和服务端之间的连接断开,就打印断开提示并返回一个空字典的pickle序列化值。
4. 模拟掉线状态,让客户端主动断开连接,并将is_connected值设置为False。
5. 使用time.sleep()函数模拟卡顿状态。
运行结果如下:
首先运行服务端,在运行第一个客户端前,我们先把上面新增的代码注释掉,表示该客户端的网络正常。在运行另一个客户端之前,再把注释取消掉,表示该客户端处在弱网状态下。
右边的客户端处于弱网状态下,笔者在控制人物时会明显感觉到卡顿。掉线之后,其他客户端玩家人物就会从游戏窗口上消失,而该弱网玩家也会在其他玩家的游戏窗口上消失。
5.2 断线重连
当客户端和服务端之间的连接断开后,客户端应该再次发送连接请求。
修改GameWindow类:
# client.py
class GameWindow:
...
def connect(self):
try:
self.sock.connect((self.host, self.port))
except Exception as e: # 3
print(repr(e))
print("连接失败,10秒后尝试重新连接。")
time.sleep(10)
self.reconnect()
else:
self.player.id = self.sock.recv(2048).decode("utf-8")
self.is_connected = True
def reconnect(self): # 1
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect()
def send_player_data(self):
if not self.is_connected:
print("和服务端断开连接,尝试重新连接")
self.reconnect() # 2
self.player.message = ""
return pickle.dumps({})
...
...
代码解释如下:
1. 在reconnect()函数中,我们重新实例化了一个socket对象并调用connect()方法向服务端发起连接。
2. 和服务端断开连接后,调用reconnect()方法重新连接。
3. 如果因为网络原因连接失败的话,就等10秒后再次尝试连接。
运行结果如下:
从打印结果可以看出来,当玩家掉线后,客户端与服务端重新连接了,窗口更新还是很卡顿。如果没有个服务端重连,那人物移动是不会卡顿的。
5.3 优化玩家名称显示
如果大家运行了5.2小节中的代码,会发现断线重连后,玩家的id可能会发生改变。这是因为在重连后,服务端的conn对象是新的,而str(id(conn))的值也可能和断线前的不一样。
注:当然在游戏中应该显示玩家自定义的名称,如果是这样的话,那重连后不会有这个名称变换的问题,因为玩家名称都是根据玩家账号从数据库中获取的。
为了简单起见,我们就在Player类中添加一个name属性,这个就当做玩家自定义的名称。
修改Player类:
# player.py
class Player:
def __init__(self, p_id, x, y, pic_num, frame_width, frame_height):
...
self.message = ""
self.name = f"葬爱{uuid.uuid4()}" # 1
...
def draw(self, win, pic):
win.blit(pic, (self.x, self.y), self.frame_rect)
font = pygame.font.Font("SimHei.ttf", 10) # 2
name_text = font.render(self.name, True, (150, 150, 150))
win.blit(name_text, (self.x + round(self.frame_width/2) - round(name_text.get_width()/2), self.y - name_text.get_height()))
...
代码解释如下:
1. name变量用来保存玩家名称,uuid用来防止名称相同的情况(当然uuid很长,拿来作为名称显示不合理,这里重点是不想让玩家名称重复)。
2. 之前是将id值显示到窗口上,现在改为显示name值。
运行结果如下:
断线重连后,玩家名称不会改变。
5.4 完整代码下载地址
链接:https://pan.baidu.com/s/1aq3-0HWPbNwvCoFgPYpYEg
密码:q15r