闲时论道:构造基于接口的分布式应用(.NET Framework 2.0)

    资历尚浅,还不会用专业术语;有词不达意的地方,朋友们包涵!

    我在做一个分布式应用的时候,突想起早前看到的一篇文章,大意是,在分布式客户端与服务端间构建接口,在服务端负责实现,客户端负责调用。优点是,任何“实现”的更改、升级只在服务端更新、再部署即可,而不用牵涉到客户端。对于系统维护,这是大有裨益的。

    我想是自己长大了,从前对这个思路似懂非懂,如今通过搜索原始素材、“使劲”的解读模仿(相当的用力),应该说基本理解了。

    这个思路的要点在于:接口库同时部署在服务端、客户端,实现接口的运行库部署在服务端,但在客户端需要部署一个欺骗编译器的“假运行库”;“假运行库”旨在使得服务端的实现类在客户端得到明调用。举例如下:


    这是服务端、客户端都有部署的接口函数(RemotingInterface.dll):

  1. using System;  
  2.  
  3. namespace RemotingInterface {  
  4.     /// <summary>  
  5.     /// 分布式管理者. 这个类存在的意义在于: 其帮助客户端通过公用接口,   
  6.     /// 而在服务器端创建接口相对应的类型. 该类作为接口与类型的构造器出现.  
  7.     /// </summary>  
  8.     public interface IRemotingManager {  
  9.       
  10.         /// <summary>  
  11.         /// 构造公用接口的服务器端类型实例.  
  12.         /// </summary>  
  13.         /// <typeparam name="I">公用接口.</typeparam>  
  14.         /// <param name="erroMessage">意外信息.</param>  
  15.         /// <returns>公用接口对应类型的实例.</returns>          
  16.         I GetObject<I>(out string erroMessage) where I: class;  
  17.           
  18.        /// <summary>  
  19.        /// 服务器地址. 由客户端初始化服务端实例的服务器地址.  
  20.        /// </summary>  
  21.        /// <param name="formatUrl">服务器地址.</param>  
  22.        /// <param name="erroMessage">意外信息.</param>  
  23.         void InitServerUrl(string formatUrl, out string erroMessage);  
  24.           
  25.     }  
  26. }  

    这是服务端“真实现”的类(RemotingLibrary.dll):

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using RemotingInterface;  
  4. using System.Collections;  
  5.  
  6. namespace RemotingLibrary {  
  7.  
  8.     /// <summary>  
  9.     /// 服务端分布式管理者. 实际继承分布式管理接口.  
  10.     /// </summary>  
  11.     public class RemotingManager: MarshalByRefObject, IRemotingManager {  
  12.  
  13.         #region Server Instance  
  14.  
  15.         private static Hashtable constructors = new Hashtable();  
  16.  
  17.         static RemotingManager() {  
  18.             RemotingConfiguration.Configure(AppDomain.CurrentDomain.FriendlyName + ".config", false);  
  19.         }  
  20.  
  21.         /// <summary>  
  22.         /// 服务端注册服务类型.  
  23.         /// </summary>  
  24.         /// <typeparam name="I">服务接口名称.</typeparam>  
  25.         /// <typeparam name="T">服务类型名称.</typeparam>  
  26.         public static void RegistService<I, T>()  
  27.             where I: class 
  28.             where T: MarshalByRefObject, I, new() {  
  29.             string name = typeof(I).Name;  
  30.             if(!constructors.ContainsKey(name))  
  31.                 constructors.Add(name, new Constructor<T>());  
  32.         }  
  33.  
  34.         /// <summary>  
  35.         /// 服务端卸载服务类型.  
  36.         /// </summary>  
  37.         /// <typeparam name="I">服务接口名称.</typeparam>  
  38.         public static void UnRegistService<I>() where I: class {  
  39.             string name = typeof(I).Name;  
  40.             if(constructors.ContainsKey(name))  
  41.                 constructors.Remove(name);  
  42.         }  
  43.  
  44.         #endregion  
  45.  
  46.         private string serverUrl = null;  
  47.         private bool serverUrlInit = true;  
  48.  
  49.         #region IRemotingManager 成员  
  50.  
  51.         T IRemotingManager.GetObject<T>(out string erroMessage) {  
  52.             try {  
  53.                 erroMessage = null;  
  54.                 IConstructor cstr = constructors[typeof(T).Name] as IConstructor;  
  55.                 if(cstr == null) {  
  56.                     erroMessage = string.Format("服务端未注册的接口类型 {0}", typeof(T).Name);  
  57.                     return null;  
  58.                 }  
  59.                 if(serverUrl == null)  
  60.                     return cstr.GetObject() as T;  
  61.                 else 
  62.                     return cstr.GetObjectByUrl(serverUrl) as T;  
  63.             }  
  64.             catch(Exception E) {  
  65.                 erroMessage = E.Message;  
  66.             }  
  67.             return null;  
  68.         }  
  69.  
  70.         void IRemotingManager.InitServerUrl(string formatUrl, out string erroMessage) {  
  71.             if(serverUrlInit) {  
  72.                 serverUrl = formatUrl;  
  73.                 serverUrlInit = false;  
  74.                 erroMessage = null;  
  75.             }  
  76.             else 
  77.                 erroMessage = "服务器地址已经初始化.";  
  78.         }  
  79.  
  80.         #endregion  
  81.  
  82.         #region Remoting Constructor  
  83.  
  84.         interface IConstructor {  
  85.             object GetObject();  
  86.             object GetObjectByUrl(string formatServerUrl);  
  87.         }  
  88.  
  89.         class Constructor<T>: IConstructor  
  90.             where T: class, new() {  
  91.  
  92.             #region IConstructor 成员  
  93.  
  94.             object IConstructor.GetObject() {  
  95.                 return new T();  
  96.             }  
  97.  
  98.             object IConstructor.GetObjectByUrl(string formatServerUrl) {  
  99.                 if(string.IsNullOrEmpty(formatServerUrl))  
  100.                     throw new ArgumentNullException("formatServerUrl", "无法创建指定分布式类型, 远程服务器地址为空.");  
  101.                 return (T)Activator.GetObject(typeof(T), string.Format(formatServerUrl, typeof(T).Name));  
  102.             }  
  103.  
  104.             #endregion  
  105.         }  
  106.  
  107.         #endregion  
  108.     }  
  109. }  

    这是客户端“假实现”的类(RemotingCheater.dll):

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Threading;  
  4. using RemotingInterface;  
  5.  
  6. namespace RemotingLibrary {  
  7.  
  8.     /// <summary>  
  9.     /// 客户端分布式管理者. 伪装继承分布式管理接口.  
  10.     /// </summary>  
  11.     public class RemotingManager: MarshalByRefObject, IRemotingManager {  
  12.  
  13.         #region Client Instance  
  14.  
  15.         /// <summary>  
  16.         /// 服务器地址.  
  17.         /// </summary>  
  18.         private static readonly string serverManagerUrl = null;  
  19.           
  20.         /// <summary>  
  21.         /// 读取客户端配置  
  22.         /// </summary>  
  23.         static RemotingManager() {  
  24.             string url = System.Configuration.ConfigurationManager.AppSettings["RemotingServerUrl"];  
  25.             if(string.IsNullOrEmpty(url)) {  
  26.                 try {// WebForm  
  27.                     RemotingConfiguration.Configure(AppDomain.CurrentDomain.BaseDirectory + "\\web.config", false);  
  28.                     return;  
  29.                 }  
  30.                 catch {  
  31.                 }  
  32.                 try {// WinForm  
  33.                     RemotingConfiguration.Configure(AppDomain.CurrentDomain.FriendlyName + ".config", false);  
  34.                     return;  
  35.                 }  
  36.                 catch {  
  37.                 }  
  38.                 // WindowsService  
  39.                 RemotingConfiguration.Configure(Thread.GetDomain().BaseDirectory + AppDomain.CurrentDomain.FriendlyName + ".config", false);  
  40.             }  
  41.             else {  
  42.                 url = url.Replace('\\', '/');  
  43.                 if(url.EndsWith("/")) {  
  44.                     url = url + "{0}";  
  45.                 }  
  46.                 else {  
  47.                     url = url + "/{0}";  
  48.                 }  
  49.                 serverManagerUrl = url;  
  50.             }  
  51.         }  
  52.  
  53.         /// <summary>  
  54.         /// 获取分布式类管理者.  
  55.         /// </summary>  
  56.         /// <returns></returns>  
  57.         public static IRemotingManager GetManager() {  
  58.             if(serverManagerUrl == null) {  
  59.                 return new RemotingManager();  
  60.             }  
  61.             else {  
  62.                 RemotingManager rm = (RemotingManager)Activator.GetObject(typeof(RemotingManager),  
  63.                  string.Format(serverManagerUrl, typeof(RemotingManager).Name));  
  64.                  string err;  
  65.                 ((IRemotingManager)rm).InitServerUrl(serverManagerUrl, out err);  
  66.                 return rm;  
  67.             }  
  68.         }  
  69.  
  70.         #endregion  
  71.           
  72.         #region IRemotingManager 成员  
  73.  
  74.         T IRemotingManager.GetObject<T>(out string erroMessage) {  
  75.             throw new NotImplementedException("只可通过远程获取!");  
  76.         }  
  77.          
  78.         void IRemotingManager.InitServerUrl(string formatUrl, out string erroMessage) {  
  79.             throw new NotImplementedException("只可通过远程获取!");  
  80.         }  
  81.  
  82.         #endregion  
  83.     }  
  84. }  

    注意:“假实现”与“真实现”的非静态字段及其可访问性必须保持一致,否则运行时将报错;类静态的属性、方法等是各自独立的,并在各自的端点生效:“真实现”的静态属性、方法在服务端生效,“假实现”的静态属性、方法在客户端生效。


       朋友们看到了,服务端对接口的实现相当复杂,而在客户端只是简单的“假实现”;不用担心,当你在客户端通过构造器 public static IRemotingManager GetManager() {  ... }  获取实例的时候,其实是在服务端创建并返回。

    不过,这样“真假实现”其实是不优雅的。我尝试在客户端丢弃“假实现”,而通过“接口”直接调用服务端的实现。迂回于泛型的运用,看上去我做到了。上面朋友们看到的 IRemotingManager 就是我用来协助客户端创建“远程接口实例”的辅助类。在部署了上面传统实现 IRemotingManager 的前提下,看下面较优雅的对 IRemotingFile 的应用:


    两端部署的接口约定(RemotingInterface.dll):

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4.  
  5. namespace RemotingInterface {  
  6.     /// <summary>  
  7.     /// 分布式文件操作  
  8.     /// </summary>  
  9.     public interface IRemotingFile {  
  10.         /// <summary>  
  11.         /// 获取指定目录下所有文件列表.  
  12.         /// </summary>  
  13.         /// <param name="path">指定目录.</param>  
  14.         /// <returns>文件列表.</returns>  
  15.         string[] GetFileNameList(string path, out string erroMessage);  
  16.           
  17.         /// <summary>  
  18.         /// 重命名文件.  
  19.         /// </summary>  
  20.         /// <param name="newFileName">新文件名.</param>  
  21.         /// <param name="oriFileName">旧文件名.</param>  
  22.         void ChangeFileName(string newFileName, string oriFileName, out string erroMessage);  
  23.           
  24.         /// <summary>  
  25.         /// 删除文件.  
  26.         /// </summary>  
  27.         /// <param name="fileName">指定文件.</param>  
  28.         void DeleteFile(string fileName, out string erroMessage);  
  29.     }  
  30. }  

    服务端的实现(RemotingLibrary.dll):

  1. using System;  
  2. using System.IO;  
  3. using RemotingInterface;  
  4.  
  5. namespace RemotingLibrary {  
  6.     public class RemotingFile: MarshalByRefObject, IRemotingFile {  
  7.       
  8.         #region IRemotingFile 成员  
  9.  
  10.         string[] RemotingInterface.IRemotingFile.GetFileNameList(string path, out string erroMessage) {  
  11.             try {  
  12.                 erroMessage = null;  
  13.                 FileInfo[] fis = new DirectoryInfo(path).GetFiles();  
  14.                 string[] ret = new string[fis.Length];  
  15.                 for(int i = 0; i < fis.Length; i++) {  
  16.                     ret[i] = fis[i].FullName;  
  17.                 }  
  18.                 return ret;  
  19.             }  
  20.             catch(Exception E) {  
  21.                 erroMessage = E.Message;  
  22.             }  
  23.             return null;  
  24.         }  
  25.  
  26.         void RemotingInterface.IRemotingFile.ChangeFileName(string newFileName, string oriFileName, out string erroMessage) {  
  27.             try {  
  28.                 erroMessage = null;  
  29.                 System.IO.File.Move(oriFileName, newFileName);  
  30.             }  
  31.             catch(Exception E) {  
  32.                 erroMessage = E.Message;  
  33.             }  
  34.         }  
  35.  
  36.         void RemotingInterface.IRemotingFile.DeleteFile(string fileName, out string erroMessage) {  
  37.             try {  
  38.                 erroMessage = null;  
  39.                 System.IO.File.Delete(fileName);  
  40.             }  
  41.             catch(Exception E) {  
  42.                 erroMessage = E.Message;  
  43.             }  
  44.         }  
  45.  
  46.         #endregion  
  47.           
  48.     }  
  49. }  

    服务端启动(RemotingConsoleServer.exe):

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Text;  
  4. using RemotingInterface;  
  5. using RemotingLibrary;  
  6.  
  7. namespace RemotingConsoleServer {  
  8.     class Program {  
  9.         static void Main(string[] args) {  
  10.             Console.WriteLine("正在开启分布式服务 ...");  
  11.             RemotingManager.RegistService<IRemotingFile, RemotingFile>();  
  12.             //RemotingManager.RegistService<IRemotingImage, RemotingImage>();  
  13.             Console.WriteLine("分布式服务已启动 ...");  
  14.             Console.ReadLine();  
  15.         }  
  16.     }  
  17. }  

    客户端调用(RemotingClient.exe):

  1. using System;  
  2. using RemotingInterface;  
  3. using RemotingLibrary;  
  4.  
  5. namespace RemotingClient {  
  6.     class Program {  
  7.         static void Main(string[] args) {  
  8.             string err;  
  9.             IRemotingFile  mrc = RemotingManager.GetManager().GetObject<IRemotingFile>(out err);  
  10.             string[] fs = mrc.GetFileNameList("d:/Test", out err);  
  11.             if(err == null){  
  12.                 Console.WriteLine("已获取文件列表:");  
  13.                 foreach(string s in fs)  
  14.                     Console.WriteLine("\t" + s);  
  15.             }  
  16.             else 
  17.                 Console.WriteLine(string.Format("Exception: {0}", err));  
  18.             Console.ReadLine();  
  19.         }  
  20.     }  
  21. }  

    朋友们又该注意到了,接口函数中,我无一例外的使用了 out string erroMessage 变量用以传递错误信息。这种方式可能过于粗糙,朋友只作参考,不必延用。有关分布式异常处理方面,webabcd 老大好像有一些处理方案,可惜当初我没有深究。

    缺憾:RemotingManager.GetManager() 看来必须每次调用时都如此实时获取,而不能“缓存”(比如,IRemotingManager rm = RemotingManager.GetManager(); IRemotingFile  mrc = rm.GetObject<IRemotingFile>(out err);),因为服务端会定时回收已创建的实例,如果缓存,将会不可预知地出现当前实例失效的情况。是否我太懒,我只粗浅的了解了一下服务端所谓的“过时策略”,而没有深入并找出更好的解决方案。“实时获取”大概会使得系统运行“稳定”,但带给性能的负影响我实在无法预估。


    最后谈较容易被轻视甚至忽略的配置问题。

    服务端的配置,很传统,没太多可“计较”的;当增加新的“分布类”时,记得增加它相关的配置

  1. <?xml version="1.0" encoding="utf-8" ?> 
  2. <configuration> 
  3.     <system.runtime.remoting> 
  4.         <application name="ServiceProvider"> 
  5.             <service> 
  6.                 <wellknown mode="SingleCall" type="RemotingLibrary.RemotingManager,RemotingLibrary" objectUri="RemotingManager"></wellknown> 
  7.                 <wellknown mode="SingleCall" type="RemotingLibrary.RemotingFile,RemotingLibrary" objectUri="RemotingFile"></wellknown> 
  8.             </service> 
  9.             <channels> 
  10.                 <channel ref="tcp server" port="6000"></channel> 
  11.             </channels> 
  12.         </application> 
  13.     </system.runtime.remoting> 
  14. </configuration> 

    客户端的配置,我的策略是,当 appSetting 中不存在 RemotingServerUrl 配置项,程序将读取 system.runtime.remoting 节;但是,我自己没有测试通过 system.runtime.remoting 配置节获取远程实例,个人万不能保证系统运作正常

  1. <?xml version="1.0" encoding="utf-8" ?> 
  2. <configuration> 
  3.     <appSettings> 
  4.         <!--优先读取--> 
  5.         <add key="RemotingServerUrl" value="tcp://192.168.*.*:6000"/> 
  6.     </appSettings> 
  7.     <!--当 appSettings 中 RemotingServer 不存在或为空时, 读取该节 system.runtime.remoting.--> 
  8.     <!--<system.runtime.remoting> 
  9.         <application> 
  10.             <client> 
  11.                 <wellknown type="RemotingLibrary.RemotingManager,RemotingLibrary" url="tcp://192.168.*.*:6000/RemotingManager"/> 
  12.                 <wellknown type="RemotingLibrary.RemotingFile,RemotingLibrary" url="tcp://192.168.*.*:6000/RemotingFile"/> 
  13.             </client> 
  14.         </application> 
  15.     </system.runtime.remoting>--> 
  16. </configuration> 

    最大的遗憾:WCF 等框架已经进入平民视野,我现在把玩的不过是昨日黄花。我刚学会了一百一十米栏,微软告诉我改耍自行车了。料想明天还会改玩赛车、飞机甚至火箭!我们是幸福的“把玩者”呢,还是不幸的“追新人”?

    (源码下载

posted @ 2008-08-19 11:53  陛下  阅读(2574)  评论(13编辑  收藏  举报