一Remoting基礎 什麼是Remoting
簡而言之
我們可以將其看作是一種分布式處理方式
從微軟的產品角度來看
可以說Remoting就是DCOM的一種升級
它改善了很多功能
並極好的融合到
Net平台下
Microsoft?
NET Remoting 提供了一種允許對象通過應用程序域與另一對象進行交互的框架
這也正是我們使用Remoting的原因
為什麼呢?在Windows操作系統中
是將應用程序分離為單獨的進程
這個進程形成了應用程序代碼和數據周圍的一道邊界
如果不采用進程間通信(RPC)機制
則在一個進程中執行的代碼就不能訪問另一進程
這是一種操作系統對應用程序的保護機制
然而在某些情況下
我們需要跨過應用程序域
與另外的應用程序域進行通信
即穿越邊界
在Remoting中是通過通道(channel)來實現兩個應用程序域之間對象的通信的
如圖所示
首先客戶端通過Remoting訪問通道以獲得服務端對象再通過代理解析為客戶端對象這就提供一種可能性即以服務的方式來發布服務器對象遠程對象代碼可以運行在服務器上(如服務器激活的對象和客戶端激活的對象)然後客戶端再通過Remoting連接服務器獲得該服務對象並通過序列化在客戶端運行
在Remoting中對於要傳遞的對象設計者除了需要了解通道的類型和端口號之外無需再了解數據包的格式但必須注意的是客戶端在獲取服務器端對象時並不是獲得實際的服務端對象而是獲得它的引用這既保證了客戶端和服務器端有關對象的松散耦合同時也優化了通信的性能
Remoting的兩種通道
Remoting的通道主要有兩種Tcp和Http在Net中SystemRuntimeRemotingChannel中定義了IChannel接口IChannel接口包括了TcpChannel通道類型和Http通道類型它們分別對應Remoting通道的這兩種類型
TcpChannel類型放在名字空間SystemRuntimeRemotingChannelTcp中Tcp通道提供了基於Socket的傳輸工具使用Tcp協議來跨越Remoting邊界傳輸序列化的消息流TcpChannel類型默認使用二進制格式序列化消息對象因此它具有更高的傳輸性能HttpChannel類型放在名字空間SystemRuntimeRemotingChannelHttp中它提供了一種使用Http協議使其能在Internet上穿越防火牆傳輸序列化消息流默認情況下HttpChannel類型使用Soap格式序列化消息對象因此它具有更好的互操作性通常在局域網內我們更多地使用TcpChannel如果要穿越防火牆則使用HttpChannel
遠程對象的激活方式
在訪問遠程類型的一個對象實例之前必須通過一個名為Activation的進程創建它並進行初始化這種客戶端通過通道來創建遠程對象稱為對象的激活在Remoting中遠程對象的激活分為兩大類服務器端激活和客戶端激活
() 服務器端激活又叫做WellKnow方式很多又翻譯為知名對象為什麼稱為知名對象激活模式呢?是因為服務器應用程序在激活對象實例之前會在一個眾所周知的統一資源標識符(URI)上來發布這個類型然後該服務器進程會為此類型配置一個WellKnown對象並根據指定的端口或地址來發布對象Net Remoting把服務器端激活又分為SingleTon模式和SingleCall模式兩種
SingleTon模式此為有狀態模式如果設置為SingleTon激活方式則Remoting將為所有客戶端建立同一個對象實例當對象處於活動狀態時SingleTon實例會處理所有後來的客戶端訪問請求而不管它們是同一個客戶端還是其他客戶端SingleTon實例將在方法調用中一直維持其狀態舉例來說如果一個遠程對象有一個累加方法(i=++i)被多個客戶端(例如兩個)調用如果設置為SingleTon方式則第一個客戶獲得值為第二個客戶獲得值為因為他們獲得的對象實例是相同的如果熟悉AspNet的狀態管理我們可以認為它是一種Application狀態
SingleCall模式SingleCall是一種無狀態模式一旦設置為SingleCall模式則當客戶端調用遠程對象的方法時Remoting會為每一個客戶端建立一個遠程對象實例至於對象實例的銷毀則是由GC自動管理的同上一個例子而言則訪問遠程對象的兩個客戶獲得的都是我們仍然可以借鑒AspNet的狀態管理認為它是一種Session狀態
() 客戶端激活與WellKnown模式不同Remoting在激活每個對象實例的時候會給每個客戶端激活的類型指派一個URI客戶端激活模式一旦獲得客戶端的請求將為每一個客戶端都建立一個實例引用SingleCall模式和客戶端激活模式是有區別的首先對象實例創建的時間不一樣客戶端激活方式是客戶一旦發出調用的請求就實例化而SingleCall則是要等到調用對象方法時再創建其次SingleCall模式激活的對象是無狀態的對象生命期的管理是由GC管理的而客戶端激活的對象則有狀態其生命周期可自定義其三兩種激活模式在服務器端和客戶端實現的方法不一樣尤其是在客戶端SingleCall模式是由GetObject()來激活它調用對象默認的構造函數而客戶端激活模式則通過CreateInstance()來激活它可以傳遞參數所以可以調用自定義的構造函數來創建實例
二遠程對象的定義
前面講到客戶端在獲取服務器端對象時並不是獲得實際的服務端對象而是獲得它的引用因此在Remoting中對於遠程對象有一些必須的定義規范要遵循
由於Remoting傳遞的對象是以引用的方式因此所傳遞的遠程對象類必須繼承MarshalByRefObjectMSDN對MarshalByRefObject的說明是MarshalByRefObject 是那些通過使用代理交換消息來跨越應用程序域邊界進行通信的對象的基類不是從 MarshalByRefObject 繼承的對象會以隱式方式按值封送當遠程應用程序引用一個按值封送的對象時將跨越遠程處理邊界傳遞該對象的副本因為您希望使用代理方法而不是副本方法進行通信因此需要繼承MarshallByRefObject
以下是一個遠程對象類的定義
public class ServerObject:MarshalByRefObject
{
public Person GetPersonInfo(string namestring sexint age)
{
Person person = new Person();
personName = name;
personSex = sex;
personAge = age;
return person;
}
}
這個類只實現了最簡單的方法就是設置一個人的基本信息並返回一個Person類對象注意這裡返回的Person類由於這裡所傳遞的Person則是以傳值的方式來完成的而Remoting要求必須是引用的對象所以必須將Person類序列化
因此在Remoting中的遠程對象中如果還要調用或傳遞某個對象例如類或者結構則該類或結構則必須實現串行化Attribute[SerializableAttribute]
[Serializable]
public class Person
{
public Person()
{}
private string name;
private string sex;
private int age;
public string Name
{
get {return name;}
set {name = value;}
}
public string Sex
{
get {return sex;}
set {sex = value;}
}
public int Age
{
get {return age;}
set {age = value;}
}
}
將該遠程對象以類庫的方式編譯成Dll這個Dll將分別放在服務器端和客戶端以添加引用
在Remoting中能夠傳遞的遠程對象可以是各種類型包括復雜的DataSet對象只要它能夠被序列化遠程對象也可以包含事件但服務器端對於事件的處理比較特殊我將在本系列之三中介紹
三服務器端
根據第一部分所述根據激活模式的不同通道類型的不同服務器端的實現方式也有所不同大體上說服務器端應分為三步
注冊通道
要跨越應用程序域進行通信必須實現通道如前所述Remoting提供了IChannel接口分別包含TcpChannel和HttpChannel兩種類型的通道這兩種類型除了性能和序列化數據的格式不同外實現的方式完全一致因此下面我們就以TcpChannel為例
注冊TcpChannel首先要在項目中添加引用SystemRuntimeRemoting然後using名字空間SystemRuntimeRemotingChannelTcp代碼如下
TcpChannel channel = new TcpChannel();
ChannelServicesRegisterChannel(channel);
在實例化通道對象時將端口號作為參數傳遞然後再調用靜態方法RegisterChannel()來注冊該通道對象即可
注冊遠程對象
注冊了通道後要能激活遠程對象必須在通道中注冊該對象根據激活模式的不同注冊對象的方法也不同
() SingleTon模式
對於WellKnown對象可以通過靜態方法RemotingConfigurationRegisterWellKnownServiceType()來實現
RemotingConfigurationRegisterWellKnownServiceType(typeof(ServerRemoteObjectServerObject)
ServiceMessageWellKnownObjectModeSingleTon);
()SingleCall模式
注冊對象的方法基本上和SingleTon模式相同只需要將枚舉參數WellKnownObjectMode改為SingleCall就可以了
RemotingConfigurationRegisterWellKnownServiceType(typeof(ServerRemoteObjectServerObject)
ServiceMessageWellKnownObjectModeSingleCall);
()客戶端激活模式
對於客戶端激活模式使用的方法又有不同但區別不大看了代碼就一目了然
RemotingConfigurationApplicationName = ServiceMessage;
RemotingConfigurationRegisterActivatedServiceType(typeof(ServerRemoteObjectServerObject));
為什麼要在注冊對象方法前設置ApplicationName屬性呢?其實這個屬性就是該對象的URI對於WellKnown模式URI是放在RegisterWellKnownServiceType()方法的參數中當然也可以拿出來專門對ApplicationName屬性賦值而RegisterActivatedServiceType()方法的重載中沒有ApplicationName的參數所以必須分開
注銷通道
如果要關閉Remoting的服務則需要注銷通道也可以關閉對通道的監聽在Remoting中當我們注冊通道的時候就自動開啟了通道的監聽而如果關閉了對通道的監聽則該通道就無法接受客戶端的請求但通道仍然存在如果你想再一次注冊該通道會拋出異常
//獲得當前已注冊的通道
IChannel[] channels = ChannelServicesRegisteredChannels;
//關閉指定名為MyTcp的通道
foreach (IChannel eachChannel in channels)
{
if (eachChannelChannelName == MyTcp)
{
TcpChannel tcpChannel = (TcpChannel)eachChannel;
//關閉監聽
tcpChannelStopListening(null);
//注銷通道
ChannelServicesUnregisterChannel(tcpChannel);
}
}
代碼中RegisterdChannel屬性獲得的是當前已注冊的通道在Remoting中是允許同時注冊多個通道的這一點會在後面說明
四客戶端
客戶端主要做兩件事一是注冊通道這一點從圖一就可以看出Remoting中服務器端和客戶端都必須通過通道來傳遞消息以獲得遠程對象第二步則是獲得該遠程對象
注冊通道
TcpChannel channel = new TcpChannel();
ChannelServicesRegisterChannel(channel);
注意在客戶端實例化通道時是調用的默認構造函數即沒有傳遞端口號事實上這個端口號是缺一不可的只不過它的指定被放在後面作為了Uri的一部分
獲得遠程對象
與服務器端相同不同的激活模式決定了客戶端的實現方式也將不同不過這個區別僅僅是WellKnown激活模式和客戶端激活模式之間的區別而對於SingleTon和SingleCall模式客戶端的實現完全相同
() WellKnown激活模式
要獲得服務器端的知名遠程對象可通過Activator進程的GetObject()方法來獲得
ServerRemoteObjectServerObject serverObj = (ServerRemoteObjectServerObject)ActivatorGetObject(
typeof(ServerRemoteObjectServerObject) tcp://localhost:/ServiceMessage);
首先以WellKnown模式激活客戶端獲得對象的方法是使用GetObject()其中參數第一個是遠程對象的類型第二個參數就是服務器端的uri如果是http通道自然是//localhost:/ServiceMessage了因為我是用本地機所以這裡是localhost你可以用具體的服務器IP地址來代替它端口必須和服務器端的端口一致後面則是服務器定義的遠程對象服務名即ApplicationName屬性的內容
() 客戶端激活模式
如前所述WellKnown模式在客戶端創建對象時只能調用默認的構造函數上面的代碼就說明了這一點因為GetObject()方法不能傳遞構造函數的參數而客戶端激活模式則可以通過自定義的構造函數來創建遠程對象
客戶端激活模式有兩種方法
) 調用RemotingConfiguration的靜態方法RegisterActivatedClientType()這個方法返回值為Void它只是將遠程對象注冊在客戶端而已具體的實例化還需要調用對象類的構造函數
RemotingConfigurationRegisterActivatedClientType( typeof(ServerRemoteObjectServerObject)
tcp://localhost:/ServiceMessage);
ServerRemoteObjectServerObject serverObj = new ServerRemoteObjectServerObject();
) 調用進程Activator的CreateInstance()方法這個方法將創建方法參數指定類型的類對象它與前面的GetObject()不同的是它要在客戶端調用構造函數而GetObject()只是獲得對象而創建實例是在服務器端完成的CreateInstance()方法有很多個重載我著重說一下其中常用的兩個
a public static object CreateInstance(Type type object[] args object[] activationAttributes);
參數說明
type要創建的對象的類型
args 與要調用構造函數的參數數量順序和類型匹配的參數數組如果 args 為空數組或空引用(Visual Basic 中為 Nothing)則調用不帶任何參數的構造函數(默認構造函數)
activationAttributes 包含一個或多個可以參與激活的屬性的數組
這裡的參數args是一個object[]數組類型它可以傳遞要創建對象的構造函數中的參數從這裡其實可以得到一個結論WellKnown激活模式所傳遞的遠程對象類只能使用默認的構造函數而Activated模式則可以用戶自定義構造函數activationAttributes參數在這個方法中通常用來傳遞服務器的url
假設我們的遠程對象類ServerObject有個構造函數
ServerObject(string pNamestring pSexint pAge)
{
name = pName;
sex = pSex;
age = pAge;
}
那麼實現的代碼是
object[] attrs = {new UrlAttribute(tcp://localhost:/ServiceMessage)};
object[] objs = new object[];
objs[] = wayfarer;
objs[] = male;
objs[] = ;
ServerRemoteObjectServerObject = ActivatorCreateInstance(
typeof(ServerRemoteObjectServerObject)objsattrs);
可以看到objs[]數組傳遞的就是構造函數的參數
bpublic static ObjectHandle CreateInstance(string assemblyName string typeName object[] activationAttribute);
參數說明
assemblyName 將在其中查找名為 typeName 的類型的程序集的名稱如果 assemblyName 為空引用(Visual Basic 中為 Nothing)則搜索正在執行的程序集
typeName首選類型的名稱
activationAttributes 包含一個或多個可以參與激活的屬性的數組
參數說明一目了然注意這個方法返回值為ObjectHandle類型因此代碼與前不同
object[] attrs = {new UrlAttribute(tcp://localhost:/EchoMessage)};
ObjectHandle handle = ActivatorCreateInstance(ServerRemoteObject
ServerRemoteObjectServerObjectattrs);
ServerRemoteObjectServerObject obj = (ServerRemoteObjectServerObject)handleUnwrap();
這個方法實際上是調用的默認構造函數ObjectHandleUnwrap()方法是返回被包裝的對象
說明要使用UrlAttribute還需要在命名空間中添加using SystemRuntimeRemotingActivation;
五Remoting基礎的補充
通過上面的描述基本上已經完成了一個最簡單的Remoting程序這是一個標准的創建Remoting程序的方法但在實際開發過程中我們遇到的情況也許千奇百怪如果只掌握一種所謂的標准就妄想可以一招鮮吃遍天是不可能的
注冊多個通道
在Remoting中允許同時創建多個通道即根據不同的端口創建不同的通道但是Remoting要求通道的名字必須不同因為它要用來作為通道的唯一標識符雖然IChannel有ChannelName屬性但這個屬性是只讀的因此前面所述的創建通道的方法無法實現同時注冊多個通道的要求
這個時候我們必須用到SystemCollection中的IDictionary接口
注冊Tcp通道
IDictionary tcpProp = new Hashtable();
tcpProp[name] = tcp;
tcpProp[port] = ;
IChannel channel = new TcpChannel(tcpProp
new BinaryClientFormatterSinkProvider()
new BinaryServerFormatterSinkProvider());
ChannelServicesRegisterChannel(channel);
注冊Http通道
IDictionary httpProp = new Hashtable();
httpProp[name] = http;
httpProp[port] = ;
IChannel channel = new HttpChannel(httpProp
new SoapClientFormatterSinkProvider()
new SoapServerFormatterSinkProvider());
ChannelServicesRegisterChannel(channel);
在name屬性中定義不同的通道名稱就可以了
遠程對象元數據相關性
由於服務器端和客戶端都要用到遠程對象通常的方式是生成兩份完全相同的對象Dll分別添加引用不過為了代碼的安全性且降低客戶端對遠程對象元數據的相關性我們有必要對這種方式進行改動即在服務器端實現遠程對象而在客戶端則刪除這些實現的元數據
由於激活模式的不同在客戶端創建對象的方法也不同所以要分離元數據的相關性也應分為兩種情況
() WellKnown激活模式
通過接口來實現在服務器端提供接口和具體類的實現而在客戶端僅提供接口
public interface IServerObject
{
Person GetPersonInfo(string namestring sexint age);
}
public class ServerObject:MarshalByRefObjectIServerObject
{ }
注意兩邊生成該對象程序集的名字必須相同嚴格地說是命名空間的名字必須相同
() 客戶端激活模式
如前所述對於客戶端激活模式不管是使用靜態方法還是使用CreateInstance()方法都必須在客戶端調用構造函數實例化對象所以在客戶端我們提供的遠程對象就不能只提供接口而沒有類的實現實際上要做到與遠程對象元數據的分離可以由兩種方法供選擇
a利用WellKnown激活模式模擬客戶端激活模式
方法是利用設計模式中的抽象工廠下面的類圖表描述了總體解決方案
我們在服務器端的遠程對象中加上抽象工廠的接口和實現類
public interface IServerObject
{
Person GetPersonInfo(string namestring sexint age);
}
public interface IServerObjFactory
{
IServerObject CreateInstance();
}
public class ServerObject:MarshalByRefObjectIServerObject
{
public Person GetPersonInfo(string namestring sexint age)
{
Person person = new Person();
personName = name;
personSex = sex;
personAge = age;
return person;
}
}
public class ServerObjFactory:MarshalByRefObjectIServerObjFactory
{
public IServerObject CreateInstance()
{
return new ServerObject();
}
}
然後再客戶端的遠程對象中只提供工廠接口和原來的對象接口
public interface IServerObject
{
Person GetPersonInfo(string namestring sexint age);
}
public interface IServerObjFactory
{
IServerObject CreateInstance();
}
我們用WellKnown激活模式注冊遠程對象在服務器端
//傳遞對象
RemotingConfigurationRegisterWellKnownServiceType(
typeof(ServerRemoteObjectServerObjFactory)
ServiceMessageWellKnownObjectModeSingleCall);
注意這裡注冊的不是ServerObject類對象而是ServerObjFactory類對象
客戶端
ServerRemoteObjectIServerObjFactory serverFactory =
(ServerRemoteObjectIServerObjFactory) ActivatorGetObject(
typeof(ServerRemoteObjectIServerObjFactory)
tcp://localhost:/ServiceMessage);
ServerRemoteObjectIServerObject serverObj = serverFactoryCreateInstance();
為什麼說這是一種客戶端激活模式的模擬呢?從激活的方法來看我們是使用了SingleCall模式來激活對象但此時激活的並非我們要傳遞的遠程對象而是工廠對象如果客戶端要創建遠程對象還應該通過工廠對象的CreateInstance()方法來獲得而這個方法正是在客戶端調用的因此它的實現方式就等同於客戶端激活模式
b利用替代類來取代遠程對象的元數據
實際上我們可以用一個trick來欺騙Remoting這裡所說的替代類就是這個trick了既然是提供服務Remoting傳遞的遠程對象其實現的細節當然是放在服務器端而要在客戶端放對象的副本不過是因為客戶端必須調用構造函數而采取的無奈之舉既然具體的實現是在服務器端又為了能在客戶端實例化那麼在客戶端就實現這些好了至於實現的細節就不用管了
如果遠程對象有方法服務器端則提供方法實現而客戶端就提供這個方法就OK了至於裡面的實現你可以是拋出一個異常或者return 一個null值如果方法返回void那麼裡面可以是空關鍵是這個客戶端類對象要有這個方法這個方法的實現其實和方法的聲明差不多所以我說是一個trick方法如是構造函數也如此
還是用代碼來說明這種陰謀更直觀
服務器端
public class ServerObject:MarshalByRefObject
{
public ServerObject()
{}
public Person GetPersonInfo(string namestring sexint age)
{
Person person = new Person();
personName = name;
personSex = sex;
personAge = age;
return person;
}
}
客戶端
public class ServerObject:MarshalByRefObject
{
public ServerObj()
{
throw new SystemNotImplementedException();
}
public Person GetPersonInfo(string namestring sexint age)
{
throw new SystemNotImplementedException();
}
}
比較客戶端和服務器端客戶端的方法GetPersonInfo()沒有具體的實現細節只是拋出了一個異常或者直接寫上語句return null照樣OK我們稱客戶端的這個類為遠程對象的替代類
利用配置文件實現
前面所述的方法於服務器uri端口以及激活模式的設置是用代碼來完成的其實我們也可以用配置文件來設置這樣做有個好處因為這個配置文件是Xml文檔如果需要改變端口或其他我們就不需要修改程序並重新編譯而是只需要改變這個配置文件即可 () 服務器端的配置文件
<configuration>
<systemruntimeremoting>
<application name=ServerRemoting>
<service>
<wellknown mode=Singleton type=ServerRemoteObjectServerObject objectUri=ServiceMessage/>
</service>
<channels>
<channel ref=tcp port=/>
</channels>
</application>
</systemruntimeremoting>
</configuration>
如果是客戶端激活模式則把wellknown改為activated同時刪除mode屬性
把該配置文件放到服務器程序的應用程序文件夾中命名為nfig那麼前面的服務器端程序直接用這條語句即可
RemotingConfigurationConfigure(nfig);
() 客戶端配置文件
如果是客戶端激活模式修改和上面一樣調用也是使用RemotingConfigurationConfigure()方法來調用存儲在客戶端的配置文件
配置文件還可以放在nfig中如果客戶端程序是web應用程序則可以放在nfig中
啟動/關閉指定遠程對象
Remoting中沒有提供類似UnregisterWellKnownServiceType()的方法也即是說一旦通過注冊了遠程對象如果沒有關閉通道的話該對象就一直存在於通道中只要客戶端激活該對象就會創建對象實例如果Remoting傳送的只有一個遠程對象這不存在問題關閉通道就可以了如果傳送多個遠程對象呢?要關閉指定的遠程對象應該怎麼做?關閉之後又需要啟動又該如何?
我們注意到在Remoting中提供了Marshal()和Disconnect()方法答案就在這裡Marshal()方法是將MarshalByRefObject類對象轉化為ObjRef類對象這個對象是存儲生成代理以與遠程對象通訊所需的所有相關信息這樣就可以將該實例序列化以便在應用程序域之間以及通過網絡進行傳輸客戶端就可以調用了而Disconnect()方法則將具體的實例對象從通道中斷開
方法如下
首先注冊通道
TcpChannel channel = new TcpChannel();
ChannelServicesRegisterChannel(channel);
接著啟動服務
先在服務器端實例化遠程對象
ServerObject obj = new ServerObject();
然後注冊該對象注意這裡不用RemotingConfigurationRegisterWellKnownServiceType()而是使用RemotingServicesMarshal()
ObjRef objrefWellKnown = RemotingServicesMarshal(obj ServiceMessage);
如果要注銷對象則
RemotingServicesDisconnect(obj);
要注意這裡Disconnect的類對象必須是前面實例化的對象正因為此我們可以根據需要創建指定的遠程對象而關閉時則Disconnect之前實例化的對象
至於客戶端的調用和前面WellKnown模式的方法相同仍然是通過ActivatorGetObject()來獲得但從實現代碼來看我們會注意到一個問題由於服務器端是顯式的實例化了遠程對象因此不管客戶端有多少是否相同它們調用的都是同一個遠程對象因此我們將這個方法稱為模擬的SingleTon模式
客戶端激活模式
我們也可以通過Marshal()和Disconnect()來模擬客戶端激活模式首先我們來回顧遠程對象元數據相關性一節在這一節中我說到采用設計模式的抽象工廠來創建對象實例以此用SingleCall模式來模擬客戶端激活模式在仔細想想前面的模擬的SingleTon模式是不是答案就將呼之欲出呢?
在模擬的SingleTon模式中我們是將具體的遠程對象實例進行Marshal以此讓客戶端獲得該對象的引用信息那麼我們換一種思路當我們用抽象工廠提供接口工廠類實現創建遠程對象的方法然後我們在服務器端創建工廠類實例再將這個工廠類實例進行Marshal而客戶端獲取對象時不是獲取具體的遠程對象而是獲取具體的工廠類對象然後再調用CreateInstance()方法來創建具體的遠程對象實例此時對於多個客戶端而言調用的是同一個工廠類對象然而遠程對象是在各個客戶端自己創建的因此對於遠程對象而言則是由客戶端激活創建的是不同對象了
當我們要啟動/關閉指定對象時只需要用Disconnet()方法來注銷工廠類對象就可以了
六小結
MicrosoftNet Remoting真可以說是博大精深整個Remoting的內容不是我這一篇小文所能盡述的更不是我這個Remoting的初學者所能掌握的王國維在《人間詞話》一書中寫到古今之成大事業大學問者必經過三種境界昨夜西風凋碧樹獨上高樓望盡天涯路此第一境界也衣帶漸寬終不悔為伊消得人憔悴此第二境界也眾裡尋他千百度蓦然回首那人卻在燈火闌珊處此第三境界也如以此來形容我對Remoting的學習還處於獨上高樓望盡天涯路的時候真可以說還未曾登堂入室
或許需得衣帶漸寬學得Remoting終不悔方才可以蓦然回首吧
From:http://tw.wingwit.com/Article/program/net/201311/13914.html