使用 Delphi 进行透明远程处理的最简单解决方案是啥?

Posted

技术标签:

【中文标题】使用 Delphi 进行透明远程处理的最简单解决方案是啥?【英文标题】:What is the easiest solution for transparent remoting with Delphi?使用 Delphi 进行透明远程处理的最简单解决方案是什么? 【发布时间】:2010-10-12 16:54:21 【问题描述】:

我有一个用于 Win32 应用程序的两层 Delphi 应用程序,其中许多业务逻辑在一个上帝对象中实现,我想外包到一个单独的服务中。多个客户端应通过 TCP/IP telnet 样式协议访问此单独的服务。

如何让过渡变得最简单?

确切地说,我想保持这种简单性:我只想定义每个函数一次。例如,如果我想为我的应用添加密码登录功能,我只需要定义

 function Login(Username: string; PinCode: integer): boolean;

在服务器上的某个对象中运行,然后我可以从客户端使用它而无需任何额外工作。

在最坏的情况下,我必须实现三个功能而不是一个。首先,服务器上的函数体本身,其次,从网络接收文本行的解组器,将其解包并检查其有效性:

 procedure HandleCommand(Cmd: string; Params: array of string);
 begin
   ...
   if SameText(Cmd, 'Login') then begin
     CheckParamCount(Params, 2);
     ServerObject.Login(
       Params[0],
       StrToInt(Params[1])
     );
   end;
 end;

第三,marshaller,当被客户端调用时,打包参数并将它们发送到服务器:

function TServerConnection.Login(Username: string; PinCode: integer): boolean;
begin
  Result := StrToBool(ServerCall('Login '+Escape(Username)+' '+IntToStr(PinCode)));
end;

显然,我不想要这个。

到目前为止,我已经设法摆脱了 unmarshaller。使用 Delphi RTTI,我编写了一个通用解组器,它通过名称查找已发布的方法,检查参数并调用它。

所以现在我可以向服务器对象添加一个已发布的方法,并且可以从 telnet 调用它:

 function Login(Username: string; PinCode: integer): boolean;

 > login john_locke
 Missing parameter 2 (PinCode: integer)!

但是我该怎么写编组器呢?我无法动态获取服务器函数列表并将函数添加到客户端对象。我可能可以保留某种动态伪函数集合,但这会使我的客户调用丑陋:

ServerConnection.Call('Login', [Username, Password]);

另外,这会破坏类型安全,因为每个参数都作为变体传递。如果可能的话,我想保持编译时类型安全。

也许是客户端代码自动生成?我大概可以在我的服务器上写“GetFunctionList()”和“GetFunctionPrototype(Name: string)”:

> GetFunctionList
Login
Logout
IsLoggedIn

> GetFunctionPrototype Login
function Login(Username: string; PinCode: integer): boolean;

这样每次我需要更新客户端时,我只需从服务器重新查询所有函数原型,并自动为它们生成编组器代码。但这会将编译与执行混为一谈:我必须首先编译服务器,然后启动它并查询它的功能,然后构建一个客户端编组器,然后才重新编译客户端。复杂!

另一种选择是编写一个通用编组器函数,然后从所有客户端原型函数中调用它:

procedure TServerConnection.GenericMarshaller(); assembler;
asm
  //finds the RTTI for the caller function, unwinds stack, pops out caller params,
  //packs them according to RTTI and sends to the server.
  //receives the result, pushes it to stack according to RTTI, quits
  //oh god
end;

function TServerConnection.Login(Username: string; PinCode: integer): boolean; assembler;
asm
  call GenericMarshaller
end;

这节省了我每次编写手动打包的时间(出错的机会更少),但仍然需要我手动将服务器函数原型复制到客户端对象中。另外,编写这个通用编组器可能会是一个活生生的地狱。

还有使用 RPC 的选项,但我不喜欢它,因为我需要重新定义 IDL 中的所有函数。 Delphi 的 IDL 编辑器很烂。而对于 OLE 接口,Delphi 也会强行生成“安全调用”函数,这些函数也很糟糕。除了检查对 RPC 类的每个函数调用之外,没有办法实现自动检测断开和自动恢复连接功能:

function TServerWrapper.Login(Username: string; PinCode: integer): boolean;
begin
  try
    RealObject.Login(Username, Pincode);
  except
    on E: EOleException do
      if IndicatesDisconnect(E) then
        Disconnect;
      Reconnect;
      RealObject.Login(Username, Pincode);
  end;
end;

我们回到了几个函数,而不是一个。

那么,你们有什么建议呢?我还缺少哪些其他选择? Delphi 中是否有用于远程处理的通用模式或现成的解决方案?

【问题讨论】:

当心 n 层应用比 2 层更复杂。一些逻辑需要重新设计。服务器可能是无状态的(客户端应该有一种发送状态的方法),也可能是有状态的(您必须处理服务器上的每个状态)。您可能会遇到多线程和可伸缩性问题,因为现在一个服务器应该执行 n 个客户端的任务。您必须保护敏感数据的通信通道,并处理身份验证和授权。您不使用 n 层来简化代码。您可以使用它来更好地控制应用程序及其数据。 【参考方案1】:

就个人而言,我认为您不应该基于透明远程处理来构建分布式系统。过程级 RPC 只是错误的粒度级别,无法实现健壮和高性能的客户端-服务器交互;让它变得可容忍将迫使您编写定义不明确的程序,这些程序会做大量的事情,并接受大量参数,只是为了避免网络往返(=> 大量小调用)API 对性能和可靠性的影响。

更多地考虑消息。考虑在客户端上建立一个消息队列以传递给您的服务器,也许与每条消息相关联的回调来处理返回值,如果有的话(匿名方法非常适合回调!)。然后,一次性将所有消息分发到服务器,并接收和解析结果,根据需要为处理的每条消息调用回调等。

如果您的服务器在处理复合请求(来自客户端的消息队列)到事务中时有某种方法可以封装所有状态更改,那么它的效果会更好:您将获得一种很好的方法来处理第 n 次发生的错误消息 - 只需丢弃服务器上完成的所有工作,就好像客户端请求从未进入一样继续进行。这通常也简化了客户端对服务器状态的理解。

我过去曾围绕这些原则构建系统,并且它们很有效。让网络透明,假装系统的物理布局不存在,从长远来看只会造成痛苦。

【讨论】:

感谢您的有趣评论。我已经用其他类型的服务器做到了这一点,但这个主要由客户端功能组成,这些功能大多是自给自足的,而且调用很少。网络延迟我没问题,而且如果我需要异步发送,我可以轻松切换到它下面并模拟仅用于透明调用的同步回复。【参考方案2】:

您绝对应该至少在 Delphi 2010 中使用 DataSnap,最好在 Delphi XE 中使用。你描述的基本上就是DataSnap。它是一个非常强大的 RPC 系统,允许您创建可以为客户端提供任何 Delphi 类型或类的服务器。一旦服务器就位,客户端就可以创建代理代码来调用服务器,就像它是应用程序的本机部分一样。没有 IDL,没有 COM,只有干净的 Delphi 代码。服务器甚至可以生成 REST/JSON 组合,以便与非 Delphi 客户端一起使用。

DataSnap 正是您所寻找的——干净、整洁、强大且简单。

【讨论】:

没有安全性...尤其是在 XE 之前的版本中(除非您使用使用 DCOM 安全性的 DCOM 版本)。当然,您始终可以以明文形式或以蹩脚的安全方式发送您的 PIN 码,密钥永远不会更改并存储在本地...... 听起来不错,我试试看,谢谢。唯一困扰我的是它似乎有同样的缺点:你需要先编译并运行服务器,然后才能为客户端生成代码。不确定这将如何集成到自动构建中。 没有问题,只要你使用正确的方式保持服务器和客户端代码同步。生成客户端存根后,您无需重新生成它,直到您更改服务器。将它们推送到 VCS,当自动构建工具拉取代码时,它只需要编译服务器和客户端 - 它不需要重新生成任何东西。 如今,安全性已融入 DataSnap 和 ssl 支持,但我仍然更喜欢 RemObjects SDK 和 DataAbstract。【参考方案3】:

您应该查看 RemObjects SDK 等框架之一。您使用他们的工具来定义界面,它会为您完成所有工作。然后,您只需实现这些功能,并在客户端中调用它们。通过采用业务逻辑组件并将其作为接口,我已将单个应用程序转换为客户端/服务器应用程序。无需推出您自己的有线协议。

【讨论】:

刚刚提到 components4developers 的 kbmMW 作为此类事情的另一个好框架。【参考方案4】:

您可以编写一个程序,使用 RTTI 为您构建必要的单元,而不是手动编写所有代码。我过去曾使用包含数千个表的 ORM 系统来完成此操作……编写代码生成器从数据库模式中吐出与系统一起工作所需的类和单元,更快、更容易。

这种方法的另一个优点是更容易测试,因为它可以通过可预测的行为进行扩展。

【讨论】:

以上是关于使用 Delphi 进行透明远程处理的最简单解决方案是啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用 .NET 获得快速 RPC 的最简单方法?

使用 Python 进行 SSH 的最简单方法是啥?

连接到 .NET 远程服务器对象的最简单方法是啥

使用delphi+intraweb进行微信开发1~4代码示例

delphi都支持哪几种图片格式?

Dubbo