c#多程式通訊,今天,它來了

陳顯達發表於2022-01-09

引言

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

     程式碼地址:http://121.43.235.192:8082/s/DjJkmyaj6Lk6sXj

 

相關文章