《CLR via C#》讀書筆記-異步編程(一)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章