[C#] IpcChannel雙向通信,參考MAF的AddInProcess開發插件,服務斷開重新打開及服務生存周期管理


先前項目太忙了,沒時間寫博客,發現了一個有趣的東西,匆匆忙忙就寫完了,先描述一下需求背景:客戶端有幾張百萬級別的表需要聯合統計(如果是最大權限的賬號),改變查詢條件又要重新統計,因此常常sql執行還沒結束就取消了,但不管關閉數據庫還是結束線程都必須等到sql執行結束,無奈之下只能考慮進程通信,取消就直接殺掉進程,首先考慮的是現成的MAF,於是有了下面的代碼:

 僅僅一個接口就需要建這么多個項目,DLL多點也就算了,相關文件夾還必須如上圖這么放,這么放就算了,但是公共的DLL(如System.Data.SQLite.dll)還不能一起用,進程隔離的話公共DLL必須插件、主程序各自一份,這不是打包的時候莫名增加了體積嗎?這肯定不能接受啊,但我還沒有放棄,因為ProcessStartInfo可以指定工作目錄,既我只要指定主程序根目錄為工作目錄,還是可以實現DLL共享,但是絕望的是AddInProcess是密封的,無法控制它是如何啟動的。雖然不滿足期望,好處是在看它的源碼時發現是基於IpcChannel通信的,去官網了解之后,意外地發現使用很簡單,再結合MAF里AddInProcess的思想,我的實現如下:

服務端提供什么服務應有客服端指定,既需要一個空白進程,負責加載服務所在程序集,並提供服務,經過一段時間磨礪,發現不需要創建客戶端,下面的代碼就可以實現:

// 客服端啟動代理類
public sealed class IpcProcessProxy<T>
{
    public T Proxy => proxy;
    public event EventHandler Exited;

    private Process process;
    private T proxy;

    public bool Start(int startUpTimeout = 3000)
    {
        var type = typeof(T);   // 鍥約類型

        var ipcServerDic = new Dictionary<string, string>
        {
            ["name"] = "ipcServer",     // 服務端名稱
            ["portName"] = Guid.NewGuid().ToString("N")     // 服務端地址
        };
        var contractDic = new Dictionary<string, string>
        {
            ["assemblyName"] = type.Assembly.GetName().Name,    // 程序集
            ["typeName"] = typeName,        // 類型名稱
            ["objectUri"] = objectUri       // Ipc提供的服務名稱
        };

        var arg1 = String.Join("&", ipcServerDic.Select(q => $"{q.Key}={q.Value}"));
        var arg2 = String.Join("&", contractDic.Select(q => $"{q.Key}={q.Value}"));

        var startInfo = new ProcessStartInfo
        {
            CreateNoWindow = true,
            UseShellExecute = false,
            FileName = "IpcProcess.exe",
            Arguments = $"{arg1} {arg2}",
            WorkingDirectory = Environment.CurrentDirectory
        };

        process = Process.Start(startInfo);     // 啟動進程,並根據參數啟動服務
        process.Exited += Exited;

        // 等待服務端服務啟動
        using (var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + ipcServerDic["portName"]))
        {
            if (readyEvent.WaitOne(startUpTimeout, false))
            {
                var url = $"ipc://{ipcServerDic["portName"]}/{contractDic["objectUri"]}";
                proxy = (T)Activator.GetObject(typeof(T), url);     // 通過約定url,獲取服務端服務代理對象
                return proxy != null;
            }
        }
    }
}

// 服務端
static void Main(string[] args)
{
    var arg1 = args[0];
    var arg2 = args[1];

    var ipcServerDic = new Dictionary<string, string>();
    var contractDic = new Dictionary<string, string>()

    foreach (var kvStr in arg1.Split('&'))
    {
        var kv = kvStr.Split('=');
        ipcServerDic.Add(kv[0], kv[1]);
    }
    foreach (var kvStr in arg2.Split('&'))
    {
        var kv = kvStr.Split('=');
        contractDic.Add(kv[0], kv[1]);
    }

    var assembly = Assembly.Load(contractDic["assemblyName"]);      // 加載程序集
    var type = assembly.GetType(contractDic["typeName"], true, false)

    // 注冊Ipc服務
    var channel = new IpcServerChannel(ipcServerDic, new BinaryServerFormatterSinkProvider());
    ChannelServices.RegisterChannel(channel, false)

    // 創建服務對象,並創建代理
    var serverObj = (MarshalByRefObject)Activator.CreateInstance(type);
    RemotingServices.Marshal(serverObj, contractDic["objectUri"]);
    
    // 通知客服端,服務已經啟動了
    var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "IpcProxy:" + ipcServerDic["portName"]);
    readyEvent.Set();

    Console.ReadLine();
}

這里和官方最大的區別是沒有在服務端使用RegisterWellKnownServiceType,也沒有創建客服端,更沒有在客服端使用WellKnownClientTypeEntry,而是在服務端使用RemotingServices.Marshal開啟一個服務代理,在客服端使用Activator.GetObject獲取服務代理對象,這樣做的好處是:在服務端,先拿到對象(如果繼承特定類型,可進行特殊處理,后續雙向通信會用到),在客服端,不需要通過new()獲取服務,而且說不定其他地方new()這個類型並不想用代理服務,再就是Activator.GetObject不需要直接引用類型,可以是繼承的接口,這樣使代碼更低耦合。

  以上就是簡單的IpcChannel單向通信,適用於調用服務並返回結果,但不適用於大多數插件場景,比如分批返回多個結果,或者某個方法傳入回調函數通知進度;一個功能復雜的插件往往需要雙向通信,我就有這方面的需求,因此開始不斷地探索...

嘗試一:給遠程對象添加一個事件Event,這樣客服端監聽這個事件就可以得到通知,但發現只要某個對象監聽了這個事件,那么它也會被視為服務端對象,最后發現客服端永遠也不可能拿到這個事件。

嘗試二:在服務端注冊一個IpcServerChannel,在客服端注冊一個IpcClientChannel,然后研究他們之間是怎么通信的,突破口應該就在他們的構造函數的第二個參數,分別實現了發送消息和處理請求的方法,但是已經超出我的理解范圍了。

嘗試三:在服務端和客服端分別注冊一個IpcServerChannel,當服務端想要回調給客戶端時,就向客服端發送消息,這就必須要求服務端的代理服務對象能拿到客服端的代理服務對象,這樣才能調用客服端代理服務對象方法,思路是這樣的:

1、首選在客服端注冊Ipc服務,並提供代理服務

2、啟動服務端進程,並啟動自身的服務(繼承特定類型,添加一個事件,當需要回調時觸發),根據約定,找到客服端提供的代理服務,監聽自身服務回調事件,事件觸發時調用客服端代理服務方法

3、客服端代理服務收到請求時,再觸發類似事件,提供給外部調用者使用

下面是啟動代理服務的核心代碼:

public class IpcProcessProxy : ISponsor
{
    static IpcProcessProxy()
    {
        // 客服端服務地址,將此作為參數傳遞給服務進程
        ipcClientDic = new Dictionary<string, string>
        {
            ["name"] = "ipcClient",
            ["portName"] = Guid.NewGuid().ToString("N")
        };
        // 客服端的代理對象
        callRemoteObj = new CallRemoteObject(ipcClientDic["portName"]);
        callRemoteObj.Called += OnCalled;   // 當收到回調時,觸發此事件
    }
    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
    protected void Start()
    {
        try
        {
            lock (locker)
            {
                // 創建Ipc服務
                if (channel == null)
                    channel = new IpcServerChannel(ipcClientDic, new BinaryServerFormatterSinkProvider());
                // 注冊服務
                if (ChannelServices.GetChannel(channel.ChannelName) == null)
                    ChannelServices.RegisterChannel(channel, false);
                // 啟動代理服務
                objRef = RemotingServices.Marshal(callRemoteObj, "CallRemoteObject");
                // 管理服務的生存周期
                if (lease == null)
                {
                    lease = (ILease)RemotingServices.GetLifetimeService(callRemoteObj);
                    lease.Register(this);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            Console.WriteLine("啟動Ipc通信服務失敗!");
        }
    }
    public TimeSpan Renewal(ILease lease)
    {
        Console.WriteLine("租約到期,生存狀態:" + lease.CurrentState);
        return TimeSpan.FromSeconds(30);
    }
    /// <summary>
    /// 注冊要監聽的回調方法
    /// </summary>
    /// <param name="url">服務端Ipc地址</param>
    /// <param name="methodName">回調方法名稱</param>
    /// <param name="callback">監聽回調方法的委托</param>
    protected void RegisterCallback(string url, string methodName, Delegate callback)
    {
        if (callback == null)
            return;
        lock (locker)
        {
            var callbacks = urls.GetOrAdd(url, new Dictionary<string, Delegate>());
            callbacks.Set(methodName, callback);
        }
    }
    protected void UnRegisterCallback(string url)
    {
        lock (locker)
        {
            urls.Remove(url);
        }
    }
    
    /// <summary>
    /// 收到服務端的回調
    /// </summary>
    /// <param name="url">服務端Ipc地址</param>
    /// <param name="methodName">回調方法名稱</param>
    /// <param name="parameters">回調方法參數</param>
    private static void OnCalled(string url, string methodName, object[] parameters)
    {
        lock (locker)
        {
            // 找到監聽該地址且指定方法的委托
            if (!urls.TryGetValue(url, out Dictionary<string, Delegate> callbacks) || !callbacks.TryGetValue(methodName, out Delegate callback) || callback == null)
                return;
            // 執行委托
            callback.DynamicInvoke(parameters);
        }
    }
    protected void Close()
    {
        if (lease != null)
        {
            lease.Unregister(this);
            lease = null;
        }
        if (objRef != null)
        {
            RemotingServices.Unmarshal(objRef);
            objRef = null;
        }
        if (callRemoteObj != null)
        {
            RemotingServices.Disconnect(callRemoteObj);
            callRemoteObj = null;
        }
        if (channel != null)
        {
            ChannelServices.UnregisterChannel(channel);
            channel = null;
        }
    }
    private static IpcServerChannel channel;
    private static CallRemoteObject callRemoteObj;
    private static ObjRef objRef;
    private static Dictionary<string, Dictionary<string, Delegate>> urls = new Dictionary<string, Dictionary<string, Delegate>>();
    protected static Dictionary<string, string> ipcClientDic;
    private static object locker = new object();
    private static ILease lease;
}

項目資源我上傳到了CSDN(IpcChannel雙向通信,參考MAF的AddInProcess開發插件,服務斷開重新打開及服務生存周期管理),本人博客小白一個,有時需要下載CSDN的資源卻沒有積分,所以想到這個方法,理解萬歲!

使用這套IpcChannel雙向通信也有一個星期,遇到了幾個問題,比如異常類型:System.Runtime.Remoting.RemotingException,異常信息:對象“/CallRemoteObject”已經斷開連接或不在服務器上。對於此問題可能是我總是強制結束進程,在傳輸過程中斷開導致的服務掛掉,但測試發現,無論在發送時結束進程,還是在接收時,都無法復現這個異常,希望知道的留言告訴我怎么必然復現,因此只能把希望寄托於如何重新打開服務,經過嘗試發現只要重新調用RemotingServices.Marshal就行,記住代理的服務對象還是同一個,如果使用新的對象,就會提示:System.Runtime.Remoting.RemotingException: 找到與同一 URI“/49cb660d_9357_4a1a_9732_4488fbad07eb/CallRemoteObject”關聯 的兩個不同對象。

為了更穩妥,因為還是無法找到服務關閉的原因,使用了RemotingServices.GetLifetimeService管理服務的生存周期,應該是生效了,后面幾天都沒有看到類似斷開連接或不在服務器上的異常。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM