在學習《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