【问题标题】:Client/Server state synchronization for desktop application桌面应用程序的客户端/服务器状态同步
【发布时间】:2019-12-10 04:39:17
【问题描述】:

我正在开发一个需要在多个客户端之间进行同步的桌面应用程序。基本上,一群人(比如说 2 到 10 人)都运行同一个应用程序。其中一个托管服务器,其他客户端连接到该服务器。托管服务器的客户端也连接到他自己的服务器。

应用程序应在所有客户端之间保持同步,这意味着所有客户端在应用程序中看到相同的数据。具体来说,我可以用两种不同的形式定义有问题的数据:

  1. 具有特定值的简单属性(该值必须保持同步)
  2. 属性列表(列表中的项目及其值必须保持同步)

(1) 的简单示例可能是:客户端当前选择了列表中的哪个项目,以及客户端鼠标指针在应用程序窗口中的当前位置。这些属性不断变化,但这些属性的数量是恒定的并且不会增加(例如在设计时定义)。

(2) 的一个示例可以是聊天消息列表。这些列表会在运行时增长,无法预测会有多少项。

以下是状态、客户端和聊天消息的 C# 示例代码:

public class State
{
    // A single value shared between all clients
    public int SimpleInteger {get;set;}

    // List of connected clients and their individual states
    public List<Client> Clients {get;set;}

    // List of chat messages
    public List<ChatMessage> Messages {get;set;}
}

public class Client 
{
    public string ClientId {get;set;}
    public string Username {get;set;}

    public ClientState ClientState {get;set;}
}

public class ClientState 
{
    public string ClientId {get;set;}
    public int SelectedIndex {get;set;}
    public int MouseX {get;set;}
    public int MouseY {get;set;}
}

public class ChatMessage 
{
    public string ClientId {get;set;}
    public string Message {get;set;}
}

我已经断断续续地研究了很长时间,但是无论我想出什么样的状态同步,它都没有奏效。

当我搜索解决方案时,我只找到游戏的解决方案,但这些解决方案并不是很有帮助,因为我的要求不同:

  • 我无法处理“删除更新”,我无法预测(插值或推断)其他客户端正在做什么。每个客户都需要接收每个更新以保持同步。
  • 另一方面,我不关心滞后(在合理范围内)。如果我看到其他客户端的更新延迟大约一秒就好了。
  • 当新客户端连接(或重新连接)时,必须传输大部分状态(例如:示例 2 中的聊天消息列表)。每个客户都需要了解整个聊天历史记录,因此必须在客户连接时下载。

我目前的解决方案可以总结如下:

  • 服务器跟踪状态,例如真相的来源。
  • 状态包含需要同步的属性。
  • 该状态还包含已连接用户的列表(及其用户名等)。
  • 每个客户端还保留一份状态的本地副本,他们可以立即采取行动。例如,它们会不断更新其在本地状态中的鼠标位置。
  • 每当客户端更新其本地状态时,此更新都会发送到服务器。
    • 这里的潜在例外情况是变化太快的东西,例如鼠标位置,我只会定期发送。
  • 服务器还会更新常见的“真实来源”状态。
  • 最后,服务器使用新的更新状态更新所有其他客户端。

最后两个步骤是我苦苦挣扎的地方。我可以想到两种同步状态的方法,一种简单但可能效率不高,另一种效率高但容易出错。

  1. 服务器只是将整个状态发送给所有客户端。
    • 服务器一收到来自客户端的更新,就会将更新应用到状态并广播新状态。其他所有客户都会替换他们的本地状态。
    • 我觉得这可能会起作用,但是由于“列表”项目(例如聊天消息),状态会迅速增长。在我之前的尝试中,这很快就成为了一个问题,并且将状态发送回变得太慢了。
  2. 服务器将相同的更新(它收到的)重新发送给所有其他客户端。
    • 然后,每个客户端仅将新更新应用到其本地状态以与服务器同步。
    • 这可能效率更高,并且仅在客户端连接时才需要发送整个状态。
    • 但是,过去我经常遇到客户端不再同步的异步问题。我真的不知道是什么原因造成的,可能是消息之间的冲突(例如服务器告诉客户端更新状态中的值,但客户端只是更新了他的本地值,这有优先权?)。发生这种情况后,一切都完全出错了,因为更新现在被应用于两个不同的状态并产生不同的结果。

我正在寻找有关如何实现此目标的一般概念的指导。我正在使用几个消息传递库来实现客户端和服务器之间的实际通信,我认为这部分不是问题。例如,我可以确保在这些库中接收到每条消息(尽管我不确定订单是否得到保证)。就像我之前说的,延迟不是问题,但我必须保证服务器和所有其他客户端都接收到每个状态更新。

任何帮助都会很棒!谢谢。

【问题讨论】:

    标签: server synchronization client desktop-application messaging


    【解决方案1】:

    这是一个难题,有很多棘手的地方我不想自己构建。我想到了身份验证、冲突更新、API 管理、网络中断、单点故障和本地持久性。

    如果您准备使用基于云的解决方案,Google Cloud Firestore 会处理这些棘手的领域并满足您的需要:

    • 客户端通过创建、更新或删除记录将数据保存到数据库中。 Example code
    • 每当创建、更新或删除记录时,所有客户端都会收到实时通知。 Example code

    (点击以上链接后,请务必点击代码框上方的C# 以查看 C# 代码)。

    【讨论】:

      【解决方案2】:

      任何 SQL DBMS 都实现了问题陈述的一个重要部分:它维护共享状态。考虑 ACID 的承诺:

      • 一致性。在任何一个时刻,从数据库读取的所有客户端都可以保证看到相同的信息。
      • 原子性。更新数据库的客户端可以根据需要使用尽可能多的步骤。提交事务时,数据会完全更改或根本不更改。
      • 隔离。服务器给每个客户端一个单独与它交互的错觉。它处理并发更新,并更新数据库,就像更新是串行到达一样。
      • 您可能不关心此应用程序的耐用性。

      在我看来,客户之间的调解是 DBMS 对您的应用程序最有用的功能。这将节省您的工作和头痛。另一个不明显的好处是它可以强制执行状态信息的一致性规则。这对于防止过时/损坏的客户端修改共享状态非常有用。

      您的问题陈述的第二部分是通知 2-10 个客户更改状态。有很多方法可以做到这一点。

      一些 DBMS 可以通过触发器访问操作系统服务。您可以让更新触发器发出通知。或者,更新客户端可以这样做。

      实际的通知机制可能非常简单。客户端可以连接到服务器(您编写的)并阻止读取(2)。服务器本身在端口上侦听更新通知。收到一个后,它会将其重复给所有连接的客户端。当客户端的读取请求返回时,是时候查询数据库以获取更新的状态,并发布新的读取。

      为了防止在多个更新接连到达时出现一种“雷击群”问题,当客户端读取更新消息时,它可以继续读取更新直到 EWOULDBLOCK,然后才查询 DBMS。 OTOH,如果查看中间状态很重要(查看每次更新,而不仅仅是当前状态),DBMS 完全有能力存储和提供所有版本,并用时间戳或序列号区分它们。

      如果您不想直接使用 TCP 套接字,您可能更喜欢ZeroMQ

      在这个设计中,每个客户端都有三个连接:DBMS、读取通知套接字和(可能)服务器通知套接字。服务器有 N+1 个连接,用于 N 个客户端和一个侦听套接字。您没有要实现的锁,很少跟踪参与,没有问题重新同步,以及客户端之间的短窗口不一致,因为每个客户端都根据其通知进行操作。

      【讨论】:

        【解决方案3】:

        您的问题有多种解决方案。

        1. 您可以使用开箱即用的 BizTalk 服务器。这可能不是您的想法。

        2. 如果您想要更自制的东西,您可以使用 WCF(Windows 通信基础)和 MSMQ(Microsoft 消息队列)。这将为您提供有保证的消息传递和持久的消息(如果您愿意)。您不必担心连接丢失以及消息传输过程中发生的其他错误。

        3. 您可以再往下走,使用直接的 TCP 和 UDP 协议来传输消息。但是现在,您必须处理更多错误情况。

        【讨论】:

          【解决方案4】:

          正如您所理解的,这是一个复杂的问题,涉及许多活动部件。在我一直在研究这个问题时,我在各种问答网站上阅读了一些关于此类问题的问题,并指出这种事情完全是一个独立的项目。

          免责声明:我自己没有这样做,所以我不知道这会有多好,但也许你可以接受我的建议并与他们合作,如果你还没有这样做了。我参与过实施该项目的项目,但我并未直接参与该实施。

          连接

          由于您没有说明您使用哪个库进行连接,我假设您使用的是 websockets 或类似的东西。如果没有,我建议你转向 websockets 之类的东西。它允许客户端和服务器之间的(近)恒定连接,以便可以双向推送数据,避免客户端轮询和拉取数据。下面的链接似乎对如何做到这一点有一个不错的了解,所以我不会尝试。因为链接死了,这是他们给出的第一个示例代码,看起来很简单。

          ​using System.Net.Sockets;
          using System.Net;
          using System;
          
          class Server {
              public static void Main() {
                  TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 80);
          
                  server.Start();
                  Console.WriteLine("Server has started on 127.0.0.1:80.{0}Waiting for a connection...", Environment.NewLine);
          
                  TcpClient client = server.AcceptTcpClient();
          
                  Console.WriteLine("A client connected.");
              }
          }
          

          https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server

          客户端启动

          服务器和客户端之间建立稳定连接后,您需要确保数据同步。当用户启动应用程序时,您可以获取每个表中最新更改的时间戳并将其与服务器进行比较。如果它们完全相同,则您可以合理地期望该表没有更改。我假设每个表都有一列,其中包含对行的最后一次编辑的时间戳。

          对于已更改的表,您可以让服务器根据客户端的“最后更改的时间戳”将新的和更新的行发送给客户端。

          由于不能保证 100% 连接互联网,因此您还需要跟踪客户端连接的时间与他们使用应用程序的时间(除非应用程序无法运行没有连接到服务器)。此信息还需要发送到服务器,以与客户端未连接期间更改的数据进行比较。

          完成时间戳匹配后,您需要比较行数。如果它们匹配,您可以更合理地假设这些表是相同的。如果不是,您可以查看有关匹配 ID/主键的信息。有多种不同的方法可以做到这一点,包括 1:1 匹配(这是最慢但最可靠的),或者您可以对 ID 进行一些数学运算(假设为数字 ID)并尝试查看 100 行批次中有什么不同(例如)。思路:如果前 100 行添加排序后的自增整数 ID 在客户端和服务器上相同,则所有这些行都存在于两个服务器上,但如果不匹配,您可以尝试 1:1匹配以查看缺少的内容。因为这对于大型数据库来说可能会很长,您可能希望在另一个表中跟踪这种类型的同步,因此不需要一直这样做。

          相反,您可能需要一个表来跟踪所有未发送到客户端的数据。这需要确认发送的数据已正确插入客户端数据库。这也可以在客户端工作以跟踪尚未发送到服务器的内容。当然,这种事情很快就会变得很麻烦,即使你只是在跟踪键、表名和时间戳。如果您不定期删除旧数据,您可以快速收集数百万行。这就是为什么我建议跟踪未发送的数据,以便该表不再跟踪和删除任何变成“已发送”的数据。

          如果您不想编码和管理所有这些,您可以尝试使用一个库来完成它。那里有各种各样的。甚至微软也有一个,但它的支持时间仅延长至 2021 年 1 月 1 日。在那之后会发生什么,我怀疑连微软都不知道,但它会让你花 1.25 年的时间来想出一个不同的解决方案。

          使用 Sync 框架创建同步提供程序

          同步框架可用于构建应用程序,这些应用程序可以使用任何协议通过网络同步来自任何数据存储的数据。我们将向您展示它是如何工作的,并帮助您开始构建自定义同步提供程序。

          https://docs.microsoft.com/en-us/previous-versions/sql/synchronization/mt490616(v=msdn.10)

          https://support.microsoft.com/en-us/lifecycle/search?alpha=Microsoft%20Sync%20Framework%202.1

          正常运行时间

          一旦您在启动时(或启动后在后台同步)您的数据,您就可以简单地将数据正常发送到服务器,就像用户进行更改时一样。由于您将拥有一个 websocket 类型的连接,因此服务器从其他客户端获得的任何更改都将能够推送到所有其他客户端。

          就在您的应用中实时更改数据而言,您可能必须不断地轮询本地/客户端数据库以获取时间戳更改,以便适当地更新 UI。 C# 中可能有一些东西可以为您或您可以找到的其他库执行此操作。

          结论

          在这一点上,我没有想法。对我来说这似乎是合理的,即使它需要做很多工作。希望你能把我所拥有的东西作为你自己关于如何完成任务的想法的基础。看来你还有很多工作要做,祝你好运!

          脚注

          由于我是在几天未得到答复后目前唯一的答案,我将假设没有其他人有更好的建议。如果他们这样做,我会鼓励他们做出自己的答案,而不是抱怨我的。人们会调整这个答案,但在制作 cmets 时请记住社区标准。

          我只是回答这个问题,因为我没有看到其他人在这个网站或其他网站上这样做过。它只是到处都是零散的碎片,人们仍然无法理解它的整体意义。

          这个和类似的问题之前已经在这个网站上被问过并且被关闭为“太宽泛”。如果您和读者有同样的感觉,请在问题而不是这个答案上投票。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-06-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-01-01
            • 2023-03-19
            相关资源
            最近更新 更多