Python daphne 如何实现「顶号」功能(避免让用户多开)
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第25天,点击查看活动详情。
背景
我开发过《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋、象棋等游戏。
其中有一个特性,我斟酌了许久:是否允许用户多开?
- 如果允许多开,那么开发者必须保存同一用户的多个WebSocket连接,发生变更时,给这些连接同时发送一样的消息。当然这些连接发来的消息也必须都能按顺序处理。
- 如果不允许多开,那么开发者必须花费精力实现「顶号」逻辑,明确告知被顶号的用户——你被下线了。这样才有较好的用户体验。
最终,我选择了「不允许多开」的方案,如图:
因为我考虑到,「多开」对用户没有任何意义,反而会浪费服务器资源。当我们的服务不允许用户「多开」时,针对每个用户只需要保存1个WebSocket连接,提高了性能。
前提知识
我使用的技术是Python Daphne,所以本文也针对这种技术选型来做分享。
你可以参考《用86行代码写一个联机五子棋WebSocket后端》,文章用 daphne 写了个简单的 WebSocket 服务。
daphne 应用中的参数
我们在用 daphne 开发应用时,需要定义一个应用函数application
,它有3个参数,分别是scope
receive
send
。
官方文档只告诉你,每当有一个 WebSocket 连接建立时,都会新启动一个application
协程,把这个请求的上下文存入 scope
(包括header、uri等信息),而开发者可以调用 receive
接收客户端数据,调用 send
向客户端发送数据。
但是官方文档没告诉你关于receive
和send
的更多秘密和原理。我是通过阅读 daphne 源码,再加上自己了解一点点 Python 异步编程,才掌握了他们的高级用法。分享给大家。
receive 和 send 的本质
因为 Python 中通常不会标注类型,所以很多人只是会用,但不了解 receive
send
的本质。
通过 daphne 源码,可以找到这一段:
可以看到,receive
仅仅只是asyncio.Queue().get
,即异步编程中的队列的get方法。
而send
则是一个partial
函数,即附带了默认参数的函数,它指定了调用handle_reply
时的第一个参数为protocol
。而protocol
则是ws_protocol.py
中的WebSocketProtocol
的实例。
这个protocol
里面存储了非常重要的一个变量:application_queue
,这个变量,其实就是receive
对应的异步队列。
以上是我看了很多天、很多遍源码,才得到的结论,官方文档完全没有提及,现在我直接分享给你!请多看几遍。
理解 receive send 本质后,可以做什么
之前我们提到,我们可以把send
函数引用保存下来,从而实现在用户A的协程中给用户B发消息(反之亦然),也可以实现在任意用户的协程中发送广播消息。
如果我们能够把所有用户的application_queue
引用保存下来,那么我们可以做更多事情:
- 可以主动发个假的消息给它,让它
receive
到来自服务端的消息(而非来自客户端的消息),从而做出反应。这实现了多用户之间的最快速的后端通信。 - 可以清空它的
receive
队列,让它不执行任何客户端发送来的指令。
如何获取application_queue
引用
问题来了,我们定义application
,只获得了3个参数scope
receive
send
,没办法获得application_queue
引用呀?怎么办呢?
别急,你记得send
是个partial
函数吗?有一个默认参数,这个默认参数其实是可以获取的,通过args
即可获取。于是:
queue = send.args[0].application_queue
我们获得了application_queue
引用。
如何实现「顶号」逻辑
于是,「顶号」逻辑,也可以实现:
当玩家A在另一处登陆时,他上一个 WebSocket 连接,应该立马结束,意思是这个 WebSocket 不应该再处理任何还未开始处理的请求了。我们需要把application_queue
清空。并且,立马主动关掉 WebSocket 连接。
queue = self.send.args[0].application_queue
try:
while True:
queue.get_nowait()
except QueueEmpty:
pass
queue.put_nowait({"type": "websocket.close"})
注意这里{"type": "websocket.close"}
其实就是一个自定义事件,标准的事件中,没有这个type
。
随后,需要在application
中,链接建立后的while True
每次receive
后,判断一下这个事件类型:
async def application(scope, receive, send):
# ...
while True:
event = await receive()
if event['type'] == 'websocket.close':
# 自定义事件,用于服务端主动关闭某用户的链接
await send({'type': 'websocket.close', 'code': 4001})
break
# ...
写在最后
我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋、象棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》、《极致用户体验》。
转载自:https://juejin.cn/post/7158479792386342942