温故之.NET进程间通信——管道

鉴于小哥哥、小姐姐们每天的工作压力都很大。决定以后每一篇文章讲解的知识点最多不超过三个。这样有三个好处

  • 小哥哥、小姐姐们可以多花一点时间休息,或者陪陪家人
  • 知识点少,可以保证我们可以理解得更深刻,也更容易记住
  • 知识点少,这样我们就可以在不增加篇幅的情况下,尽可能地讲得深入一些。况且篇幅太长,小哥哥、小姐姐们看久了可能会比较累

进程间传递数据,常见的有以下几种方式:

  • 管道:包括命名管道和匿名管道,这篇文章将讲解这种方式
  • 内存映射文件:借助文件和内存空间之间的映射关系,应用(包括多个进程)可以直接对内存执行读取和写入操作,从而实现进程间通信
  • Socket:使用套接字在不同的进程间通信,这种通信方式下,需要占用系统至少一个端口
  • SendMessage:通过窗口句柄的方式来通信,此通信方式基于 Windows 消息 WM_COPYDATA 来实现
  • 消息队列:在对性能要求不高的情况下,我们可以使用 Msmq。但在实际项目中,一般使用 ActiveMQKafkaRocketMQRabbitMQ等这些针对特定场景优化的消息中间件,以获得最大的性能或可伸缩性优势

其中,管道、内存映射文件、SendMessage的方式,一般用于单机上进程间的通信,在单机上使用这三种方式,比使用 Socket 要相对高效,且更容易控制

Socket 、消息队列或其他基于Socket的通信方式,则适用范围更广。它不仅适用于本机进程间的通信,还适用于跨机器(包括跨网段)之间的通信,比如同一个集群里面不同服务器之间的通信、微服务群下各个微服务之间的通信。这也是目前用得最多得方式

虽然在互联网化的今天,本机进程间通信可能用得不多。但在这篇文章中,我们还是有必要了解基于管道的进程间通信方式,后面我们会目前用得比较广泛的一些框架

命名管道

命名管道,它可以在管道服务器和一个或多个管道客户端之间提供进程间通信。其特点如下

  • 命名管道可以是单向的,也可以是双向的
  • 它们支持基于消息的通信(即创建服务端管道时,指定 PipeTransmissionMode.Message 选项),并允许多个客户端使用相同的管道名称同时连接到服务器端进程
  • 支持模拟,这样连接进程就可以在远程服务器上使用自己的权限

它既可用于本机进程间的通信,也能用于跨机器之间的通信,但实际中很少这样用

跨机器通信,特别是在跨网络的情况下,目前普遍的做法是使用一些通信框架(比如分布式或微服务中,可使用NettyRPCRESTThrift等等),毕竟这些通信框架大都成熟稳定,还经历过商用的考验

用于发送数据的示例代码如下

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

namespace App {
    class Program {
        static void Main(string[] args) {
            /// 第一个参数为管道的名称,第二个参数表示此处的管道用于发送数据
            using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("pipe_demo", PipeDirection.Out)) {
                // 等待连接,程序会阻塞在此处,直到有一个连接到达
                pipeServer.WaitForConnection();

                try {
                    using (StreamWriter sw = new StreamWriter(pipeServer)) {
                        sw.AutoFlush = true;
                        // 向连接的客户端发送消息
                        sw.WriteLine("hello world ");
                    }
                } catch (IOException e) {
                    Console.WriteLine("ERROR: {0}", e.Message);
                }
            }

            Console.ReadLine();
        }
    }
}

用于接收数据的示例代码如下

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

namespace App {
    class Program {
        static void Main(string[] args) {
            /// 第一个参数:"." 表示此管道用于本机。此处用 "localhost"、"127.0.0.1" 也是可以的
            /// 第二个参数:管道的名称
            /// 第三个参数:表示此处的管道用于接收数据
            using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", "pipe_demo", PipeDirection.In)) {
                pipeClient.Connect();

                using (StreamReader sr = new StreamReader(pipeClient)) {
                    string tmp;
                    while ((tmp = sr.ReadLine()) != null) {
                        Console.WriteLine($"收到数据: {tmp}");
                    }
                }
            }

            Console.ReadLine();
        }
    }
}

关于命名管道的命名,我们这儿使用的是 "pipe_demo" , 推荐采用 "公司名.项目名称.模块名称.管道用途" 的方式命名。这不但可以减小与其他命名管道名称冲突的可能性,还可以让这个管道更具有识别性(通过名称就能指定这个管道是干嘛的)

其中,通过在创建管道时,指定 PipeDirection 选项,可以让管道工作于双工、半双工的通信模式下

public enum PipeDirection {
    // 表示此管道用于接收数据
    In = 1,
    // 表示此管道用于发送数据
    Out = 2,
    // 表示此管道既可发送数据,也可以接收数据
    InOut = 3
}

如果对这种通信方式感兴趣,可以参考 NamedPipeServerStreamNamedPipeClientStream 其他的构造函数,来找到更加符合自身业务的模式

匿名管道

匿名管道只能在本机上提供进程间通信。与命名管道相比,其有如下特点

  • 匿名管道需要的开销更少,但提供的服务有限
  • 匿名管道是单向的,且不能通过网络使用,即不能跨网进行通信
  • 仅支持一个服务器实例
  • 匿名管道可用于线程间通信,也可用于父进程和子进程之间的通信,因为管道句柄可以轻松传递给所创建的子进程。

服务端 AnonymousPipeServerStream 定义如下

public sealed class AnonymousPipeServerStream : PipeStream {
    public AnonymousPipeServerStream();
    public AnonymousPipeServerStream(PipeDirection direction);
    public AnonymousPipeServerStream(PipeDirection direction, HandleInheritability inheritability);
    public AnonymousPipeServerStream(PipeDirection direction, HandleInheritability inheritability, int bufferSize);
    public AnonymousPipeServerStream(PipeDirection direction, SafePipeHandle serverSafePipeHandle, SafePipeHandle clientSafePipeHandle);
    public AnonymousPipeServerStream(PipeDirection direction, HandleInheritability inheritability, int bufferSize, PipeSecurity pipeSecurity);

    public SafePipeHandle ClientSafePipeHandle { get; }
    // 此管道的传输模式:在匿名管道中,只支持 PipeTransmissionMode.Byte 这种方式
    public override PipeTransmissionMode TransmissionMode { get; }
    public override PipeTransmissionMode ReadMode { set; }
    public void DisposeLocalCopyOfClientHandle();
    public string GetClientHandleAsString();
    protected override void Dispose(bool disposing);
}

可以看到,其定义了多个构造函数,提供了本机进程中的多种管道通信模式。其中

  • HandleInheritability 用于指明子进程是否可以继承服务器端的底层句柄
  • SafePipeHandle 用于指定客户端和服务端的安全句柄
  • PipeSecurity 用于指定客户端的访问权限

一般情况下,我们只需要使用前三个构造函数即可,后面几个用的很少

客户端 AnonymousPipeClientStream 定义如下

public sealed class AnonymousPipeClientStream : PipeStream {
    public AnonymousPipeClientStream(string pipeHandleAsString);
    public AnonymousPipeClientStream(PipeDirection direction, string pipeHandleAsString);
    public AnonymousPipeClientStream(PipeDirection direction, SafePipeHandle safePipeHandle);

    // 此管道的传输模式:在匿名管道中,只支持 PipeTransmissionMode.Byte 这种方式
    public override PipeTransmissionMode TransmissionMode { get; }
    public override PipeTransmissionMode ReadMode { set; }
}

其中,pipeHandleAsString 参数是父进程在创建此子进程的时候传递的安全句柄

其服务端示例如下

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;

namespace App {
    class Program {
        static void Main(string[] args) {
            Process pipeClient = new Process();
            // 客户端可执行文件的路径
            pipeClient.StartInfo.FileName = @"C:\Users\Jame\source\repos\ConsoleApp4\ConsoleApp4\bin\Debug\ConsoleApp4.exe";

            using (AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) {
                // 将句柄传入
                pipeClient.StartInfo.Arguments =pipeServer.GetClientHandleAsString();
                pipeClient.StartInfo.UseShellExecute = false;
                pipeClient.Start();

                pipeServer.DisposeLocalCopyOfClientHandle();

                try {
                    using (StreamWriter sw = new StreamWriter(pipeServer)) {
                        sw.AutoFlush = true;
                        sw.WriteLine("SYNC");
                        pipeServer.WaitForPipeDrain();

                        Console.Write("[SERVER] Enter text: ");
                        sw.WriteLine(Console.ReadLine());
                    }
                } catch (IOException e) {
                    Console.WriteLine("[SERVER] Error: {0}", e.Message);
                }
            }

            pipeClient.WaitForExit();
            pipeClient.Close();
            Console.WriteLine("[SERVER] Client quit. Server terminating.");

            Console.ReadLine();
        }
    }
}

客户端代码如下

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

namespace App {
    class Program {
        static void Main(string[] args) {
            if (args.Length > 0) {
                // 其中,args[0] 表示传入的句柄
                using (PipeStream pipeClient = new AnonymousPipeClientStream(PipeDirection.In, args[0])) {
                    using (StreamReader sr = new StreamReader(pipeClient)) {
                        string temp;
                        do {
                            Console.WriteLine("[CLIENT] Wait for sync...");
                            temp = sr.ReadLine();
                        }
                        while (!temp.StartsWith("SYNC"));
                        
                        while ((temp = sr.ReadLine()) != null) {
                            Console.WriteLine("[CLIENT] Echo: " + temp);
                        }
                    }
                }
            }

            Console.ReadLine();
        }
    }
}

在匿名管道这个例子中,需要我们先编译客户端的代码,否则可能会有错误

  • 如果客户端还未编译,则父进程会找不到文件
  • 如果客户端已经编译,父进程可启动。但如果我们需要再次编译子进程项目时,会报文件被占用的错误

通过以上的讲解,如果需要使用管道来实现进程间的通信,我们可以按以下方式选择

  • 如果只需要单向通信,且两个进行间的关系为父子进程,则可以使用匿名管道
  • 如果需要双向通信,则使用命名管道
  • 如果我们无法决定到底该选择什么,那就选择命名管道的双向通信方式。在现在的电脑上,命名管道于匿名管道性能的差别我们可以忽略不记。而双向通信的命名管道,既可单向,又可双向,更加灵活

至此,这篇文章的内容讲解完毕。欢迎关注公众号【嘿嘿的学习日记】,所有的文章,都会在公众号首发,Thank you~

公众号二维码

猜你喜欢

转载自juejin.im/post/5b3e1ced6fb9a04fa42f92c3