c#多進程通訊,今天,它來了
引言
在c#中,可能大多數人針對於多執行緒之間的通訊,是熟能生巧,對於AsyncLocal 和ThreadLocal以及各個靜態類中支援執行緒之間傳遞的GetData和SetData方法都是信手拈來,那多進程通訊呢,實際上也是用的比較多的地方,但是能夠熟能生巧的人和多執行緒的相比的話呢,那還是有些差距的,所以我昨天整理了一下我所認知的幾個多進程之間的通訊方式,這其中是不包括各種消息中間件以及資料庫方面的,還有Grpc,WebSocket或者Signalr等方式,僅僅是以c#程式碼為例,c#的多進程通訊呢,大致上是分為這幾類的,共享記憶體,藉助Windows的MSMQ消息隊列服務,以及命名管道和匿名管道,以及IPC HTTP TCP的Channel的方式,還有常用的Socket,藉助Win32的SendMessage的Api來實現多進程通訊,還有最後一種就是多進程之間的訊號量相關的Mutex,程式碼我會放在文章的末尾,大家有需要的話可以去下載來看看,接下來就為大家一一奉上。
共享記憶體
共享記憶體呢,實際上c#中可以有很多種實現方式,主要是藉助於Win32的Api來實現以及,使用MemoryMappedFile這個類來實現共享記憶體,前者需要引入多個Win32的dll的方法,後者使用起來就比較簡單,只需要調用類的CreatNew方法設置好記憶體映射文件名稱以及大小,以及操作許可權就可以實現,同時支援Accessor和Stream的方式去進行讀寫,但是性能方面肯定是Win32的性能好,而且Win32的話不受語言的限制,至於這個類是否受限於語言,目前我是不太清楚的。接下來,咱們就看看客戶端和服務端使用共享記憶體的方式和獲取數據的程式碼。
服務端:
MemoryMappedFile memoryAccessor = MemoryMappedFile.CreateNew("ProcessCommunicationAccessor", 500, MemoryMappedFileAccess.ReadWrite);//創建共享記憶體映射文件對象,第一個參數為映射的名稱,與客戶端需要對應,500為大小,單位為位元組,MemoryMappedFileAccess為訪問許可權,是讀寫還是只讀 只寫,此處不能使用Using 否則脫離Using 就會釋放,客戶端無法獲取到此名稱的記憶體映射對象 using (var accessor = memoryAccessor.CreateViewAccessor())//獲取映射文件對象的視圖 { var helo = Encoding.UTF8.GetBytes("Accessor"); accessor.WriteArray(0, helo, 0, helo.Length);//將給定的值寫入此視圖中 richTextBox1.Text += Environment.NewLine + "Accessor Send Val:Accessor"; } MemoryMappedFile memoryStream = MemoryMappedFile.CreateNew("ProcessCommunicationStream", 500, MemoryMappedFileAccess.ReadWrite);//創建流的映射文件對象 using (var stream = memoryStream.CreateViewStream())//獲取映射文件的流 { var helo = Encoding.UTF8.GetBytes("Stream"); stream.Write(helo, 0, helo.Length);//將給定的值寫入此記憶體流中 richTextBox1.Text += Environment.NewLine + "Accessor Send Val:Stream"; }
客戶端:
MemoryMappedFile memoryAccessor = MemoryMappedFile.OpenExisting("ProcessCommunicationAccessor");//獲取服務端定義的ProcessCommunicationAccessor名稱的記憶體映射文件然後調用ReadArray方法讀取到服務端寫入的數據 using (var accessor = memoryAccessor.CreateViewAccessor()) { var s = new byte[999]; var read = accessor.ReadArray(0, s, 0, s.Length); var str = Encoding.UTF8.GetString(s); richTextBox1.Text += Environment.NewLine + "Accessor Read Val:" + str.ToString(); } MemoryMappedFile memoryStream = MemoryMappedFile.OpenExisting("ProcessCommunicationStream");//獲取服務端定義的ProcessCommunicationStream名稱的記憶體映射文件然後調用ReadToEnd方法讀取到服務端寫入的數據 using (var stream = memoryStream.CreateViewStream()) { using (var reader = new StreamReader(stream)) { var str = reader.ReadToEnd(); richTextBox1.Text += Environment.NewLine + "Stream Read Val:" + str + "\r\n"; } }
可以看到我們在服務端定義了一個是Accessor類型的MemoryMappedFile在寫入數據的時候是用MemortViewAccessor的方式去寫入的,然後又定義了一個使用Stream的方式去進行寫入數據,在客戶端中,我們直接使用OpenExisting方法去判斷是否存在這個對象,如果存在的話,就使用了服務端定義的CreatNew這個對象,如果不存在則是Null,當然了也可以使用其他的方式去進行獲取,例如CreateOrOpen判斷是否是獲取的還是重新創建的方式,我們在客戶端使用ReadArray和ReadToEnd的方式讀取了服務端寫入的Accessor和Stream的數據,然後我們就可以在客戶端和服務端之間進行一個數據傳輸的一個通訊。
Windows的MSMQ
使用MSMQ的前提是需要在本電腦安裝了消息隊列,安裝方式需要在控制面板,程式和功能那裡啟用或關閉程式,在列表中找到我們需要的消息隊列(MSMQ)伺服器然後安裝,安裝完成後,我們點擊我的電腦右鍵管理找到最下面的服務和應用程式就可以看到我們安裝的消息隊列了,然後找到專用隊列,我們在這裡新建一個隊列,然後就可以在我們的程式碼中使用了,這裡呢我只是簡單寫一個示範,實際上在Messaging命名空間里,還支援對消息隊列許可權的控制,等等的操作,接下來我們看看如何在程式碼中使用消息隊列。
服務端中我們定義了我們需要使用的消息隊列的類型以及名稱,名稱規範的話也可以參考官網對名稱定義的介紹,還支援其他方式名稱的定義,定義好之後呢,我們便發送了一個消息Message HelloWorld的一條消息
MessageQueue queue = new MessageQueue(".\\Private$\\MessageQueue");//右鍵我的電腦,點擊管理 找到服務和應用程式找到專用隊列,創建的專用隊列名稱就是MessageQueue queue.Send("Message HelloWorld");//然後發送消息 richTextBox1.Text += Environment.NewLine + "MessageQueue Send Val:Message HelloWorld";
客戶端中,我們也是和服務端定義了一個消息隊列的一個對象,然後我們監聽這個消息隊列的收到消息的事件,開始非同步接收消息,在接收完畢之後呢,會走到我們寫的ReceiveCompleted的完成事件中,然後我們結束非同步接收的,獲取到服務端發送的消息,然後使用XmlMessageFormatter對象去格式化我們服務端發送的消息,這裡的Type是服務端發送的消息類型,兩者需要對應,在接受並展示到UI之後,我們在開始非同步接收。
var context = WindowsFormsSynchronizationContext.Current; MessageQueue myQueue = new MessageQueue(".\\Private$\\MessageQueue");//定義消息隊列對象,和服務端的地址一樣, myQueue.ReceiveCompleted += (a, b) =>//定義接受完成的時間 { var cts = context; var queue = a as MessageQueue;//隊列對象 queue.EndReceive(b.AsyncResult); var msg = b.Message;//接收到的消息對象 msg.Formatter = new XmlMessageFormatter() { TargetTypes = new Type[] { typeof(string) } };//設置接收到的消息使用什麼方式格式化 var msgVal = msg.Body;//此處是服務端發送的具體的消息對象 cts.Send(new System.Threading.SendOrPostCallback(s => { richTextBox1.Text += Environment.NewLine + "MessageQueue Read Val:" + msgVal + "\r\n"; }), null); queue.BeginReceive(); }; myQueue.BeginReceive();
命名管道
命名管道和匿名管道位於System.Io.Pipe命名空間下,顧名思義,命名管道是需要我們給管道命名一個名稱的以便於客戶端來進行連接,我們需要定義管道的名稱,指定管道的方向,是輸入還是輸出 還是輸入輸出,還可以定義最大的服務端實例數量,以及傳輸的消息類型是Byte還是Message,以及是否開啟非同步等。接下來我們看看服務端和客戶端之間通訊的程式碼。
服務端:我們定義了管道名稱是ProcessCommunicationPipe,並且定義是可以輸入也可以輸出,10個實例,以及使用Message傳輸類型,開啟非同步通訊,然後我們非同步的等待客戶端鏈接,在鏈接成功之後呢,我們通知UI客戶端已經鏈接到了服務端,然後非同步去接收客戶端發來的消息,並且展示到UI上面。
///定義一個命名管道,第一個參數是管道名稱,第二個參數代表是輸入類型還是輸出類型 還是輸入輸出類型,以及設置最大的伺服器實例,設置傳輸類型,以及開啟可以非同步的進行讀取和寫入 namedPipeServerStream = new NamedPipeServerStream("ProcessCommunicationPipe", PipeDirection.InOut, 10, PipeTransmissionMode.Message, PipeOptions.Asynchronous); //非同步等待客戶端鏈接,如果上面的Options不是Asynchronous 非同步則會報錯 namedPipeServerStream.WaitForConnectionAsync().ContinueWith(s => { var cts = synchronizationContext; //刷新UI 告知有客戶端鏈接 cts.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Client Is Connected;"; }), null); var valByte = new byte[1024]; //非同步讀取客戶端發送的消息 namedPipeServerStream.ReadAsync(valByte, 0, valByte.Length).ContinueWith(m => { var val = valByte; var str = Encoding.UTF8.GetString(val); cts.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Server Receive Val:" + str; }), null); }); });
服務端發送程式碼:我們定義了一個Send的發送按鈕,以及一個發送內容的文本框,然後我們只需要調用Server的WriteAsync就可以將我們的數據寫入到Server中發送到客戶端。
//命名管道發送消息到客戶端 var data = Encoding.UTF8.GetBytes(textBox1.Text); //發送消息到客戶端 namedPipeServerStream.WriteAsync(data, 0, data.Length); richTextBox1.Text += Environment.NewLine + "Server Send Val:" + textBox1.Text;
客戶端:
我們定義了一個Client的對象,.代表是當前電腦,以及和服務端一樣的管道名稱,同樣定義為開啟非同步,以及是輸入輸出類型的。然後非同步的去鏈接服務端,然後更新UI,通知已經鏈接成功,並且非同步等待服務端給客戶端發送消息,從而顯示到UI上面。
var cts = WindowsFormsSynchronizationContext.Current; //定義管道對象,如果需要是網路之間通訊.替換為服務端的伺服器名稱和pipeName namedPipeClientStream = new NamedPipeClientStream(".", "ProcessCommunicationPipe", PipeDirection.InOut, PipeOptions.Asynchronous); //非同步鏈接服務端 namedPipeClientStream.ConnectAsync().ContinueWith(s => { var cs = cts; cs.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Server Is Connected;"; }), null); var valByte = new byte[1024]; //非同步等待收到服務端發送的消息 然後更新到UI namedPipeClientStream.ReadAsync(valByte, 0, valByte.Length).ContinueWith(sb => { var val = valByte; var str = Encoding.UTF8.GetString(val); cts.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Client Receive Val:" + str; }), null); }); });
客戶端發送程式碼:同服務端一樣,寫入我們的數據,服務端就會走到ReadAsync的方法中去,服務端就可以接收到我們發送的數據並且展示到UI,
//命名管道發送消息到服務端 var data = Encoding.UTF8.GetBytes(textBox1.Text); namedPipeClientStream.WriteAsync(data, 0, data.Length); richTextBox1.Text += Environment.NewLine + "Client Send Val:" + textBox1.Text;
匿名管道
匿名管道是我們服務端是父進程,需要我們服務端去使用Process啟用開啟我們的子進程,然後傳入我們客戶端的句柄到客戶端,客戶端再根據傳入的參數鏈接到服務端,從而可以實現通訊,但是匿名管道不支援網路之間的通訊,以及不支援輸入輸出,僅支援要麼輸入要麼輸出,同時,匿名管道提供了PipeAccessRule來控制訪問許可權。接下來,我們看一下客戶端和服務端是如何通訊,以及服務端如何去啟動客戶端。
服務端:服務端去定義Process設置我們需要啟動的子進程,然後定義我們的匿名管道,然後將客戶端鏈接的Handlestring傳到客戶端,然後啟動我們的客戶端,在定義非同步接收消息之後的回調,然後展示到頁面上。
//定義客戶端子進程 Process Client = new Process(); //子進程路徑 Client.StartInfo.FileName = @"E:\CoreRepos\ProcessCommunicationClient\bin\Debug\ProcessCommunicationClient.exe"; //定義匿名管道, AnonymousPipeServerStream anonymousPipeServerStream = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable); Client.StartInfo.Arguments = anonymousPipeServerStream.GetClientHandleAsString(); Client.StartInfo.UseShellExecute = false; Client.Start(); //關閉本地複製的客戶端 anonymousPipeServerStream.DisposeLocalCopyOfClientHandle(); var byteVal = new byte[1024]; //非同步接受收到的消息 anonymousPipeServerStream.ReadAsync(byteVal, 0, byteVal.Length).ContinueWith(s => { var cts = synchronizationContext; var val = byteVal; var str = Encoding.UTF8.GetString(val); cts.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "匿名 Server Receive Val:" + str; }), null); });
客戶端:客戶端中我們需要將Winform的Program的Main方法中添加一個string數組的參數然後傳入到我們的窗體中,這樣匿名客戶端管道鏈接服務端就可以鏈接成功。
//此處定義匿名管道的對象,Vs[0]來自服務端的Process的Arguments屬性的值 anonymousPipeClientStream = new AnonymousPipeClientStream(PipeDirection.Out, Vs[0]);
客戶端發送程式碼:
我們直接調用WriteAsync方法寫入我們的數據,服務端就可以接收到我們發送的資訊。
//發送消息到匿名管道服務端 var vss = Encoding.UTF8.GetBytes(textBox2.Text); anonymousPipeClientStream.WriteAsync(vss, 0, vss.Length); richTextBox1.Text += Environment.NewLine + "匿名Client Send Val:" + textBox2.Text;
Channel
Channel下面是有IPC,HTTP和TCP三種類型,三種類型都提供了ClientChannel 以及ServerChannel和Channel的類,Channel類是簡化了Server和Client的操作,可以直接使用Channel來進行定義服務端和客戶端通訊的對象,接下面我們看看Ipc通訊的方式。
IPC
我們定義了一個IpcChannel的對象並且指定ip為127.0.0.1埠是8081,然後我們需要向管道服務註冊我們的管道資訊,然後註冊我們需要注入的類型,以及資源的URL地址,還有生命周期是單例還是每次獲取都不一樣,只有這兩種周期,然後我們看看客戶端使用的程式碼。
服務端:
///定義IPC信道,埠和ip,也可以直接定義埠 ipcChannel = new IpcChannel("127.0.0.1:8081"); //向信道註冊當前管道 ChannelServices.RegisterChannel(ipcChannel, true); //注入對象到服務端,並且指定此對象的URL,以及生命周期,是單例還是每次獲取都不一樣 RemotingConfiguration.RegisterWellKnownServiceType(typeof(ProcessCommunicationIpc), "Ipc.rem", WellKnownObjectMode.Singleton); richTextBox1.Text += Environment.NewLine + "IPCServer Is Open;";
客戶端:
我們定義了一個空的管道資訊並且註冊進去,然後定義我們需要獲取的類型,以及類型的URL資源地址,並且調用RegisterWellKnownClientType方法,這個方法我的見解是相當於告知服務端我們需要使用的資源,然後我們直接New這個對象,調用SetName方法,就可以實現通訊,那如果服務端怎麼獲取到數據呢,那有的同學就會問了,莫急,我們看下一段程式碼。
IpcChannel ipcChannel = new IpcChannel();//定義一個IPC管道對象同樣需要註冊到管道服務中 ChannelServices.RegisterChannel(ipcChannel, true); WellKnownClientTypeEntry entry = new WellKnownClientTypeEntry(typeof(ProcessCommunicationIpc), "ipc://127.0.0.1:8081/Ipc.rem");//定義我們需要獲取的類型以及此類型的Url RemotingConfiguration.RegisterWellKnownClientType(entry);//相當於告知服務端我們需要用的對象 ProcessCommunicationIpc processCommunicationIpc = new ProcessCommunicationIpc();//定義一個這個對象 processCommunicationIpc.SetName(textBox3.Text);//然後調用這個SetNama方法 richTextBox1.Text += Environment.NewLine + "IPCClient Send Val:" + textBox3.Text;
服務端接收程式碼:我們直接調用Activator的GetObject方法從我們服務端定義的地址獲取到我們註冊的類型,然後調用Name屬性就可以看到Name是我們客戶端寫入的數據,因為我們定義的生命周期是單例的,所以這裡可以實現客戶端和服務端之間的通訊,實際上Http和Tcp的使用方式同IPC一樣,都是大同小異,我們可以看看HTTP和TCP使用的程式碼就會明白了。
//從我們定義的IPCurl獲取代理對象,然後判斷值是否改變 var processCommunicationIpc = Activator.GetObject(typeof(ProcessCommunicationIpc), "ipc://127.0.0.1:8081/Ipc.rem") as ProcessCommunicationIpc; var name = processCommunicationIpc.Name; richTextBox1.Text += Environment.NewLine + "IPCServer Receive Val:" + name;
Http
服務端:
///定義HTTP信道,埠 HttpChannel httpChannel = new HttpChannel(8082); //向信道註冊當前管道 ChannelServices.RegisterChannel(httpChannel, false); //注入對象到服務端,並且指定此對象的URL,以及生命周期,是單例還是每次獲取都不一樣 RemotingConfiguration.RegisterWellKnownServiceType(typeof(ProcessCommunicationHttp), "Http.rem", WellKnownObjectMode.Singleton); richTextBox1.Text += Environment.NewLine + "HttpServer Is Open;";
服務端接收:
//從我們定義的Http url獲取代理對象,然後判斷值是否改變 var processCommunicationIpc = Activator.GetObject(typeof(ProcessCommunicationHttp), "//127.0.0.1:8082/Http.rem") as ProcessCommunicationHttp; var name = processCommunicationIpc.Name; richTextBox1.Text += Environment.NewLine + "HttpServer Receive Val:" + name;
客戶端:
HttpChannel httpChannel=new HttpChannel();//定義一個HTTP管道對象同樣需要註冊到管道服務中 ChannelServices.RegisterChannel(httpChannel, false); WellKnownClientTypeEntry entry = new WellKnownClientTypeEntry(typeof(ProcessCommunicationHttp), "//127.0.0.1:8082/Http.rem");//定義我們需要獲取的類型以及此類型的Url RemotingConfiguration.RegisterWellKnownClientType(entry);//相當於告知服務端我們需要用的對象 ProcessCommunicationHttp processCommunicationIpc = new ProcessCommunicationHttp();//定義一個這個對象 processCommunicationIpc.SetName(textBox4.Text);//然後調用這個SetNama方法 richTextBox1.Text += Environment.NewLine + "HttpClient Send Val:" + textBox4.Text;
TCP
服務端:
///定義Tcp信道,埠 TcpChannel tcpChannel = new TcpChannel(8083); //向信道註冊當前管道 ChannelServices.RegisterChannel(tcpChannel, true); //注入對象到服務端,並且指定此對象的URL,以及生命周期,是單例還是每次獲取都不一樣 RemotingConfiguration.RegisterWellKnownServiceType(typeof(ProcessCommunicationTcp), "Tcp.rem", WellKnownObjectMode.Singleton); richTextBox1.Text += Environment.NewLine + "TcpServer Is Open;";
服務端接收:
//從我們定義的Tcp url獲取代理對象,然後判斷值是否改變 var processCommunicationIpc = Activator.GetObject(typeof(ProcessCommunicationTcp), "tcp://127.0.0.1:8083/Tcp.rem") as ProcessCommunicationTcp; var name = processCommunicationIpc.Name; richTextBox1.Text += Environment.NewLine + "TcpServer Receive Val:" + name;
客戶端:
TcpChannel tcpChannel = new TcpChannel();//定義一個TCP管道對象同樣需要註冊到管道服務中 ChannelServices.RegisterChannel(tcpChannel, true); WellKnownClientTypeEntry entry = new WellKnownClientTypeEntry(typeof(ProcessCommunicationTcp), "tcp://127.0.0.1:8083/Tcp.rem");//定義我們需要獲取的類型以及此類型的Url RemotingConfiguration.RegisterWellKnownClientType(entry);//相當於告知服務端我們需要用的對象 ProcessCommunicationTcp processCommunicationIpc = new ProcessCommunicationTcp();//定義一個這個對象 processCommunicationIpc.SetName(textBox5.Text);//然後調用這個SetNama方法 richTextBox1.Text += Environment.NewLine + "TcpClient Send Val:" + textBox5.Text;
可以看到基本上都是一樣的,但是有些地方是不一樣的,這裡我是沒有寫那部分的程式碼,例如Http是可以配置HttpHandler的,其他方面使用起來都是大同小異。
Socket
Socket可能是大家用的最多的進程通訊了,它也不僅僅是進程之間,同時也是支援網路之間的通訊,同時協議類型支援的也是比較多的,並且支援雙向通訊,可以發送文件等,這裡就不作過多的介紹了,直接上程式碼
服務端:
我們直接定義服務端對象,並且指定地址和埠開始監聽並且非同步等待鏈接,
//定義Socket對象,以及協議,傳輸類型 Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp); var ipAddress = IPAddress.Parse("127.0.0.1"); var endpoint = new IPEndPoint(ipAddress, 8084); //指定綁定的ip和埠 socket.Bind(endpoint); //鏈接的最大長度 socket.Listen(10); socket.BeginAccept(Accept, socket);//非同步等待鏈接 richTextBox1.Text += Environment.NewLine + "Socket Server Is Listening;";
服務端非同步接受程式碼:在有連接之後我們直接去獲取到鏈接的客戶端對象的Socket並且賦值給我們的Socket全局變數,然後更新UI,並且非同步的去讀取客戶端發送的消息。
private void Accept(IAsyncResult asyncResult) { var socket = asyncResult.AsyncState as Socket; var client = socket.EndAccept(asyncResult);//獲取鏈接的客戶端 if (client != null) { var cs = synchronizationContext; Client=client; //更新UI 提示已經鏈接 cs.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Socket Client Is Connected;"; }), null); //非同步接受消息 client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, Read, client); } socket.BeginAccept(Accept, socket); }
服務端接收數據程式碼:
我們在接收到了客戶端發的消息之後,我們解析成字元串,然後更新到UI上面。
var cts = synchronizationContext; var client = asyncResult.AsyncState as Socket; var data=client.EndReceive(asyncResult);//獲取接受的數據長度 var str = Encoding.UTF8.GetString(buffer);//轉換為字元然後顯示到介面 cts.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Socket Server Receive Val:" + str; }), null);
服務端發送程式碼:
我們直接調用我們獲取到的Client的Socket對象,發送我們需要發送的消息即可。
//將消息發送到客戶端 var sendVal=Encoding.UTF8.GetBytes(textBox2.Text); Client.Send(sendVal,SocketFlags.None); richTextBox1.Text += Environment.NewLine + "Socket Server Send Val:" + textBox2.Text;
客戶端:定義好服務端的IP和埠然後我們非同步鏈接,在鏈接成功之後我們在發送我們的數據到服務端,並且非同步等待服務端給我們發送消息。
var cs = cts; //定義Socket客戶端對象 Socket socket = new Socket(SocketType.Stream,ProtocolType.Tcp); var ipAddress = IPAddress.Parse("127.0.0.1"); var endpoint = new IPEndPoint(ipAddress, 8084); //定義需要鏈接的服務端的IP和埠然後非同步鏈接服務端 socket.ConnectAsync(endpoint).ContinueWith(s => { //鏈接之後發送消息到服務端 var arg = new SocketAsyncEventArgs(); var sendVal=Encoding.UTF8.GetBytes(textBox6.Text); arg.SetBuffer(sendVal,0, sendVal.Length); socket.SendAsync(arg); cs.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Socket Client Send Val:" + textBox6.Text; }), null); //非同步等待服務端發送的消息 socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, Read, socket); });
客戶端接收程式碼:
我們直接從我們的服務端Socket對象中讀取我們的數據然後展示到UI上面。
var cs = cts; var client = asyncResult.AsyncState as Socket; var data = client.EndReceive(asyncResult); //獲取服務端給客戶端發送的消息 var str = Encoding.UTF8.GetString(buffer); cs.Send(new System.Threading.SendOrPostCallback(b => { richTextBox1.Text += Environment.NewLine + "Socket Client Receive Val:" + str; }), null);
Win32 Api SendMessage
在窗體程式中,我們可以重寫窗體的DefWndProc方法,來實現進程之間的消息通訊,需要引入Win32的SendMessage方法來實現,這個方法可以實現給一個或者多個窗體之間發送消息,我們可以指定我們需要發送的窗體的句柄,以及我們發送的消息類型的Code也可以自己寫,以及我們需要傳過去的參數,可以定義為結構體進行傳送,接收方,再從記憶體中將句柄轉為對應的結構體就可以使用,這裡我使用的傳輸數據類型是Int類型的數據,如果需要傳結構體的話,引入的Dll設置SendMessage方法處可以設置,以及在接收方需要使用記憶體的操作類Marshal類進行轉為結構體,接下來我們看看客戶端是如何和服務端進行通訊的。
服務端:我們重寫這個方法之後,等待客戶端給我們發送消息就行,m.msg是和客戶端商定好的消息類型。
protected override void DefWndProc(ref System.Windows.Forms.Message m) { if (m.Msg == 0x1050) { var paraA =(int) m.WParam; var paramB = (int)m.LParam; richTextBox1.Text += Environment.NewLine + "Win32 Msg Receive Val:"+paraA; richTextBox1.Text += Environment.NewLine + "Win32 Msg Receive Val:" + paramB; } base.DefWndProc(ref m); }
客戶端程式碼:
我們需要引入我們使用的SendMessage方法
[DllImport("user32.dll", EntryPoint = "SendMessage")] private static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam,int lParam);
發送程式碼:
我們需要獲取到我們要發送給那個進程,然後獲取到主程式的句柄,然後傳入我們的消息code,以及我們的參數資訊,這樣服務端就可以接收到我們客戶端發送過去的10,20的數據,
//獲取到我們需要發送到的窗體的進程,然後獲取他的主窗體句柄,將我們的消息10,20發送到指定的窗體中,然後會執行DefWndProc方法,然後在方法中判斷msg類型是否和我們這邊發送的0x1050一致,就可以收到客戶端發送的消息,第二個參數是我們定義的消息類型,可以自己定義數字 也可以根據Win32 api裡面規定的對應的功能用哪些也可以 var process=Process.GetProcessesByName("ProcessCommunication").FirstOrDefault(); SendMessage(process.MainWindowHandle, 0x1050, 10,20);
Mutex訊號量
在前面的多執行緒博文中,我有講過Mutex是進程之間也可以,是作業系統層面的,我們可以使用WaitOne進入到我們的程式碼段中,並且只有一個執行緒可以進入,在結束後我們需要釋放調這個鎖,從而其他執行緒就可以獲取到,既然Mutex是進程之間也可以,那多個進程之間也可以共享一個Mutex對象,A進程使用WaitOnd的時候B進程是只能等待A進程釋放才可以使用。
服務端程式碼:
我們定義了Mutex的對象,然後開啟了一個執行緒去進行死循環刷新UI資訊,然後循環內部我們鎖定鎖,然後通知UI,然後在釋放鎖,這樣客戶端同樣的程式碼必須等到ReleaseMutex之後才可以進去到循環內部更新UI的部分。
var isNew = false; //定義Mutex對象,參數一是否具有初始權,第二個為系統中的名稱,第三個代表是否是新建的; var mutex = new Mutex(false, "ProcessCommunication", out isNew);//用來和客戶端用同一個對象,在循環中有且僅有一個進程可以使用這個對象,即子進程在使用WaitOne方法的時候 父進程是沒有辦法進入到循環體中,只有調用了子進程調用ReleaseMutex方法,父進程才可以使用;通常可以用這個可以實現多進程訪問同一個文件 等。 Task.Run(() => { var cs = synchronizationContext; int i = 0; while (true) { mutex.WaitOne(); cs.Send(new SendOrPostCallback(s => { richTextBox1.Text += Environment.NewLine + i; }), null); i++; mutex.ReleaseMutex(); } });
客戶端:
客戶端和服務端程式碼一樣,但是運行起來加斷點是可以看到客戶端進入了cs.send之後,服務端是沒有辦法進入的,必須等待客戶端ReleaseMutex之後才可以進入,這也就是我前面說的可以用這個去實現多進程操作對象的一個場景。
var isNew = false; //創建Mutex對象 var mutex = new Mutex(false,"ProcessCommunication",out isNew);//用來和客戶端用同一個對象,在循環中有且僅有一個進程可以使用這個對象,即子進程在使用WaitOne方法的時候 父進程是沒有辦法進入到循環體中,只有調用了子進程調用ReleaseMutex方法,父進程才可以使用;通常可以用這個可以實現多進程訪問同一個文件 等。 Task.Run(() => { var cs = cts; int i = 0; while (true) { mutex.WaitOne(); cs.Send(new SendOrPostCallback(s => { richTextBox1.Text += Environment.NewLine+i; }), null); i++; mutex.ReleaseMutex(); } });
結束
今天的多進程的分享就到這裡了,那實際上還有很多種方式可以實現多進程,網路之間的通訊,消息隊列,WebSocket,Api以及Grpc等等,這裡只是演示一下c#中並且大多數支援FrameWork下的多進程通訊,如果有不明白的地方,可以添加群找到我,或者查看加的所有的Net群是否有一個叫四川觀察的,那也是我,有不明白的可以隨時問我,我都在,程式碼我給大家共享出來,大家可以去看一下。
程式碼地址://121.43.235.192:8082/s/DjJkmyaj6Lk6sXj