WPF 封裝 dotnet remoting 調用其他進程

本文告訴大家一個封裝好的庫,使用這個庫可以快速搭建多進程相互使用。

WPF 使用RPC調用其他進程 已經告訴大家調用的原理,但是大家可以看到,如果自己寫一個框架是比較難的。

因爲我經常調用 C++ 代碼,如果C++出現異常,軟件會直接退出,所以我就想把 C++ 代碼放在其他進程,這樣C++運行出現異常了,軟件也不會直接退出。

但是如果每次都需要自己寫相同的代碼,我是不同意的,因爲很容易寫錯。

因爲我的代碼放在了公司代碼使用,所以我不會把源代碼放出來,但是大家可以通過複製本文的類來創建框架。

創建端口

創建端口包含一個接口和一個類,因爲我需要在一個設備運行,所以爲了性能,我不使用 http 連接,這時的端口可以使用一個字符串

爲了區分兩個程序,我把程序分爲兩個,一個是 WPF 程序,一個是C++程序。因爲另一個程序主要是運行 C++ 代碼。

爲了讓兩個程序能聯繫,就需要約定端口,因爲這是框架,可能需要使用 http 通信,所以就需要寫一個接口,如果需要使用 http 修改端口就繼承,這樣框架纔可以在很多地方使用。

    /// <summary>
    ///     創建端口
    /// </summary>
    public interface IPortGenerator
    {
        /// <summary>
        ///     獲得端口
        /// </summary>
        /// <returns></returns>
        string GetPort();
    }

因爲我需要在一個系統運行兩個程序,所以我的端口是這樣寫

    /// <summary>
    ///     創建端口
    /// </summary>
    public class PortGenerator : IPortGenerator
    {
        /// <summary>
        ///     獲得一個隨機端口
        /// </summary>
        /// <returns></returns>
        public string GetPort()
        {
            const int maxPort = 65535;
            const int minPort = 49152;
            return (Process.GetCurrentProcess().Id % (maxPort - minPort) + minPort).ToString(); //獲得進程ID作爲端口號
        }
    }

調用軟件

從 WPF 程序調用 C++ 程序需要告訴他參數,參數就是剛纔的端口

這時 C++ 程序使用命令行解析,請看安利一款非常好用的命令行參數庫:McMaster.Extensions.CommandLineUtils - walterlv

創建一個類用來解析

    public class Options
    {
        [Option('p', "port", Required = true, HelpText = "遠程開啓的端口")]
        public string Port { get; set; }
    }

解析只需要使用主函數傳入的 args 就可以拿到端口

            Parser.Default.ParseArguments<Options>(args).MapResult(options =>
            {
                Console.WriteLine("端口號" + options.Port);
                new Thread(() =>
                {
                    Console.WriteLine("啓動庫");
                    Run(options.Port, assembly);
                    Console.WriteLine("啓動完成");
                }).Start();
                return 0;
            }, _ => -1);

這裏啓動一個新的線程因爲C++程序需要使用另一個線程去計算,主函數的線程會如果沒有使用 Console.Read() 會退出。

現在 WPF 可以開始調用 C++ 程序,使用下面的代碼進行管理

    /// <summary>
    ///     管理其他進程
    /// </summary>
    public class RemoteProcessManager
    {
        /// <summary>
        ///     管理其他進程
        /// </summary>
        /// <param name="processName">進程名,用於啓動進程</param>
        public RemoteProcessManager(string processName)
        {
            ProcessName = processName;
        }

        /// <summary>
        ///     獲取管理的進程
        /// </summary>
        public string ProcessName { get; }

        /// <summary>
        ///     獲取是否連接
        /// </summary>
        public bool IsConnected { get; private set; }

        /// <summary>
        ///     創建端口
        /// </summary>
        public IPortGenerator PortGenerator { get; set; }


        /// <summary>
        ///     遠程應用退出時,建議監聽後使用異常
        /// </summary>
        public event EventHandler RemoteExited;

        /// <summary>
        ///     連接
        /// </summary>
        public void Connect()
        {
            if (IsConnected)
            {
                throw new InvalidOperationException("禁止多次連接");
            }

            IsConnected = true;

            if (PortGenerator == null)
            {
                PortGenerator = new PortGenerator();
            }

            Port = PortGenerator.GetPort();

            //啓動遠程進程
            var st = new ProcessStartInfo(ProcessName, "-p " + Port);

#if !DEBUG
            st.CreateNoWindow = true;
            st.WindowStyle = ProcessWindowStyle.Hidden;
#endif
            var remoteGuardian = Process.Start(st); //監控遠程應用

            if (remoteGuardian == null)
            {
                throw new RemoteProcessStartException("啓動時出現返回值爲空")
                {
                    Data = ProcessStartInfo
                };
            }

            ProcessId = remoteGuardian.Id;
            remoteGuardian.EnableRaisingEvents = true;
            remoteGuardian.Exited += RemoteGuardian_OnExited;

            CleanRegister();

            _channel = Terminal.CreatChannel(); //客戶端

            ChannelServices.RegisterChannel(_channel, false);
        }

        /// <summary>
        ///     從遠程獲得實例
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T GetObject<T>()
        {
            CheckProcess();
            return (T) Activator.GetObject(typeof(T),
                "Ipc://" + Port + "/" + typeof(T).Name);
        }

        /// <summary>
        ///     結束遠程進程
        /// </summary>
        public void ExitProcess()
        {
            _isManualExit = true;

            var remoteProcessBrake = GetObject<RemoteProcessBrake>();
#pragma warning disable 618
            remoteProcessBrake.OnExit();
#pragma warning restore 618
        }

        private IChannel _channel;

        /// <summary>
        ///     是否手動退出遠程應用
        /// </summary>
        private bool _isManualExit;

        private void RemoteGuardian_OnExited(object sender, EventArgs e)
        {
            IsConnected = false;
            ProcessId = -1;

            //手動退出就不需要事件
            if (_isManualExit)
            {
                return;
            }

            //防止360等垃圾軟件覺得這個應用可以退出
            RemoteExited?.Invoke(sender, e);

            //即使被你退出了,我還是要啓動,但是可能存在一些地方使用的變量放在本地,所以拿到的值就是之前的應用,還是需要用戶重啓
            Connect();
        }

        /// <summary>
        ///     清理註冊,因爲一個信道只能註冊
        /// </summary>
        private void CleanRegister()
        {
            if (_channel != null)
            {
                ChannelServices.UnregisterChannel(_channel);
            }
        }


        private int ProcessId { set; get; } = -1;
        private string Port { get; set; }

        private void CheckProcess()
        {
            if (!IsConnected || ProcessId == -1)
            {
                throw new NativeProcessException("遠程應用已經意外結束或沒有啓動");
            }

            var processes = Process.GetProcesses();
            if (ProcessId != -1 && processes.All(temp => temp.Id != ProcessId))
            {
#if DEBUG
                throw new NativeProcessException();
#else
                IsConnected = false;
                Connect();
#endif
            }
        }
    }

注意現在的代碼存在很多類沒有引用

從上面代碼可以看到,這裏使用的連接是 IPC ,因爲調用其他進程是在同一個電腦,所以這時使用 IPC 的效率會比 http 和 tcp 高。原因是 IPC 是進程間通信,效率和內存共享差不多。而使用 http 或 tcp 需要把信息發送給本地巡迴,然後再返回。而且使用 http 需要做額外的過程,需要走 http 的協議。使用 tcp 需要使用握手,性能都比 IPC 差。

運行的類

所有需要在 C++ 程序運行的類都需要註冊,因爲C++程序需要找到程序集所有符合的類,所以需要這些類標記

    /// <summary>
    /// 放在遠程的實例
    /// <remarks>請不要在代碼保存實例</remarks>
    /// </summary>
    interface IRemote
    {

    }
    /// <summary>
    ///  共享使用的類,這個類會在遠程進程創建
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class RemoteAttribute : Attribute
    {

    }

例如有一個類需要在 C++ 程序運行,在 WPF 程序使用,那麼就需要這樣寫

    [Remote]
    public class BaltrartularLouronay : MarshalByRefObject
    {
        public void TerecaLesi()
        {
            Console.WriteLine("調用");
        }
    }

繼承 MarshalByRefObject 標記 Remote 就可以了

運行C++程序

運行需要獲得程序所有類,需要在C++程序使用的類,實現它。

    /// <summary>
    /// 遠程本機進程
    /// </summary>
    public class RemoteNative
    {
        /// <summary>
        ///  加載程序集
        /// </summary>
        /// <param name="args">傳入參數</param>
        /// <param name="assembly"></param>
        public void Run(string[] args, Assembly assembly)
        {
            Parser.Default.ParseArguments<Options>(args).MapResult(options =>
            {
                Console.WriteLine("端口號" + options.Port);
                new Thread(() =>
                {
                    Console.WriteLine("啓動庫");
                    Run(options.Port, assembly);
                    Console.WriteLine("啓動完成");
                }).Start();
                return 0;
            }, _ => -1);
        }

        /// <summary>
        ///  加載程序集
        /// </summary>
        /// <param name="port">端口</param>
        /// <param name="assembly">程序集</param>
        public void Run(string port, Assembly assembly)
        {
            _channel = Terminal.CreatChannel(port);

            ChannelServices.RegisterChannel(_channel, false);

            //設置租用管理器的初始租用時間爲無限 [http://www.cnblogs.com/wayfarer/archive/2004/08/05/30437.html](http://www.cnblogs.com/wayfarer/archive/2004/08/05/30437.html )
            LifetimeServices.LeaseTime = TimeSpan.Zero;

            //註冊實例
            var remoteProcessBrake = new RemoteProcessBrake();

            remoteProcessBrake.Exit += RemoteProcessBrake_Exit;

            // 防止對象回收
            // 如果不使用 var objRef = x 那麼在運行就發現 System.Runtime.Remoting.RemotingException:“找不到請求的服務”
            var objRef = RemotingServices.Marshal(remoteProcessBrake, remoteProcessBrake.GetType().Name);

            _objRefList.Add(objRef);

            Init(assembly);
        }

        private void RemoteProcessBrake_Exit(object sender, EventArgs e)
        {
            Environment.Exit(0);
        }

        private void Init(Assembly assembly)
        {
            foreach (var temp in assembly.GetTypes().Where(temp => temp.GetCustomAttribute<RemoteAttribute>() != null))
            {
                var obj = CreateInstance(temp);
                if (obj != null)
                {
                    // 防止對象回收
                    var objRef = RemotingServices.Marshal(obj, temp.Name);
                    _objRefList.Add(objRef);
                }
            }
        }

        private MarshalByRefObject CreateInstance(Type type)
        {
            ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
            return constructor == null ? null : Activator.CreateInstance(type) as MarshalByRefObject;
        }

        /// <summary>
        /// 防止對象回收
        /// </summary>
        private List<ObjRef> _objRefList = new List<ObjRef>();

        private IChannel _channel;
    }

通道

如果需要兩個程序連接,需要創建通道

   /// <summary>
    /// 通道
    /// </summary>
    public class Terminal
    {
        /// <summary>
        /// 創建連接
        /// </summary>
        /// <param name="port">對於服務端需要一個標示符,對於客戶端請使用空</param>
        /// <returns></returns>
        public static IChannel CreatChannel(string port = "")
        {
            if (string.IsNullOrEmpty(port))
            {
                port = Guid.NewGuid().ToString("N");
            }

            var serverProvider = new BinaryServerFormatterSinkProvider();
            var clientProvider = new BinaryClientFormatterSinkProvider();
            serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
            IDictionary props = new Hashtable();
            props["portName"] = port;

            return new IpcChannel(props, clientProvider, serverProvider);
        }
    }

對於 WPF 程序只需要創建隨機的端口,對於 C++ 程序需要創建 WPF 程序告訴他的端口,這樣 WPF 程序纔可以發送數據到 C++ 程序

使用

嘗試把上面的類複製到自己的一個項目,然後創建兩個項目,一個是 WPF 程序,一個是C++程序,讓兩個程序都引用這個項目。

注意創建的項目需要引用 System.Runtime.Remoting

例如創建 MairzearPowhel 程序做 WPF 程序用來調用 SedreaSudome 程序。在 MairzearPowhel 需要引用 SedreaSudome 可以獲得裏面的類而且用來啓動 SedreaSudome 。

    /// <summary>
    /// MainWindow.xaml 的交互邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void SouhaiNosoja()
        {
            // 啓動C++程序
            var remoteProcessManager = new RemoteProcessManager("SedreaSudome.exe");

            // 連接
            remoteProcessManager.Connect();

            var baltrartularLouronay = remoteProcessManager.GetObject<BaltrartularLouronay>();

            // 執行裏面方法
            baltrartularLouronay.TerecaLesi();
        }

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            SouhaiNosoja();

        }
    }

對於 C++ 程序只需要幾個代碼

    class Program
    {
        static void Main(string[] args)
        {
            Debugger.Launch();

            var remoteNative = new RemoteNative();
            remoteNative.Run(args, typeof(Program).Assembly);

            while (true)
            {
                Console.Read();
            }
        }
    }

如果發現無法使用,請聯繫我

感謝 洪三水提供圖片


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章