【问题标题】:Guaranteeing that a piece of information is stored in one single place at a time保证一条信息一次存储在一个地方
【发布时间】:2011-03-14 15:06:24
【问题描述】:

在应用程序关闭时,我想通过 Internet(使用 Indy)将文件发送到服务器。如果数据发送正常,我想从计算机中删除该文件。关键部分是我的文件不应该同时存储在两个地方(服务器、本地计算机)。

例如,我的应用程序可能会在将文件发送到服务器之后并在从本地磁盘删除文件之前意外停止。在这种情况下,文件将同时存在于这两个地方。

应用程序可能会因为以下原因停止:电源故障、Control+Alt+Del、操作系统关闭、用户关闭、系统挂起(可能还有其他原因我忘记了?)。

如何保证文件存储在一个地方?


我认为我们可以认为将文件写入磁盘是瞬时的,因为文件非常小。

【问题讨论】:

  • 您为什么不假设它随时都可能死掉,而是担心确保保存到磁盘的数据的一致性按照设计得到照顾。这就是所谓的“交易”的意思。要么全部存储,要么都不存储。您可能遇到磁盘错误、电源问题或网络问题。你可能会遇到很多问题。你无法阻止它们,你只能尝试使你的设计健壮并明确地处理它们。
  • @Warren 在下面看到我的回答:即使电源出现故障,您也可以轻松地将文件缓冲区刷新到磁盘以强制执行 ACID 行为。
  • 嗨沃伦。你能详细说明一下(或指向我一些特定的文档)吗?谢谢。
  • 我在下面更新了我的答案。

标签: delphi


【解决方案1】:

不,没有保证。操作系统和控制操作系统的最终用户总是可以终止进程。但是,操作系统通常不会尝试这样的事情,而且用户当然不会在不知不觉中杀死您的应用程序。

您可以在关机期间运行任何您喜欢的代码(例如,在Form1Close 中)。但是,如果您在几秒钟内运行一些代码而不处理消息,Windows 可能会认为您的应用程序已冻结,并询问用户是否希望终止它。因此,与往常一样,您应该在自己的线程中执行“慢”代码,以便主应用程序线程仍然响应。 [但如果最终用户不要求 Windows 终止进程,它可以无限期地运行,即使它表现不佳。]

此外,如果您希望这段代码真的运行几秒钟,您最好告诉最终用户发生了什么。例如,您可以显示一个please wait 窗口。

如果我是你,我会在主窗体中编写一个OnCloseQuery 处理程序。这个设置CanClose := false,显示状态窗口,并启动关闭线程。当这个线程完成时,它将关闭主窗体(例如通过发送消息)。 OnCloseQuery 处理程序还应该检查关闭线程是否正在运行。如果是这样,它应该简单地设置CanClose := false,而不是启动(另一个)关闭线程。例如,如果最终用户反复单击关闭框,就会发生这种情况。我认为OnCloseQuery 过程也会在线程启动主窗体关闭时运行。这次它应该正常关闭。您可以通过编写一些代码告诉OnCloseQuery 处理程序它是由线程启动的,或者通过在线程完成时设置ShutdownThreadComplete := true 标志来实现这一点。因此,您将执行CanClose := ShutdownThreadComplete

类似这样的:

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  if not ShutdownThreadStarted then
  begin
    CreatePleaseWaitMessage;
    StartShutdownThread;
    ShutdownThreadStated := true;
    CanClose := false;
    Exit;
  end;

  CanClose := ShutdownThreadComplete;
end;

【讨论】:

  • 阅读您的帖子我看到了第二种解决方案(不涉及锁定 GUI):如果连接后未成功连接,我可以设置 Indy(希望这是可能的)中止连接x 秒。这样,Windows 将永远不会看到应用程序被冻结。谢谢安德烈亚斯 (+1)。
  • TIdTCPClient 在 Indy 9 及更早版本的 Connect() 方法中具有 ATimeout 参数,在 Indy 10 中具有 ConnectTimeout 属性。
  • 谢谢雷米。这正是我所需要的。
【解决方案2】:

您可以使用以下 Windows API 调用来确保将数据写入磁盘:

var 
  F: TFileStream;

begin
  F := TFileStream.Create(....
  try
    F.Write(...)
  (...)
    FlushFileBuffers(F.Handle); // this will flush the content to disk
  finally
    F.Free;
  end;
end;

the official documentation for this API

补充说明:

在将文件发送到磁盘之前,切勿删除文件内容。 ACID 行为的一个公理是保留旧数据,直到新数据可用。你也一样。

一种可能性是后台进程。您可以向后台进程发送消息(例如作为轻量级 Windows 服务运行),然后退出客户端应用程序。后台进程将负责将文件发送到服务器,并从服务器接收到重复单元确认。然后后台进程将能够删除文件,并等待下一个进程。

客户端只需要确保后台进程已经处理了请求。由于它将是本地的,因此它将是瞬时的。

【讨论】:

  • 嗨。问题不是将文件写入磁盘(这需要几毫秒),而是与服务器通信(1-2 秒)。
  • 我认为释放流也会刷新数据。
  • @Altar 不,TFileStream.Destroy 将关闭文件句柄,这不会将数据刷新到磁盘。它将数据刷新到内部 Windows 缓冲区,稍后将物理刷新到磁盘。请参阅我的答案中来自 MSDN 的链接。
  • “一种可能是后台进程” - - - 您刚刚将问题从应用程序转移到后台进程(服务)。如果服务在第一步(删除本地文件)和第二步(发送文件到服务器)之间发生计算机/电源故障怎么办?
  • “ACID 行为的一个公理是......” - - - 我不争论 ACID,但由于计算机可能在这两个操作之间失败,我宁愿根本没有文件而不是有两个地方(它更适合我的需要)。
【解决方案3】:

我不知道你为什么要求文件不能同时保存在客户端和服务器上。但是如果你可以稍微放宽这个限制,你可以在读取后重命名文件以表明正在发送文件,然后在确认服务器收到文件后最后将其删除。

如果这是不可接受的,您可以使用以下更复杂的步骤:

  • 通知服务器您将发送文件,取回加密密钥
  • 从磁盘读取文件
  • 加密文件并将加密版本写入磁盘,确保已刷新
  • 打开未加密的文件并用 0 覆盖其内容,确保已刷新
  • 删除未加密的文件(从此时起该文件在客户端磁盘上实际上不再存在)
  • 将加密文件发送到服务器
  • 删除加密文件

如果发送过程中出现错误,应用程序下次启动时会找到加密文件,然后可以将加密文件发送到服务器。服务器已经知道它最初生成的解密密钥。

【讨论】:

  • 您好,托尔斯滕。好主意。也许一开始我可以用重命名的文件来实现简化版本。但是有一个问题,当我将文件发送到服务器时,我必须等待服务器响应文件已成功接收。我收到文件但一直没有机会回复,我将再次在两个地方保存文件。
  • 这就是加密所阻止的。虽然在服务器接收到加密文件后,客户端上仍有可能存在加密文件(如果通信在正确或错误的时刻中断),客户端上的文件将被加密只有服务器拥有的密钥。因此,该文件也可能不再存在于客户端。
【解决方案4】:

不,如果您在主窗体的 OnClose(或 OnDestroy)中执行以下操作: 1/写入文件 2/关闭文件 该文件将被完全写入并关闭。 查看 closefile 帮助以获得更多解释。

【讨论】:

    【解决方案5】:

    这就是我(部分)解决问题的方法

     Delete the file from disk (keep copy in RAM);
     Send the file to server;
     if server receives file ok
     then DoNothing (file was already deleted)
     else Write file back to disk;
    

    问题是在第 1 步和第 2 步之间文件不存在。如果应用程序在那个时候死掉,文件就会丢失(但是,这比在两个地方更容易接受)。不幸的是,第 1 步和第 2 步之间的延迟非常大,因为与远程服务器的通信非常慢(甚至可能长达 1-2 秒)。

    【讨论】:

      猜你喜欢
      • 2016-04-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-08
      • 1970-01-01
      • 2015-06-02
      • 2012-06-19
      • 2015-04-18
      相关资源
      最近更新 更多