使用 Delphi 实现透明远程处理的最简单解决方案是什么?
我有一个两层的 Delphi for Win32 应用程序,其中有很多业务逻辑在一个上帝对象中实现,我想外包到一个单独的服务中。这个单独的服务应该由多个客户端通过 TCP/IP telnet 风格的协议访问。
我如何才能使过渡变得最简单?
准确地说,我想保持这种简单性:我想每个函数只定义一次。例如,如果我想向我的应用程序添加 pin 码登录功能,我只需要
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;
第三,编组器,当客户端调用时,打包参数并将它们发送到服务器:
function TServerConnection.Login(Username: string; PinCode: integer): boolean;
begin
Result := StrToBool(ServerCall('Login '+Escape(Username)+' '+IntToStr(PinCode)));
end;
显然,我不想要这个。
到目前为止,我已经成功摆脱了解组器。我使用 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 中是否有远程处理的常见模式或现成的解决方案?
I have a two tier Delphi for Win32 application with a lot of business logic implemented in a god object I want to outsource into a separate service. This separate service should be accessed by multiple clients via TCP/IP telnet-style protocol.
How do I go about making the transition most simple?
Precisely, I'd like to keep this simplicity: I want to define every function just once. For example, if I want to add a pin code Login functionality to my app, I would just need to define the
function Login(Username: string; PinCode: integer): boolean;
function in some object on the server, then I can use it from clients without any additional work.
In the worst case, I have to implement three functions instead of one. First, the function body itself on the server, second, the unmarshaller which receives a text line from network, unpacks it and checks for validity:
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;
Third, the marshaller, which when called by client, packs the params and sends them to server:
function TServerConnection.Login(Username: string; PinCode: integer): boolean;
begin
Result := StrToBool(ServerCall('Login '+Escape(Username)+' '+IntToStr(PinCode)));
end;
Obviously, I don't want this.
So far I have managed to rid myself of the unmarshaller. Working with Delphi RTTI, I wrote a generic unmarshaller which looks for a published method by name, checks the params and calls it.
So now I can just add a published method to the server object and I'm able to call it from telnet:
function Login(Username: string; PinCode: integer): boolean;
> login john_locke
Missing parameter 2 (PinCode: integer)!
But what do I do about writing a marshaller? I can't dynamically fetch server function list and add functions to the client object. I can probably keep some kind of dynamic pseudo-function collection but that would make my client calls ugly:
ServerConnection.Call('Login', [Username, Password]);
Plus, this defeats type-safety as every parameter is passed as a variant. If possible, I'd like to keep compile-time type-safety.
Maybe client code auto-generation? I can probably write "GetFunctionList()" and "GetFunctionPrototype(Name: string)" in my server:
> GetFunctionList
Login
Logout
IsLoggedIn
> GetFunctionPrototype Login
function Login(Username: string; PinCode: integer): boolean;
So that every time I need to update the client, I just re-query all the function prototypes from the server and automatically generate marshaller code for them. But that would mix compilation with execution: I'd have to first compile the server, then start it and query its functions, then build a client marshaller and recompile client only after that. Complicated!
Another option is to write a generic marshaller function and then call it from all the client prototype functions:
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;
This saves me writing manual packaging every time (less chance for errors), but still requires me to manually copy server function prototypes into the client object. Plus, writing this generic marshaller is probably going to be a living hell.
Then there's the option of using RPC, but I don't like it because I'd need to re-define all the functions in IDL. And Delphi's IDL editor sucks. And for OLE interfaces Delphi forcefully generates "safecall" functions which suck, too. And there's no way to implement auto-detect-disconnect and auto-restore-connection functionality other than checking EVERY SINGLE function call to the RPC class:
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;
We're back to several functions instead of one.
So, what would you guys suggest? What are the other choices I'm missing? Are there common patterns or ready solutions for remoting in Delphi?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
就我个人而言,我认为您不应该基于透明远程处理来构建分布式系统。对于健壮且高性能的客户端-服务器交互来说,过程级 RPC 的粒度级别是错误的。让它变得可以容忍将迫使您编写定义不明确的程序,这些程序会执行大量操作并获取大量参数,只是为了避免繁琐的(=>大量小调用)API 的网络往返对性能和可靠性的影响。
更多地从消息的角度思考。考虑在客户端上构建一个消息队列以传递到服务器,也许使用与每个消息关联的回调来处理返回值(如果有)(匿名方法非常适合回调!)。然后,一次性将所有消息分派到服务器,并接收和解析结果,根据需要为每条处理的消息调用回调等。
如果您的服务器在将复合请求(来自客户端的消息队列)处理到事务中时有某种方式封装所有状态更改,那么它的效果会更好:您将获得一种处理第 n 条消息上发生的错误的好方法 - 简单丢弃在服务器上完成的所有工作,并像客户端请求从未进入一样继续进行。这通常也简化了客户端对服务器状态的理解。
我过去曾围绕这些原则构建过系统,并且它们有效。使网络透明,假装系统的物理布局不存在,从长远来看只会带来痛苦。
Personally, I don't think you should be architecting a distributed system based around transparent remoting. Procedure-level RPC is simply the wrong level of granularity for robust and performant client-server interaction; making it tolerable will force you to write ill-defined procedures which do whole heaps of things, and take masses of parameters, simply to avoid the performance and reliability hit of network roundtrips of chatty (=> lots of little calls) APIs.
Think more in terms of messages. Consider building up a queue of messages on the client to pass off to your server, perhaps with callbacks associated with each message to handle return values, if any (anonymous methods work great for callbacks!). Then, dispatch all the messages to the server in one go, and receive and parse the results, invoking callbacks etc. as necessary for each message processed.
If your server has some way of encapsulating all state changes while processing a compound request (a queue of messages from a client) into a transaction, it works even better: you get a nice way of handling errors that occur on the nth message - simply discard all work done on the server, and proceed as if the client request had never come in. This normally simplifies the client's understanding of the server's state too.
I've built systems in the past oriented around these kinds of principles, and they work. Making the network transparent, pretending the physical layout of the system doesn't exist, just causes pain in the long term.
您绝对应该至少在 Delphi 2010 中使用 DataSnap,最好是在 Delphi XE 中。您所描述的基本上就是 DataSnap。它是一个非常强大的 RPC 系统,允许您创建可以为客户端提供任何 Delphi 类型或类的服务器。一旦服务器就位,客户端就可以创建代理代码来调用服务器,就像它是应用程序的本机部分一样。没有 IDL,没有 COM,只有干净的 Delphi 代码。服务器甚至可以生成 REST/JSON 组合,以便于非 Delphi 客户端使用。
DataSnap 正是您所寻找的——干净、整洁、强大且简单。
You should absolutely be using DataSnap in Delphi 2010 at least, preferably in Delphi XE. What you described is basically what DataSnap is. It is a very powerful RPC system that allows you to create servers that can serve up any Delphi type or class to a client. Once you have the server in place, the client can create proxy code to call the server just like it was a native part of your application. No IDL, no COM, nothing but clean Delphi code. The server can even produce a REST/JSON combination for easy use with non-Delphi clinets.
DataSnap is exactly what you are looking for -- clean, neat, powerful, and easy.
您应该查看 RemObjects SDK 等框架之一。您使用他们的工具来定义接口,它会为您完成所有工作。然后,您只需实现这些函数,并在客户端中调用它们即可。我通过采用业务逻辑组件并将其作为界面,将单个应用程序转换为客户端/服务器应用程序。无需推出自己的有线协议。
You should look at one of the frameworks like RemObjects SDK. You use their tool to define an interface, and it does all the work for you. You then just implement the functions, and in the client just call them. I've converted a single app into a client/server app by taking the business logic component and making it the interface. No need to roll your own wire protocol.
您可以编写一个程序,使用 RTTI 为您构建必要的单元,而不是手动编写所有内容。我过去曾使用包含数千个表的 ORM 系统完成此操作...编写代码生成器更快、更容易,该生成器从数据库模式中吐出使用系统所需的类和单元。
这种方法的另一个优点是更容易测试,因为它可以通过可预测的行为进行扩展。
You could write a program that used RTTI to build the necessary units for you rather than hand code everything. I have done this in the past with an ORM system that contained thousands of tables...was faster and much easier to write a code generator that spit out the classes and units necessary to work with the system from the database schema.
The other advantage of this approach is that it is easier to test since it can be scaled with predictable behavior.