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