闲时论道:构造基于接口的分布式应用(.NET Framework 2.0)
资历尚浅,还不会用专业术语;有词不达意的地方,朋友们包涵!
我在做一个分布式应用的时候,突想起早前看到的一篇文章,大意是,在分布式客户端与服务端间构建接口,在服务端负责实现,客户端负责调用。优点是,任何“实现”的更改、升级只在服务端更新、再部署即可,而不用牵涉到客户端。对于系统维护,这是大有裨益的。
我想是自己长大了,从前对这个思路似懂非懂,如今通过搜索原始素材、“使劲”的解读模仿(相当的用力),应该说基本理解了。
这个思路的要点在于:接口库同时部署在服务端、客户端,实现接口的运行库部署在服务端,但在客户端需要部署一个欺骗编译器的“假运行库”;“假运行库”旨在使得服务端的实现类在客户端得到明调用。举例如下:
这是服务端、客户端都有部署的接口函数(RemotingInterface.dll):
- using System;
- namespace RemotingInterface {
- /// <summary>
- /// 分布式管理者. 这个类存在的意义在于: 其帮助客户端通过公用接口,
- /// 而在服务器端创建接口相对应的类型. 该类作为接口与类型的构造器出现.
- /// </summary>
- public interface IRemotingManager {
- /// <summary>
- /// 构造公用接口的服务器端类型实例.
- /// </summary>
- /// <typeparam name="I">公用接口.</typeparam>
- /// <param name="erroMessage">意外信息.</param>
- /// <returns>公用接口对应类型的实例.</returns>
- I GetObject<I>(out string erroMessage) where I: class;
- /// <summary>
- /// 服务器地址. 由客户端初始化服务端实例的服务器地址.
- /// </summary>
- /// <param name="formatUrl">服务器地址.</param>
- /// <param name="erroMessage">意外信息.</param>
- void InitServerUrl(string formatUrl, out string erroMessage);
- }
- }
这是服务端“真实现”的类(RemotingLibrary.dll):
- using System;
- using System.Runtime.Remoting;
- using RemotingInterface;
- using System.Collections;
- namespace RemotingLibrary {
- /// <summary>
- /// 服务端分布式管理者. 实际继承分布式管理接口.
- /// </summary>
- public class RemotingManager: MarshalByRefObject, IRemotingManager {
- #region Server Instance
- private static Hashtable constructors = new Hashtable();
- static RemotingManager() {
- RemotingConfiguration.Configure(AppDomain.CurrentDomain.FriendlyName + ".config", false);
- }
- /// <summary>
- /// 服务端注册服务类型.
- /// </summary>
- /// <typeparam name="I">服务接口名称.</typeparam>
- /// <typeparam name="T">服务类型名称.</typeparam>
- public static void RegistService<I, T>()
- where I: class
- where T: MarshalByRefObject, I, new() {
- string name = typeof(I).Name;
- if(!constructors.ContainsKey(name))
- constructors.Add(name, new Constructor<T>());
- }
- /// <summary>
- /// 服务端卸载服务类型.
- /// </summary>
- /// <typeparam name="I">服务接口名称.</typeparam>
- public static void UnRegistService<I>() where I: class {
- string name = typeof(I).Name;
- if(constructors.ContainsKey(name))
- constructors.Remove(name);
- }
- #endregion
- private string serverUrl = null;
- private bool serverUrlInit = true;
- #region IRemotingManager 成员
- T IRemotingManager.GetObject<T>(out string erroMessage) {
- try {
- erroMessage = null;
- IConstructor cstr = constructors[typeof(T).Name] as IConstructor;
- if(cstr == null) {
- erroMessage = string.Format("服务端未注册的接口类型 {0}", typeof(T).Name);
- return null;
- }
- if(serverUrl == null)
- return cstr.GetObject() as T;
- else
- return cstr.GetObjectByUrl(serverUrl) as T;
- }
- catch(Exception E) {
- erroMessage = E.Message;
- }
- return null;
- }
- void IRemotingManager.InitServerUrl(string formatUrl, out string erroMessage) {
- if(serverUrlInit) {
- serverUrl = formatUrl;
- serverUrlInit = false;
- erroMessage = null;
- }
- else
- erroMessage = "服务器地址已经初始化.";
- }
- #endregion
- #region Remoting Constructor
- interface IConstructor {
- object GetObject();
- object GetObjectByUrl(string formatServerUrl);
- }
- class Constructor<T>: IConstructor
- where T: class, new() {
- #region IConstructor 成员
- object IConstructor.GetObject() {
- return new T();
- }
- object IConstructor.GetObjectByUrl(string formatServerUrl) {
- if(string.IsNullOrEmpty(formatServerUrl))
- throw new ArgumentNullException("formatServerUrl", "无法创建指定分布式类型, 远程服务器地址为空.");
- return (T)Activator.GetObject(typeof(T), string.Format(formatServerUrl, typeof(T).Name));
- }
- #endregion
- }
- #endregion
- }
- }
这是客户端“假实现”的类(RemotingCheater.dll):
- using System;
- using System.Runtime.Remoting;
- using System.Threading;
- using RemotingInterface;
- namespace RemotingLibrary {
- /// <summary>
- /// 客户端分布式管理者. 伪装继承分布式管理接口.
- /// </summary>
- public class RemotingManager: MarshalByRefObject, IRemotingManager {
- #region Client Instance
- /// <summary>
- /// 服务器地址.
- /// </summary>
- private static readonly string serverManagerUrl = null;
- /// <summary>
- /// 读取客户端配置
- /// </summary>
- static RemotingManager() {
- string url = System.Configuration.ConfigurationManager.AppSettings["RemotingServerUrl"];
- if(string.IsNullOrEmpty(url)) {
- try {// WebForm
- RemotingConfiguration.Configure(AppDomain.CurrentDomain.BaseDirectory + "\\web.config", false);
- return;
- }
- catch {
- }
- try {// WinForm
- RemotingConfiguration.Configure(AppDomain.CurrentDomain.FriendlyName + ".config", false);
- return;
- }
- catch {
- }
- // WindowsService
- RemotingConfiguration.Configure(Thread.GetDomain().BaseDirectory + AppDomain.CurrentDomain.FriendlyName + ".config", false);
- }
- else {
- url = url.Replace('\\', '/');
- if(url.EndsWith("/")) {
- url = url + "{0}";
- }
- else {
- url = url + "/{0}";
- }
- serverManagerUrl = url;
- }
- }
- /// <summary>
- /// 获取分布式类管理者.
- /// </summary>
- /// <returns></returns>
- public static IRemotingManager GetManager() {
- if(serverManagerUrl == null) {
- return new RemotingManager();
- }
- else {
- RemotingManager rm = (RemotingManager)Activator.GetObject(typeof(RemotingManager),
- string.Format(serverManagerUrl, typeof(RemotingManager).Name));
- string err;
- ((IRemotingManager)rm).InitServerUrl(serverManagerUrl, out err);
- return rm;
- }
- }
- #endregion
- #region IRemotingManager 成员
- T IRemotingManager.GetObject<T>(out string erroMessage) {
- throw new NotImplementedException("只可通过远程获取!");
- }
- void IRemotingManager.InitServerUrl(string formatUrl, out string erroMessage) {
- throw new NotImplementedException("只可通过远程获取!");
- }
- #endregion
- }
- }
注意:“假实现”与“真实现”的非静态字段及其可访问性必须保持一致,否则运行时将报错;类静态的属性、方法等是各自独立的,并在各自的端点生效:“真实现”的静态属性、方法在服务端生效,“假实现”的静态属性、方法在客户端生效。
朋友们看到了,服务端对接口的实现相当复杂,而在客户端只是简单的“假实现”;不用担心,当你在客户端通过构造器 public static IRemotingManager GetManager() { ... } 获取实例的时候,其实是在服务端创建并返回。
不过,这样“真假实现”其实是不优雅的。我尝试在客户端丢弃“假实现”,而通过“接口”直接调用服务端的实现。迂回于泛型的运用,看上去我做到了。上面朋友们看到的 IRemotingManager 就是我用来协助客户端创建“远程接口实例”的辅助类。在部署了上面传统实现 IRemotingManager 的前提下,看下面较优雅的对 IRemotingFile 的应用:
两端部署的接口约定(RemotingInterface.dll):
- using System;
- using System.Collections.Generic;
- using System.Text;
- namespace RemotingInterface {
- /// <summary>
- /// 分布式文件操作
- /// </summary>
- public interface IRemotingFile {
- /// <summary>
- /// 获取指定目录下所有文件列表.
- /// </summary>
- /// <param name="path">指定目录.</param>
- /// <returns>文件列表.</returns>
- string[] GetFileNameList(string path, out string erroMessage);
- /// <summary>
- /// 重命名文件.
- /// </summary>
- /// <param name="newFileName">新文件名.</param>
- /// <param name="oriFileName">旧文件名.</param>
- void ChangeFileName(string newFileName, string oriFileName, out string erroMessage);
- /// <summary>
- /// 删除文件.
- /// </summary>
- /// <param name="fileName">指定文件.</param>
- void DeleteFile(string fileName, out string erroMessage);
- }
- }
服务端的实现(RemotingLibrary.dll):
- using System;
- using System.IO;
- using RemotingInterface;
- namespace RemotingLibrary {
- public class RemotingFile: MarshalByRefObject, IRemotingFile {
- #region IRemotingFile 成员
- string[] RemotingInterface.IRemotingFile.GetFileNameList(string path, out string erroMessage) {
- try {
- erroMessage = null;
- FileInfo[] fis = new DirectoryInfo(path).GetFiles();
- string[] ret = new string[fis.Length];
- for(int i = 0; i < fis.Length; i++) {
- ret[i] = fis[i].FullName;
- }
- return ret;
- }
- catch(Exception E) {
- erroMessage = E.Message;
- }
- return null;
- }
- void RemotingInterface.IRemotingFile.ChangeFileName(string newFileName, string oriFileName, out string erroMessage) {
- try {
- erroMessage = null;
- System.IO.File.Move(oriFileName, newFileName);
- }
- catch(Exception E) {
- erroMessage = E.Message;
- }
- }
- void RemotingInterface.IRemotingFile.DeleteFile(string fileName, out string erroMessage) {
- try {
- erroMessage = null;
- System.IO.File.Delete(fileName);
- }
- catch(Exception E) {
- erroMessage = E.Message;
- }
- }
- #endregion
- }
- }
服务端启动(RemotingConsoleServer.exe):
- using System;
- using System.Collections.Generic;
- using System.Text;
- using RemotingInterface;
- using RemotingLibrary;
- namespace RemotingConsoleServer {
- class Program {
- static void Main(string[] args) {
- Console.WriteLine("正在开启分布式服务 ...");
- RemotingManager.RegistService<IRemotingFile, RemotingFile>();
- //RemotingManager.RegistService<IRemotingImage, RemotingImage>();
- Console.WriteLine("分布式服务已启动 ...");
- Console.ReadLine();
- }
- }
- }
客户端调用(RemotingClient.exe):
- using System;
- using RemotingInterface;
- using RemotingLibrary;
- namespace RemotingClient {
- class Program {
- static void Main(string[] args) {
- string err;
- IRemotingFile mrc = RemotingManager.GetManager().GetObject<IRemotingFile>(out err);
- string[] fs = mrc.GetFileNameList("d:/Test", out err);
- if(err == null){
- Console.WriteLine("已获取文件列表:");
- foreach(string s in fs)
- Console.WriteLine("\t" + s);
- }
- else
- Console.WriteLine(string.Format("Exception: {0}", err));
- Console.ReadLine();
- }
- }
- }
朋友们又该注意到了,接口函数中,我无一例外的使用了 out string erroMessage 变量用以传递错误信息。这种方式可能过于粗糙,朋友只作参考,不必延用。有关分布式异常处理方面,webabcd 老大好像有一些处理方案,可惜当初我没有深究。
缺憾:RemotingManager.GetManager() 看来必须每次调用时都如此实时获取,而不能“缓存”(比如,IRemotingManager rm = RemotingManager.GetManager(); IRemotingFile mrc = rm.GetObject<IRemotingFile>(out err);),因为服务端会定时回收已创建的实例,如果缓存,将会不可预知地出现当前实例失效的情况。是否我太懒,我只粗浅的了解了一下服务端所谓的“过时策略”,而没有深入并找出更好的解决方案。“实时获取”大概会使得系统运行“稳定”,但带给性能的负影响我实在无法预估。
最后谈较容易被轻视甚至忽略的配置问题。
服务端的配置,很传统,没太多可“计较”的;当增加新的“分布类”时,记得增加它相关的配置:
- <?xml version="1.0" encoding="utf-8" ?>
- <configuration>
- <system.runtime.remoting>
- <application name="ServiceProvider">
- <service>
- <wellknown mode="SingleCall" type="RemotingLibrary.RemotingManager,RemotingLibrary" objectUri="RemotingManager"></wellknown>
- <wellknown mode="SingleCall" type="RemotingLibrary.RemotingFile,RemotingLibrary" objectUri="RemotingFile"></wellknown>
- </service>
- <channels>
- <channel ref="tcp server" port="6000"></channel>
- </channels>
- </application>
- </system.runtime.remoting>
- </configuration>
客户端的配置,我的策略是,当 appSetting 中不存在 RemotingServerUrl 配置项,程序将读取 system.runtime.remoting 节;但是,我自己没有测试通过 system.runtime.remoting 配置节获取远程实例,个人万不能保证系统运作正常:
- <?xml version="1.0" encoding="utf-8" ?>
- <configuration>
- <appSettings>
- <!--优先读取-->
- <add key="RemotingServerUrl" value="tcp://192.168.*.*:6000"/>
- </appSettings>
- <!--当 appSettings 中 RemotingServer 不存在或为空时, 读取该节 system.runtime.remoting.-->
- <!--<system.runtime.remoting>
- <application>
- <client>
- <wellknown type="RemotingLibrary.RemotingManager,RemotingLibrary" url="tcp://192.168.*.*:6000/RemotingManager"/>
- <wellknown type="RemotingLibrary.RemotingFile,RemotingLibrary" url="tcp://192.168.*.*:6000/RemotingFile"/>
- </client>
- </application>
- </system.runtime.remoting>-->
- </configuration>
最大的遗憾:WCF 等框架已经进入平民视野,我现在把玩的不过是昨日黄花。我刚学会了一百一十米栏,微软告诉我改耍自行车了。料想明天还会改玩赛车、飞机甚至火箭!我们是幸福的“把玩者”呢,还是不幸的“追新人”?
(源码下载)