在学习《CLR via C#》的27.2小节中使用了管道。因此先对管道(pipe)的相关知识进行梳理
Pipe
其在System.IO.Pipes命名空间下,此命名空间内的类主要任务就是完成不同进程之间的通信。其包含了anonymous pipe(匿名管道)和Named pipe(命名管道)。以下内容主要讲述命名管道的相关知识
1、命名管道的本质是一个使用“共享内存”模式的Stream,不同的进程采用CLR规定的规则(即使用Pipe),才可以进行进程间的通信。
以下直接使用代码进行说明。
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].IsBackground=true;
//因为通过Thread创建的线程默认是前台线程,若前台线程会导致一个问题(特别是在UI界面上[例如winform]),造成winform应用程序不终止。因此在使用时要特别注意这点。
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的具体定义参见其定义
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());
//***RunAsClient的具体说明参见代码下方
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;
//本类在创建实例时,会将NamedPipeServerStream传入,因此在本类里面所有的ioStream变量,就是pipeServerStream
public StreamString(Stream ioStream)
{
this.ioStream = ioStream;
streamEncoding = new UnicodeEncoding();
}
public string ReadString()
{
int len = 0;
//可以地方很好玩,之前一直在想为啥要这么写,是有内部的原因吗?
//哈哈,看了WriteString就明白,这就是“原作者”写的玩的
//“原作者”想在读取内容的前两位存储内容的长度。只要按照这个规矩,客户端在发送前会将内容的长度写在最前面两位,
// 这样服务器端就知道需要创建多大的byte数组,这样就可以避免浪费空间。
// 若自己在写demo时,因为知道自己的文件内容,因此可以直接在服务器端定义byte数组的长度
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;
}
//这个地方就解释了ReadString方法中为什么会有先读取两字节的操作了
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);
}
}
RunAsClient
方法第一次没明白什么意思。这个方法从字面意思是指:服务器“变身”客户端。通过网上的查询,自己对这个方法的理解如下:当服务器实例(NamedPipeServerStream)调用本方法时,会获取连接到本服务器上的客户端的Token,使得客户端的权限应用到服务器上。换句话说,本方法并不是说服务器端使用某种机制“变身”成客户端,而是客户端的权限授予服务器端,便于服务器对客户端上的文件进行操作。
说明1:以下的场景最为常用。服务器与客户端建立连接后,服务器端需要读取客户端上的文件内容。但此时服务器端并没有读取客户端文件的权限,需要客户端授权。因此,当服务器端调用RunAsClient
后,客户端的相关权限就授予了服务器端,服务器就可以对文件进行操作了。从这个层面上讲是相当于客户端(方法中使用了“s)。
说明2:若在同一台电脑上创建了服务器端和客户端,则不使用RunAsClient
也不会报错。但若使用远程连接,若不使用RunAsClient
而直接使用读取数据的方法则会出现问题。
因此上面的例子中有如下的代码:
//若直接使用如下代码,在本机不会报错,但在远程桌面连接时,会出现异常
// fileReader.Start();
pipeServer.RunAsClient(fileReader.Start);
而fileReader.Start
的定义如下:
public void Start()
{
string contents = File.ReadAllText(fn);
ss.WriteString(contents);
}
读取客户端的文件内容。因此就需要使用RunAsClient
方法,若不使用本方法,则就会报异常。
客户端的代码如下:
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Security.Principal;
using System.Diagnostics;
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")
{
NamedPipeClientStream pipeClient =
new NamedPipeClientStream(".", "testpipe",
PipeDirection.InOut, PipeOptions.None,
TokenImpersonationLevel.Impersonation);
Console.WriteLine("Connecting to server...\n");
pipeClient.Connect();
StreamString 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()
{
int i;
string currentProcessName = Environment.CommandLine;
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);
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[{0}] has exited.",
plist[j].Id);
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();
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;
}
}
以上代码全部复制于MSDN,具体网址如下:MSDN上关于Pipe的使用
小节
在测试命名管道相关的程序中存在这样一个问题。若远程server端设置了用户名/密码,则客户端会报异常,无法连接到服务器端。另外,需要验证一个问题,要连接到server端是否需要将防火墙关闭!此问题需要验证!
参考文献:
1、http://www.cnblogs.com/langu/archive/2013/02/22/2922542.html
2、http://blog.csdn.net/jcx5083761/article/details/7955489
3、MSDN:https://msdn.microsoft.com/zh-cn/library/bb546085.aspx
4、StackOverflow:http://stackoverflow.com/questions/23832090/how-do-you-loaduserpofile-from-within-namepipeserverstream-runasclient
5、CodeProject:http://www.codeproject.com/Articles/125810/A-complete-Impersonation-Demo-in-Csharp-N