【微软技术栈】C#.NET 中的管道操作
C#.NET 管道为进程间通信提供了平台。 管道分为两种类型:
-
匿名管道。
匿名管道在本地计算机上提供进程间通信。 与命名管道相比,虽然匿名管道需要的开销更少,但提供的服务有限。 匿名管道是单向的,不能通过网络使用。 仅支持一个服务器实例。 匿名管道可用于线程间通信,也可用于父进程和子进程之间的通信,因为管道句柄可以轻松传递给所创建的子进程。在 .NET 中,可通过使用 AnonymousPipeServerStream 和 AnonymousPipeClientStream 类来实现匿名管道。
-
命名管道。
命名管道在管道服务器和一个或多个管道客户端之间提供进程间通信。 命名管道可以是单向的,也可以是双向的。 它们支持基于消息的通信,并允许多个客户端使用相同的管道名称同时连接到服务器进程。 命名管道还支持模拟,这样连接进程就可以在远程服务器上使用自己的权限。在 .NET 中,可通过使用 NamedPipeServerStream 和 NamedPipeClientStream 类来实现命名管道。
1、如何:使用匿名管道进行本地进程间通信
匿名管道在本地计算机上提供进程间通信。 它们提供的功能比命名管道少,但所需要的系统开销也少。 使用匿名管道,可以在本地计算机上更轻松地进行进程间通信。 不能使用匿名管道进行网络通信。
若要实现匿名管道,请使用 AnonymousPipeServerStream 和 AnonymousPipeClientStream 类。
1.1 示例 1
下面的示例展示了如何使用匿名管道将字符串从父进程发送到子进程。 本示例在父进程中创建一个 AnonymousPipeServerStream 对象,其 PipeDirection 值为 Out。然后,父进程通过使用客户端句柄创建 AnonymousPipeClientStream 对象来创建子进程。 子进程的 PipeDirection 值为 In。
接下来,父进程将用户提供的字符串发送给子进程。 字符串在子进程的控制台中显示。
下面的示例展示了服务器进程。
using System;
using System.IO;
using System.IO.Pipes;
using System.Diagnostics;
class PipeServer
{
static void Main()
{
Process pipeClient = new Process();
pipeClient.StartInfo.FileName = "pipeClient.exe";
using (AnonymousPipeServerStream pipeServer =
new AnonymousPipeServerStream(PipeDirection.Out,
HandleInheritability.Inheritable))
{
Console.WriteLine("[SERVER] Current TransmissionMode: {0}.",
pipeServer.TransmissionMode);
// Pass the client process a handle to the server.
pipeClient.StartInfo.Arguments =
pipeServer.GetClientHandleAsString();
pipeClient.StartInfo.UseShellExecute = false;
pipeClient.Start();
pipeServer.DisposeLocalCopyOfClientHandle();
try
{
// Read user input and send that to the client process.
using (StreamWriter sw = new StreamWriter(pipeServer))
{
sw.AutoFlush = true;
// Send a 'sync message' and wait for client to receive it.
sw.WriteLine("SYNC");
pipeServer.WaitForPipeDrain();
// Send the console input to the client process.
Console.Write("[SERVER] Enter text: ");
sw.WriteLine(Console.ReadLine());
}
}
// Catch the IOException that is raised if the pipe is broken
// or disconnected.
catch (IOException e)
{
Console.WriteLine("[SERVER] Error: {0}", e.Message);
}
}
pipeClient.WaitForExit();
pipeClient.Close();
Console.WriteLine("[SERVER] Client quit. Server terminating.");
}
}
1.2 示例 2
下面的示例展示了客户端进程。 服务器进程启动客户端进程,并为此进程提供客户端句柄。 客户端代码生成的可执行文件应命名为 pipeClient.exe
,并在运行服务器进程前复制到服务器可执行文件所在的相同目录中。
using System;
using System.IO;
using System.IO.Pipes;
class PipeClient
{
static void Main(string[] args)
{
if (args.Length > 0)
{
using (PipeStream pipeClient =
new AnonymousPipeClientStream(PipeDirection.In, args[0]))
{
Console.WriteLine("[CLIENT] Current TransmissionMode: {0}.",
pipeClient.TransmissionMode);
using (StreamReader sr = new StreamReader(pipeClient))
{
// Display the read text to the console
string temp;
// Wait for 'sync message' from the server.
do
{
Console.WriteLine("[CLIENT] Wait for sync...");
temp = sr.ReadLine();
}
while (!temp.StartsWith("SYNC"));
// Read the server data and echo to the console.
while ((temp = sr.ReadLine()) != null)
{
Console.WriteLine("[CLIENT] Echo: " + temp);
}
}
}
}
Console.Write("[CLIENT] Press Enter to continue...");
Console.ReadLine();
}
}
2、如何:使用命名管道进行网络进程间通信
命名管道在管道服务器和一个或多个管道客户端之间提供进程间通信。 它们比匿名管道(用于在本地计算机上提供进程间的通信)提供更多的功能。 命名管道支持跨网络和多个服务器实例的全双工通信、基于消息的通信以及客户端模拟,这样连接进程便可在远程服务器上使用自己的权限集。若要实现名称管道,请使用 NamedPipeServerStream 和 NamedPipeClientStream 类。
2.1 示例 1
下面的示例展示了如何使用 NamedPipeServerStream 类创建命名管道。 在此示例中,服务器进程创建四个线程。 每个线程都可以接受客户端连接。 然后,连接的客户端进程向服务器提供文件名。 如果客户端拥有足够的权限,服务器进程就会打开文件,并将它的内容发送回客户端。
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
public class PipeServer
{
private static int numThreads = 4;
public static void Main()
{
int i;
Thread?[] servers = new Thread[numThreads];
Console.WriteLine("\n*** Named pipe server stream with impersonation example ***\n");
Console.WriteLine("Waiting for client connect...\n");
for (i = 0; i < numThreads; i++)
{
servers[i] = new Thread(ServerThread);
servers[i]?.Start();
}
Thread.Sleep(250);
while (i > 0)
{
for (int j = 0; j < numThreads; j++)
{
if (servers[j] != null)
{
if (servers[j]!.Join(250))
{
Console.WriteLine("Server thread[{0}] finished.", servers[j]!.ManagedThreadId);
servers[j] = null;
i--; // decrement the thread watch count
}
}
}
}
Console.WriteLine("\nServer threads exhausted, exiting.");
}
private static void ServerThread(object? data)
{
NamedPipeServerStream pipeServer =
new NamedPipeServerStream("testpipe", PipeDirection.InOut, numThreads);
int threadId = Thread.CurrentThread.ManagedThreadId;
// Wait for a client to connect
pipeServer.WaitForConnection();
Console.WriteLine("Client connected on thread[{0}].", threadId);
try
{
// Read the request from the client. Once the client has
// written to the pipe its security token will be available.
StreamString ss = new StreamString(pipeServer);
// Verify our identity to the connected client using a
// string that the client anticipates.
ss.WriteString("I am the one true server!");
string filename = ss.ReadString();
// Read in the contents of the file while impersonating the client.
ReadFileToStream fileReader = new ReadFileToStream(ss, filename);
// Display the name of the user we are impersonating.
Console.WriteLine("Reading file: {0} on thread[{1}] as user: {2}.",
filename, threadId, pipeServer.GetImpersonationUserName());
pipeServer.RunAsClient(fileReader.Start);
}
// Catch the IOException that is raised if the pipe is broken
// or disconnected.
catch (IOException e)
{
Console.WriteLine("ERROR: {0}", e.Message);
}
pipeServer.Close();
}
}
// Defines the data protocol for reading and writing strings on our stream
public class StreamString
{
private Stream ioStream;
private UnicodeEncoding streamEncoding;
public StreamString(Stream ioStream)
{
this.ioStream = ioStream;
streamEncoding = new UnicodeEncoding();
}
public string ReadString()
{
int len = 0;
len = ioStream.ReadByte() * 256;
len += ioStream.ReadByte();
byte[] inBuffer = new byte[len];
ioStream.Read(inBuffer, 0, len);
return streamEncoding.GetString(inBuffer);
}
public int WriteString(string outString)
{
byte[] outBuffer = streamEncoding.GetBytes(outString);
int len = outBuffer.Length;
if (len > UInt16.MaxValue)
{
len = (int)UInt16.MaxValue;
}
ioStream.WriteByte((byte)(len / 256));
ioStream.WriteByte((byte)(len & 255));
ioStream.Write(outBuffer, 0, len);
ioStream.Flush();
return outBuffer.Length + 2;
}
}
// Contains the method executed in the context of the impersonated user
public class ReadFileToStream
{
private string fn;
private StreamString ss;
public ReadFileToStream(StreamString str, string filename)
{
fn = filename;
ss = str;
}
public void Start()
{
string contents = File.ReadAllText(fn);
ss.WriteString(contents);
}
}
2.2 示例 2
下面的示例展示了使用 NamedPipeClientStream 类的客户端进程。 客户端连接到服务器进程,并将文件名发送到服务器。 因为此示例使用模拟,所以运行客户端应用的标识必须有权访问文件。 然后,服务器将文件内容发送回客户端。 接下来,文件内容在控制台中显示。
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Security.Principal;
using System.Text;
using System.Threading;
public class PipeClient
{
private static int numClients = 4;
public static void Main(string[] args)
{
if (args.Length > 0)
{
if (args[0] == "spawnclient")
{
var pipeClient =
new NamedPipeClientStream(".", "testpipe",
PipeDirection.InOut, PipeOptions.None,
TokenImpersonationLevel.Impersonation);
Console.WriteLine("Connecting to server...\n");
pipeClient.Connect();
var ss = new StreamString(pipeClient);
// Validate the server's signature string.
if (ss.ReadString() == "I am the one true server!")
{
// The client security token is sent with the first write.
// Send the name of the file whose contents are returned
// by the server.
ss.WriteString("c:\\textfile.txt");
// Print the file to the screen.
Console.Write(ss.ReadString());
}
else
{
Console.WriteLine("Server could not be verified.");
}
pipeClient.Close();
// Give the client process some time to display results before exiting.
Thread.Sleep(4000);
}
}
else
{
Console.WriteLine("\n*** Named pipe client stream with impersonation example ***\n");
StartClients();
}
}
// Helper function to create pipe client processes
private static void StartClients()
{
string currentProcessName = Environment.CommandLine;
// Remove extra characters when launched from Visual Studio
currentProcessName = currentProcessName.Trim('"', ' ');
currentProcessName = Path.ChangeExtension(currentProcessName, ".exe");
Process?[] plist = new Process?[numClients];
Console.WriteLine("Spawning client processes...\n");
if (currentProcessName.Contains(Environment.CurrentDirectory))
{
currentProcessName = currentProcessName.Replace(Environment.CurrentDirectory, String.Empty);
}
// Remove extra characters when launched from Visual Studio
currentProcessName = currentProcessName.Replace("\\", String.Empty);
currentProcessName = currentProcessName.Replace("\"", String.Empty);
int i;
for (i = 0; i < numClients; i++)
{
// Start 'this' program but spawn a named pipe client.
plist[i] = Process.Start(currentProcessName, "spawnclient");
}
while (i > 0)
{
for (int j = 0; j < numClients; j++)
{
if (plist[j] != null)
{
if (plist[j]!.HasExited)
{
Console.WriteLine($"Client process[{plist[j]?.Id}] has exited.");
plist[j] = null;
i--; // decrement the process watch count
}
else
{
Thread.Sleep(250);
}
}
}
}
Console.WriteLine("\nClient processes finished, exiting.");
}
}
// Defines the data protocol for reading and writing strings on our stream.
public class StreamString
{
private Stream ioStream;
private UnicodeEncoding streamEncoding;
public StreamString(Stream ioStream)
{
this.ioStream = ioStream;
streamEncoding = new UnicodeEncoding();
}
public string ReadString()
{
int len;
len = ioStream.ReadByte() * 256;
len += ioStream.ReadByte();
var inBuffer = new byte[len];
ioStream.Read(inBuffer, 0, len);
return streamEncoding.GetString(inBuffer);
}
public int WriteString(string outString)
{
byte[] outBuffer = streamEncoding.GetBytes(outString);
int len = outBuffer.Length;
if (len > UInt16.MaxValue)
{
len = (int)UInt16.MaxValue;
}
ioStream.WriteByte((byte)(len / 256));
ioStream.WriteByte((byte)(len & 255));
ioStream.Write(outBuffer, 0, len);
ioStream.Flush();
return outBuffer.Length + 2;
}
}
2.3 可靠编程
因为此示例中的客户端进程和服务器进程可以在同一台计算机上运行,所以提供给 NamedPipeClientStream 对象的服务器名称为 "."
。 如果客户端进程和服务器进程在不同的计算机上运行,"."
会被替换为运行服务器进程的计算机的网络名称。