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);

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);
{
    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");
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");
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下的多进程通讯,代码我给大家共享出来,可以参考。

代码:http://121.43.235.192:8082/s/DjJkmyaj6Lk6sXj