【C#】进程间通讯1

C#中跨进程通讯的几种常用方法。

内存映射

内存映射即MemoryMappedFile,允许进程在内存中开辟一段空间,而以读取文件的形式去读写,这个内存映射文件是与磁盘无关的,可以用于进程间进行通讯。C#中的MemoryMappedFile类封装了Win32中关于内存映射的API,可以较为简单地使用。

使用方法即通过MemoryMappedFile.CreateNew方法来创建内存映射文件,然后可以通过CreateViewStream方法获取对应的Stream,接下来的读写就和平时操作一般文件类似了。

写入端:

using System.IO.MemoryMappedFiles;

using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("testmap", 256))
{
    double s = 0.5;
    bool mutexCreated = false;
    Mutex mutex = new Mutex(true, "testmapmutex", out mutexCreated);
    using (MemoryMappedViewStream stream = mmf.CreateViewStream())
    {
        BinaryWriter writer = new BinaryWriter(stream);
        writer.Write(s);
    }
    mutex.ReleaseMutex();
    Console.ReadLine();
}

读入端:

using System.IO.MemoryMappedFiles;

try
{
    using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("testmap"))
    {

        Mutex mutex = Mutex.OpenExisting("testmapmutex");
        mutex.WaitOne();

        using (MemoryMappedViewStream stream = mmf.CreateViewStream())
        {
            BinaryReader binaryReader = new BinaryReader(stream);
            Console.WriteLine(binaryReader.ReadDouble());
        }
        mutex.ReleaseMutex();
    }
}
catch (FileNotFoundException)
{
    Console.WriteLine("Memory-mapped file does not exist.");
}

还有一种方式是使用CreateViewAccesssor方法,与上面不同的是,你可以使用Accessor对该段内存进行随机读写而不是顺序读写:

写入端:

using (var accessor = mmf.CreateViewAccessor())
{
    byte[] helo = Encoding.UTF8.GetBytes("Accessor");
    accessor.WriteArray(8, helo, 0, helo.Length);
}

读入端:

using (var accessor = mmf.CreateViewAccessor())
{
    var s = new byte[128];
    var read = accessor.ReadArray(8, s, 0, s.Length);
    var str = Encoding.UTF8.GetString(s);
    Console.WriteLine(str);
}

本质上这个类都是封装的Win32 API,效率上肯定不如直接使用API来得快,如果追求极致的性能,可以使用DllImport引入并调用API,或者通过Accessor.SafeMemoryMappedViewHandle.AcquirePointer来获取对应托管内存的指针,然后在unsafe模式下就可以自由操作了。

命名管道

命名管道即NamedPipe,是一种所有语言中都常用的进程间通讯的方法。C#中对命名管道的封装在System.IO.Pipe命名空间里。主要是NamedPipeServerStream类和NamedPipeClientStream类。在Windows系统中,每个管道都有自己唯一的名字pipename,多个服务端可以指定一个管道作为自己通讯的线路,客户端连接到服务端后,所有往来信息都在该管道流通。

服务端:

using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("testpipe", PipeDirection.InOut, 5))
{
    Console.WriteLine("管道服务端已创建");
    Console.Write("等待客户端连接……");
    pipeServer.WaitForConnection();

    Console.WriteLine("客户端已连接");
    try
    {
        using (StreamWriter sw = new StreamWriter(pipeServer))
        {
            sw.AutoFlush = true;
            Console.Write("输入数据:");
            sw.WriteLine(Console.ReadLine());
        }
    }
    catch (IOException e)
    {
        Console.WriteLine("ERROR {0}", e.Message);
    }
}

客户端:

using System.IO.Pipes;

using (NamedPipeClientStream pipeClient = new NamedPipeClientStream("testpipe"))
{
    Console.Write("连接服务端中……");
    pipeClient.Connect();

    Console.WriteLine("已经连接到服务端");
    Console.WriteLine("共{0}个服务端使用该管道", pipeClient.NumberOfServerInstances);
    using (StreamReader sr = new StreamReader(pipeClient))
    {
        string temp;
        while ((temp = sr.ReadLine()) != null)
        {
            Console.WriteLine("{0}", temp);
        }
    }
}

匿名管道

匿名管道即AnonymousPipe,是别于命名管道的另一种常用的进程间通讯方式。它们的相同点在于它们本质都是WIN32 API在系统支持上提供的通讯管道。

区别在于,匿名管道不支持网络间通讯,只能在本地进程间通讯,而命名管道支持网络间通讯。匿名管道要么为只读要么为只写,而命名管道支持可读写。

匿名管道是没有名字的,这意味着服务端必须通过某种方式将匿名管道的句柄告知客户端,一般是通过服务端来启动客户端,这样可以通过启动时的命令行参数来传递句柄,也可以使用其他进程间通讯方式。

下面以服务端启动客户端、客户端向服务端发送消息为例。

客户端:

using System.IO.Pipes;
using System.Text;

var anonymousPipeClientStream = new AnonymousPipeClientStream(PipeDirection.Out, args[0]);
var data = Encoding.UTF8.GetBytes("Hello World");
await anonymousPipeClientStream.WriteAsync(data, 0, data.Length);

服务端:

using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using System.Threading;

var anonymousPipeServerStream = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
Process client = new Process();
client.StartInfo.FileName = @"C:\Users\White\Desktop\ProcessMessage2\ProcessMessage2\bin\Debug\net6.0\ProcessMessage2.exe";
client.StartInfo.Arguments = anonymousPipeServerStream.GetClientHandleAsString();
client.Start();
anonymousPipeServerStream.DisposeLocalCopyOfClientHandle();
var bytes= new byte[1024];
await anonymousPipeServerStream.ReadAsync(bytes, 0, bytes.Length).ContinueWith(s =>
{
    Console.WriteLine(Encoding.UTF8.GetString(bytes));
});