本文告訴大家一個封裝好的庫,使用這個庫可以快速搭建多進程相互使用。
在 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(); } } }
如果發現無法使用,請聯繫我
感謝 洪三水提供圖片