如何建立 .NET Remoting 專案

最近有人問到一個老骨董 .NET Remoting,當需要跨程式資料交換,它是一個選項

Wiki 這樣介紹它

以下內容出自:https://zh.wikipedia.org/zh-tw/.NET_Remoting

.NET遠端處理[1].NET Remoting )是微軟 .NET Framework 中的一種網路通訊技術,與 XML Web Service 不同的是,它可以使用 SOAP 以外的協定來通訊,而在伺服端和用戶端之間所操作的方法近乎相同,用戶端可以不必考慮使用的協定,即可存取伺服端所開放的物件。這個技術與是由 Distributed COM所發展而來的,與DCOM最大的不同是,DCOM有限制使用 TCP Port,但.NET Remoting 可以選擇使用 TCP 或 HTTP 的方式通訊,而資料可以利用 SOAP 或二進位傳輸方式在網路上流動,二進位的傳輸效能是 SOAP 所不能比的,但 SOAP 卻可以得到和 Web Service 相互溝通的能力,因此 .NET Remoting 的設計彈性較大。』

table of contents

在 .NET Remoting 中是利用通道 (channel) 來實現兩個應用程式域之間物件的通訊。下圖出自 https://zh.wikipedia.org/wiki/.NET_Remoting

NET Remoting - 维基百科,自由的百科全书

.NET Remoting 有兩種通道

Tcp 和 Http,System.Runtime.Remoting.Channel 中定義了 IChannel 介面

TcpChannel

  • 命名空間:System.Runtime.Remoting.Channel.Tcp.TcpChannel
  • 使用 Socket 傳輸序列化訊息流,預設使用二進位序列化,具有更高的傳輸效能。

HttpChannel

  • 命名空間:System.Runtime.Remoting.Channel.Tcp.HttpChannel
  • HttpChannel 使用Http協議,(可穿越防火牆, 80 Port),傳輸序列化訊息流,預設使用 Soap 序列化。

 

遠端物件的啟用

伺服器端啟用

又叫做 WellKnow ,根據 URI 和埠號發佈服務,有兩種模式

  • SingleCall:在每一次用戶端呼叫時都生成一個執行個體 (無狀態)。
  • SingleTon:在第一次呼叫時就生成執行個體,之後每一次呼叫都使用相同的執行個體。

客戶端啟用

  • 用 Activator.GetObject 建立通道,只能呼叫預設的建構函數,部署用戶端程式較方便
  • 用 Activator.CreateInstance 建立通道,可呼叫傳遞參數給建構函數,部署用戶端程式較不方便

 

伺服器端和用戶端的傳遞

遠端物件必須要繼承 System.MarshalByRefObject 類別,System.MarshalByRefObject 用來跨應用程式的通訊基礎類別

若需要用類別或是結構,作為參數需要加上 [Serializable]

[Serializable]
public class Person
{
    public string Name { get; set; }

    public string Sex { get; set; }

    public int Age { get; set; }
}

 

實作步驟

Lab.NetRemoting.Core

新增 ITrMessage.cs,內容如下

namespace Lab.NetRemoting.Core
{
    public interface ITrMessage
    {
        string GetName();
 
        DateTime GetNow();
 
        Person GetPerson();
    }
}

 

讓用戶端依賴 ITrMessage,並用 Activator.GetObject 建立通道,不要依賴 TrMessage,未來 TrMessage 實作內容異動時,用戶端不需要更新

 

Lab.NetRemoting.Implement

建立類別庫專案,名為  Lab.NetRemoting.Implement

參考

  • Lab.NetRemoting.Core
  • Faker.Net from nuget,Install-Package Faker.Net

並新增以下 TrMessage.cs,TrMessage 實作了 MarshalByRefObject,它需要讓 Client 和 Server 使用

namespace Lab.NetRemoting.Implement
{
    public class TrMessage : MarshalByRefObject, ITrMessage
    {
        private readonly string _name = "yao";

        public TrMessage()
        {
            Console.WriteLine($"{DateTime.Now:yyyy/MM/dd hh:mm:ss}, Create Construct");
        }

        public TrMessage(string name)
        {
            this._name = name;
            Console.WriteLine($"{DateTime.Now:yyyy/MM/dd hh:mm:ss}, Create Construct pass {name}");
        }

        public string GetName()
        {
            Console.WriteLine($"{DateTime.Now:yyyy/MM/dd hh:mm:ss}, Call GetName Method");
            return this._name;
        }

        public DateTime GetNow()
        {
            Console.WriteLine($"{DateTime.Now:yyyy/MM/dd hh:mm:ss}, Call GetNow Method");
            return DateTime.Now;
        }

        public Person GetPerson()
        {
            var person = new Person
            {
                Name   = Faker.Name.FullName(),
                Gender = Faker.Boolean.Random() ? "M" : "F",
                Age    = Faker.RandomNumber.Next(1, 100)
            };
            return person;
        }
    }
}

 

Lab.NetRemoting.Server

建立 Console App 專案,名為  Lab.NetRemoting.Server

參考

  • Lab.NetRemoting.Implement
  • System.Runtime.Remoting

建立 TcpChannel 並註冊,代碼如下:

namespace Lab.NetRemoting.Server
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            
            try
            {
                var port = 9527;
                var tcpChannel = new TcpChannel(port);
                ChannelServices.RegisterChannel(tcpChannel, false);
                RemotingConfiguration.RegisterWellKnownServiceType(typeof(TrMessage)
                                                                 , "RemotingTest", WellKnownObjectMode.Singleton);
                Console.WriteLine("按任意鍵離開!");
                Console.ReadLine();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}

 

註冊通道

var port = 9527;
var tcpChannel = new TcpChannel(port);
ChannelServices.RegisterChannel(tcpChannel, false);

註冊遠端物件

註冊了通道後,要能啟用遠端物件,必須在通道中註冊該物件。根據啟用模式的不同,註冊物件的方法也不同。

SingleTon 模式

  • 在每一次用戶端呼叫時都生成一個執行個體 (無狀態)。
  • "RemotingTest" 是 Uri,用戶端訪問的位置
  • 通過靜態方法 RemotingConfiguration.RegisterWellKnownServiceType() 來實現RemotingConfiguration.RegisterWellKnownServiceType(typeof(TrMessage), "RemotingTest", WellKnownObjectMode.SingleTon);
  • 另一種作法則是透過 RemotingServices.Marshal() 來實現,這裡要實體化 TrMessage
var message = new TrMessage();
var objRef = RemotingServices.Marshal(message, "RemotingTest");
 
Console.WriteLine($"{DateTime.Now}, Server 已啟動");
Console.WriteLine("按任意鍵離開!");
Console.ReadLine();
 
RemotingServices.Disconnect(message);

SingleCall 模式

跟 SingleTon 差不多只是 WellKnownObjectMode舉列換成 SingleCall

RemotingConfiguration.RegisterWellKnownServiceType(typeof(TrMessage), "RemotingTest", WellKnownObjectMode.SingleCall);

 

客戶端啟用模式

"RemotingTest" 是 Url

RemotingConfiguration.ApplicationName = "RemotingTest";
RemotingConfiguration.RegisterActivatedServiceType(typeof(TrMessage));

 

Lab.NetRemoting.Client

建立 WinFrom App 專案,名為  Lab.NetRemoting.Client

參考

  • Lab.NetRemoting.Core

代碼如下:

namespace Lab.NetRemoting.Client
{
    public partial class Form1 : Form
    {
        private readonly ITrMessage _trMessage;
        private readonly string     _url = "tcp://127.0.0.1:9527/RemotingTest";

        public Form1()
        {
            this.InitializeComponent();

            //WellKnown 啟用模式,在客戶端建立物件時,只能呼叫預設的建構函式
            this._trMessage = (ITrMessage) Activator.GetObject(typeof(ITrMessage), this._url);
            Console.WriteLine($"{DateTime.Now}, 已連接伺服器:{this._url}");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                var now    = this._trMessage.GetNow();
                var name   = this._trMessage.GetName();
                var person = this._trMessage.GetPerson();
                var msg = $"Hi, my name is {name}\r\n"         +
                          $"Now:{now:yyyy-MM-dd hh:mm:ss}\r\n" +
                          $"Data:{JsonConvert.SerializeObject(person)}";

                //MessageBox.Show(msg);
                Console.WriteLine(msg);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

 

取得得遠端物件

 

WellKnown 啟用模式

用 System.Activator.GetObject() 方法,來獲得伺服器端的具名遠端物件

private readonly string     _url = "tcp://127.0.0.1:9527/RemotingTest";
private readonly ITrMessage _trMessage;
 
public Form1()
{
    this.InitializeComponent();
    if (this._trMessage == null)
    {
        this._trMessage = (ITrMessage) Activator.GetObject(typeof(ITrMessage), this._url);
        Console.WriteLine($"{DateTime.Now}, 已連接伺服器:{this._url}");
    }
}

 

執行畫面如下:

 

客戶端啟用模式

WellKnown模式在客戶端建立物件時,只能呼叫預設的建構函式。

//客戶端啟用模式 
RemotingConfiguration.RegisterActivatedClientType(typeof(ITrMessage), this._url); 

 

要傳入參數給建構函數,必須要依賴 Lab.NetRemoting.Implement.TrMessage Class,將來它有變化時,用戶端的部署難度就會增加,這點必須要考量

直接 new 

this._trMessage = new TrMessage("余小章");

 

動態實體化遠端物件

//動態實體化遠端物件
object[] attrs = {new UrlAttribute(this._url)};
var      objs  = new object[1];
objs[0]         = "余小章";
this._trMessage = (ITrMessage) Activator.CreateInstance(typeof(TrMessage), objs, attrs);

 

為了讓客戶端能傳入參數呼叫建構函數,又不需要依賴 Lab.NetRemoting.Implement.TrMessage class

在 Lab.NetRemoting.Core 專案新增 ITrMessageFactory.cs

namespace Lab.NetRemoting.Core
{
    public interface ITrMessageFactory
    {
        string Url { get; set; }
 
        ITrMessage CreateInstance();
 
        ITrMessage CreateInstance(string name);
    }
}

 

Lab.NetRemoting.Implement 專案新增 TrMessageFactory.cs

namespace Lab.NetRemoting.Core
{
    public class TrMessageFactory : MarshalByRefObject, ITrMessageFactory
    {
        public string Url { get; set; }
 
        public ITrMessage CreateInstance()
        {
            return new TrMessage();
        }
 
        public ITrMessage CreateInstance(string name)
        {
            return new TrMessage(name);
        }
    }
}

 

伺服器端原本註冊 TrMessage 改成 TrMessageFactory

RemotingConfiguration.RegisterWellKnownServiceType(typeof(TrMessageFactory),
                                                   "RemotingTest",
                                                   WellKnownObjectMode.Singleton);

 

用戶端原本依賴 ITrMessage 改成 ITrMessageFactory

var serverFactory = (ITrMessageFactory)Activator.GetObject(typeof(ITrMessageFactory), this._url);
serverFactory.Url = this._url;
this._trMessage = serverFactory.CreateInstance("余小章");

 

透過這樣的技巧,只要在兩邊協議的合約不動情況下,TrMessage 調整的細節都不會影響用戶端

 

如下圖,建構函數注入了余小章

 

參考連結

https://zh.wikipedia.org/wiki/.NET_Remoting

https://www.itread01.com/article/1464570786.html

 

範例專案

https://github.com/yaochangyu/sample.dotblog/tree/master/Remoting/Lab.NetRemoting

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo