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), "http://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), "http://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群是否有一個叫四川觀察的,那也是我,有不明白的可以隨時問我,我都在,代碼我給大傢共享出來,大傢可以去看一下。

     代碼地址:ProcessCommunicationServerAndClient_jb51.rar

到此這篇關於c#多進程通訊的實現示例的文章就介紹到這瞭,更多相關c#多進程通訊內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: