如何解决针对授权服务器状态的asynchronous客户端操作?

我有一个多人纸牌游戏,我正在开发过程中。

游戏比较简单; 玩家拥有一定数量的牌,他们可以激活,出售或以各种其他方式使用。

游戏不是基于回合的。 相反,它是asynchronous的,所以玩家( ABCD )都在同一时间做出select。 某些操作会影响游戏状态。 例如,从游戏中移除一张卡片或在游戏中添加一张卡片。 玩家应该始终保持同步,例如, 玩家A不能使用玩家B刚刚移除的牌,因为它不再存在。

很显然,在玩家B尝试与同一张牌交互的同时, 玩家A可能会移除一张牌。 服务器如何处理这种情况呢?

我原来的想法是,每个客户端可以散列他们的游戏状态,并将他们的命令发送到服务器,例如:

玩家A – #MYHASH# – 命令=拾起卡片Z

玩家B – #MYHASH# – 命令=删除卡Z

然后服务器将执行第一个命令并重新生成哈希。 在尝试执行第二个命令时,它会注意到由播放器B发送的散列与当前散列不同,所以服务器将拒绝该请求。

在上面的例子中, 玩家A不会注意到任何事情,游戏将继续正常进行,但玩家B将不得不被告知他们的行动失败。

国家的散列是正确的总体思想,但错误的具体实施; 两个不同的状态可以哈希到相同的值,虽然这可能是罕见的发生(取决于你的散列algorithm),它仍然是你想要处理的情况。

相反,考虑一个序列ID。 序列ID从某个初始值开始,在每次经过validation的游戏状态更改后由服务器更新。 当服务器向所有客户端发送更新时,该更新的一部分包含新的状态序列ID。 当客户端发送一个请求来改变某个服务器的时候,它包含了与它当前拥有的状态一致的序列ID。

当服务器从客户端获取请求时,如果包含的客户端序列号与当前的序列号不相同,则更新将被拒绝,因为客户端具有陈旧状态; 服务器应该通知客户端(以及新的状态和序列ID)。 否则,服务器接受移动,更新其状态和序列ID,并将改变广播到所有客户端。

很明显,这个服务器上的更新已经被同步了,但是无论如何,因为无论如何你都在修改游戏状态。

这种方法的缺点是客户端不能将多个状态变化请求分派给服务器; 发送一个后,必须等待确认和序列号更新。 对此有几种解决scheme,但我认为理想的解决scheme是在本地模拟连续的移动,就好像服务器对所有内容都表示“是”,但保持它们在内存中排队。 当服务器确认每个移动时,发送下一个排队的动作,直到服务器状态赶上客户端。 如果在任何时候您从服务器得到的响应不是该客户端自己的操作(即来自另一个客户端的更新)的确认,则放弃剩余的排队操作并重新同步游戏状态,从而显示相应的错误消息给用户,如果需要的话。

这个系统也可以作为容错(也就是容错)持久性系统的基础 – 如果你有一个存储后端有事务保证,比如数据库,你可以持久化更新请求和序列ID,能够在崩溃后恢复相同的交易顺序。 它也可以允许幂等操作; 我所描述的是在我们工作的MMO中构建物品转移系统的基础的简化视图。

只需发送命令。 如果B要求已经使用的卡片,则服务器可以发送一个YoureOutOfLuck事件作为响应。 那很简单。

你更大的担心是用户界面。 您需要立即提供点击反馈,但必须等待确认后才能执行任何实际操作。 做一个好的点击效果不是太长/烦人,但是为典型的RYT /确认提供足够的时间是非常棘手的。

你会想看看乐观locking 。

对于实际的实现,如果您使用HTTP进行通信,请检查HTTP如何使用ETag和If-Match HTTP标头进行冲突检测。 如果您不使用HTTP进行通信,或者只使用HTTP作为您自己的请求的信封,请尝试将您的devise基于ETag和If-Match。

在你的特定情况下,会发生什么事是服务器应该附加一个代表游戏状态的实体标签到发送给客户端的所有游戏状态更新。 然后,客户端必须在移动请求中附加接收到的标签,服务器检查如果请求中的标签与当前的游戏状态标签不匹配,则请求被拒绝; 如果标签与当前的游戏状态散列相匹配,则请求由服务器处理,并且更新被推送给将不得不更新其游戏状态和标签的所有客户端。 如果客户的请求被拒绝,被拒绝的客户也将更新其游戏状态和标签,但也通知用户他们不能执行所请求的动作。

要计算一个实体标签,只要您一致地生成一个实体标签,就可以使用游戏状态的散列值,随机数或序列号。 散列的优点是你不需要额外的数据库字段来存储标签,因为你可以很容易地从当前的游戏状态重新生成它。 无论您select哪种方法都不应该为客户端,因为实体标签是不透明的客户端。