C#網絡編程(異步傳輸字符串) - Part.3

這篇文章咱們將前進一大步,使用異步的方式來對服務端編程,以使它成爲一個真正意義上的服務器:能夠爲多個客戶端的屢次請求服務。可是開始以前,咱們須要解決上一節中遺留的一個問題。html

消息發送時的問題

這個問題就是:客戶端分兩次向流中寫入數據(好比字符串)時,咱們主觀上將這兩次寫入視爲兩次請求;然而服務端有可能將這兩次合起來視爲一條請求,這在兩個請求間隔時間比較短的狀況下尤爲如此。一樣,也有可能客戶端發出一條請求,可是服務端將其視爲兩條請求處理。下面列出了可能的狀況,假設咱們在客戶端連續發送兩條「Welcome to Tracefact.net!」,則數據到達服務端時可能有這樣三種狀況:編程

NOTE:在這裏咱們假設採用ASCII編碼方式,由於此時上面的一個方框正好表明一個字節,而字符串到達末尾後爲持續的0(由於byte是值類型,且最小爲0)。數組

上面的第一種狀況是最理想的狀況,此時兩條消息被視爲兩個獨立請求由服務端完整地接收。第二種狀況的示意圖以下,此時一條消息被看成兩條消息接收了:緩存

而對於第三種狀況,則是兩條消息被合併成了一條接收:服務器

若是你下載了上一篇文章所附帶的源碼,那麼將Client2.cs進行一下修改,不經過用戶輸入,而是使用一個for循環連續的發送三個請求過去,這樣會使請求的間隔時間更短,下面是關鍵代碼:網絡

string msg = "Welcome to TraceFact.Net!";

for (int i = 0; i <= 2; i++) {
    byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 得到緩存
    try {
        streamToServer.Write(buffer, 0, buffer.Length); // 發往服務器
        Console.WriteLine("Sent: {0}", msg);
    } catch (Exception ex) {
        Console.WriteLine(ex.Message);
        break;
    }
}app

運行服務端,而後再運行這個客戶端,你可能會看到這樣的結果:異步

能夠看到,儘管上面將消息分紅了三條單獨發送,可是服務端卻將後兩條合併成了一條。對於這些狀況,咱們能夠這樣處理:就好像HTTP協議同樣,在實際的請求和應答內容以前包含了HTTP頭,其中是一些與請求相關的信息。咱們也能夠訂立本身的協議,來解決這個問題,好比說,對於上面的狀況,咱們就能夠定義這樣一個協議:函數

[length=XXX]:其中xxx是實際發送的字符串長度(注意不是字節數組buffer的長度),那麼對於上面的請求,則咱們發送的數據爲:「[length=25]Welcome to TraceFact.Net!」。而服務端接收字符串以後,首先讀取這個「元數據」的內容,而後再根據「元數據」內容來讀取實際的數據,它可能有下面這樣兩種狀況:post

NOTE:我以爲這裏借用「元數據」這個術語還算比較恰當,由於「元數據」就是用來描述數據的數據。

  • 「[「」]」中括號是完整的,能夠讀取到length的字節數。而後根據這個數值與後面的字符串長度相比,若是相等,則說明發來了一條完整信息;若是多了,那麼說明接收的字節數多了,取出合適的長度,並將剩餘的進行緩存;若是少了,說明接收的不夠,那麼將收到的進行一個緩存,等待下次請求,而後將兩條合併。
  • 「[」「]」中括號自己就不完整,此時讀不到length的值,由於中括號裏的內容被截斷了,那麼將讀到的數據進行緩存,等待讀取下次發送來的數據,而後將兩次合併以後再按上面的方式進行處理。

接下來咱們來看下如何來進行實際的操做,實際上,這個問題已經不屬於C#網絡編程的內容了,而徹底是對字符串的處理。因此咱們再也不編寫服務端/客戶端代碼,直接編寫處理這幾種狀況的方法:

public class RequestHandler {
    private string temp = string.Empty;

    public string[] GetActualString(string input) {
        return GetActualString(input, null);
    }

    private string[] GetActualString(string input, List<string> outputList) {
        if (outputList == null)
            outputList = new List<string>();

        if (!String.IsNullOrEmpty(temp))
            input = temp + input;

        string output = "";
        string pattern = @"(?<=^\[length=)(\d+)(?=\])";
        int length;
                    
        if (Regex.IsMatch(input, pattern)) {

            Match m = Regex.Match(input, pattern);

            // 獲取消息字符串實際應有的長度
            length = Convert.ToInt32(m.Groups[0].Value);

            // 獲取須要進行截取的位置
            int startIndex = input.IndexOf(']') + 1;

            // 獲取今後位置開始後全部字符的長度
            output = input.Substring(startIndex);

            if (output.Length == length) {
                // 若是output的長度與消息字符串的應有長度相等
                // 說明恰好是完整的一條信息
                outputList.Add(output);
                temp = "";
            } else if (output.Length < length) {
                // 若是以後的長度小於應有的長度,
                // 說明沒有發完整,則應將整條信息,包括元數據,所有緩存
                // 與下一條數據合併起來再進行處理
                temp = input;
                // 此時程序應該退出,由於須要等待下一條數據到來才能繼續處理

            } else if (output.Length > length) {
                // 若是以後的長度大於應有的長度,
                // 說明消息發完整了,可是有多餘的數據
                // 多餘的數據多是截斷消息,也多是多條完整消息

                // 截取字符串
                output = output.Substring(0, length);
                outputList.Add(output);
                temp = "";

                // 縮短input的長度
                input = input.Substring(startIndex + length);

                // 遞歸調用
                GetActualString(input, outputList);
            }
        } else {    // 說明「[」,「]」就不完整
            temp = input;
        }

        return outputList.ToArray();
    }
}

這個方法接收一個知足協議格式要求的輸入字符串,而後返回一個數組,這是由於若是出現屢次請求合併成一個發送過來的狀況,那麼就將它們所有返回。隨後簡單起見,我在這個類中添加了一個靜態的Test()方法和PrintOutput()幫助方法,進行了一個簡單的測試,注意我直接輸入了length=13,這個是我提早計算好的。

public static void Test() {
    RequestHandler handler = new RequestHandler();
    string input;

    // 第一種狀況測試 - 一條消息完整發送
    input = "[length=13]明天中秋,祝你們節日快樂!";
    handler.PrintOutput(input);

    // 第二種狀況測試 - 兩條完整消息一次發送
    input = "明天中秋,祝你們節日快樂!";
    input = String.Format
        ("[length=13]{0}[length=13]{0}", input);
    handler.PrintOutput(input);

    // 第三種狀況測試A - 兩條消息不完整發送
    input = "[length=13]明天中秋,祝你們節日快樂![length=13]明天中秋";
    handler.PrintOutput(input);

    input = ",祝你們節日快樂!";
    handler.PrintOutput(input);

    // 第三種狀況測試B - 兩條消息不完整發送
    input = "[length=13]明天中秋,祝你們";
    handler.PrintOutput(input);

    input = "節日快樂![length=13]明天中秋,祝你們節日快樂!";
    handler.PrintOutput(input);

    
    // 第四種狀況測試 - 元數據不完整
    input = "[leng";
    handler.PrintOutput(input);     // 不會有輸出

    input = "th=13]明天中秋,祝你們節日快樂!";
    handler.PrintOutput(input);

}

// 用於測試輸出
private void PrintOutput(string input) {
    Console.WriteLine(input);
    string[] outputArray = GetActualString(input);
    foreach (string output in outputArray) {
        Console.WriteLine(output);
    }
    Console.WriteLine();
}

運行上面的程序,能夠獲得以下的輸出:

OK,從上面的輸出能夠看到,這個方法可以知足咱們的要求。對於這篇文章最開始提出的問題,能夠很輕鬆地經過加入這個方法來解決,這裏就再也不演示了,但在本文所附帶的源代碼含有修改過的程序。在這裏花費了很長的時間,接下來讓咱們回到正題,看下如何使用異步方式完成上一篇中的程序吧。

異步傳輸字符串

在上一篇中,咱們由簡到繁,提到了服務端的四種方式:服務一個客戶端的一個請求、服務一個客戶端的多個請求、服務多個客戶端的一個請求、服務多個客戶端的多個請求。咱們說到能夠將裏層的while循環交給一個新建的線程去讓它來完成。除了這種方式之外,咱們還可使用一種更好的方式――使用線程池中的線程來完成。咱們可使用BeginRead()、BeginWrite()等異步方法,同時讓這BeginRead()方法和它的回調方法造成一個相似於while的無限循環:首先在第一層循環中,接收到一個客戶端後,調用BeginRead(),而後爲該方法提供一個讀取完成後的回調方法,而後在回調方法中對收到的字符進行處理,隨後在回調方法中接着調用BeginRead()方法,並傳入回調方法自己。

因爲程序實現功能和上一篇徹底相同,我就再也不細述了。而關於異步調用方法更多詳細內容,能夠參見 C#中的委託和事件(續)

1.服務端的實現

當程序愈來愈複雜的時候,就須要愈來愈高的抽象,因此從如今起咱們再也不把全部的代碼所有都扔進Main()裏,此次我建立了一個RemoteClient類,它對於服務端獲取到的TcpClient進行了一個包裝:

public class RemoteClient {
    private TcpClient client;
    private NetworkStream streamToClient;
    private const int BufferSize = 8192;
    private byte[] buffer;
    private RequestHandler handler;
    
    public RemoteClient(TcpClient client) {
        this.client = client;

        // 打印鏈接到的客戶端信息
        Console.WriteLine("\nClient Connected!{0} <-- {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);

        // 得到流
        streamToClient = client.GetStream();
        buffer = new byte[BufferSize];

        // 設置RequestHandler
        handler = new RequestHandler();

        // 在構造函數中就開始準備讀取
        AsyncCallback callBack = new AsyncCallback(ReadComplete);
        streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
    }

    // 再讀取完成時進行回調
    private void ReadComplete(IAsyncResult ar) {
        int bytesRead = 0;
        try {
            lock (streamToClient) {
                bytesRead = streamToClient.EndRead(ar);
                Console.WriteLine("Reading data, {0} bytes ...", bytesRead);
            }
            if (bytesRead == 0) throw new Exception("讀取到0字節");

            string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Array.Clear(buffer,0,buffer.Length);        // 清空緩存,避免髒讀
        
            string[] msgArray = handler.GetActualString(msg);   // 獲取實際的字符串

            // 遍歷得到到的字符串
            foreach (string m in msgArray) {
                Console.WriteLine("Received: {0}", m);
                string back = m.ToUpper();

                // 將獲得的字符串改成大寫並從新發送
                byte[] temp = Encoding.Unicode.GetBytes(back);
                streamToClient.Write(temp, 0, temp.Length);
                streamToClient.Flush();
                Console.WriteLine("Sent: {0}", back);
            }               

            // 再次調用BeginRead(),完成時調用自身,造成無限循環
            lock (streamToClient) {
                AsyncCallback callBack = new AsyncCallback(ReadComplete);
                streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
            }
        } catch(Exception ex) {
            if(streamToClient!=null)
                streamToClient.Dispose();
            client.Close();
            Console.WriteLine(ex.Message);      // 捕獲異常時退出程序              
        }
    }
}

隨後,咱們在主程序中僅僅建立TcpListener類型實例,因爲RemoteClient類在構造函數中已經完成了初始化的工做,因此咱們在下面的while循環中咱們甚至不須要調用任何方法:

class Server {
    static void Main(string[] args) {
        Console.WriteLine("Server is running ... ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);

        listener.Start();           // 開始偵聽
        Console.WriteLine("Start Listening ...");

        while (true) {
            // 獲取一個鏈接,同步方法,在此處中斷
            TcpClient client = listener.AcceptTcpClient();              
            RemoteClient wapper = new RemoteClient(client);
        }
    }
}

好了,服務端的實現如今就完成了,接下來咱們再看一下客戶端的實現:

2.客戶端的實現

與服務端相似,咱們首先對TcpClient進行一個簡單的包裝,使它的使用更加方便一些,由於它是服務端的客戶,因此咱們將類的名稱命名爲ServerClient:

public class ServerClient {
    private const int BufferSize = 8192;
    private byte[] buffer;
    private TcpClient client;
    private NetworkStream streamToServer;
    private string msg = "Welcome to TraceFact.Net!";

    public ServerClient() {
        try {
            client = new TcpClient();
            client.Connect("localhost", 8500);      // 與服務器鏈接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        buffer = new byte[BufferSize];

        // 打印鏈接到的服務端信息
        Console.WriteLine("Server Connected!{0} --> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);

        streamToServer = client.GetStream();
    }

    // 連續發送三條消息到服務端
    public void SendMessage(string msg) {

        msg = String.Format("[length={0}]{1}", msg.Length, msg);

        for (int i = 0; i <= 2; i++) {
            byte[] temp = Encoding.Unicode.GetBytes(msg);   // 得到緩存
            try {
                streamToServer.Write(temp, 0, temp.Length); // 發往服務器
                Console.WriteLine("Sent: {0}", msg);
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                break;
            }
        }

        lock (streamToServer) {
            AsyncCallback callBack = new AsyncCallback(ReadComplete);
            streamToServer.BeginRead(buffer, 0, BufferSize, callBack, null);
        }
    }

    public void SendMessage() {
        SendMessage(this.msg);
    }

    // 讀取完成時的回調方法
    private void ReadComplete(IAsyncResult ar) {
        int bytesRead;

        try {
            lock (streamToServer) {
                bytesRead = streamToServer.EndRead(ar);
            }
            if (bytesRead == 0) throw new Exception("讀取到0字節");

            string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received: {0}", msg);
            Array.Clear(buffer, 0, buffer.Length);      // 清空緩存,避免髒讀

            lock (streamToServer) {
                AsyncCallback callBack = new AsyncCallback(ReadComplete);
                streamToServer.BeginRead(buffer, 0, BufferSize, callBack, null);
            }
        } catch (Exception ex) {
            if(streamToServer!=null)
                streamToServer.Dispose();
            client.Close();

            Console.WriteLine(ex.Message);
        }
    }
}

在上面的SendMessage()方法中,咱們讓它連續發送了三條一樣的消息,這麼僅僅是爲了測試,由於異步操做一樣會出現上面說過的:服務器將客戶端的請求拆開了的狀況。最後咱們在Main()方法中建立這個類型的實例,而後調用SendMessage()方法進行測試:

class Client {
    static void Main(string[] args) {
        ConsoleKey key;

        ServerClient client = new ServerClient();
        client.SendMessage();
        
        Console.WriteLine("\n\n輸入\"Q\"鍵退出。");
        do {
            key = Console.ReadKey(true).Key;
        } while (key != ConsoleKey.Q);
    }
}

是否是感受很清爽?由於良好的代碼重構,使得程序在複雜程度提升的狀況下依然能夠在必定程度上保持良好的閱讀性。

3.程序測試

最後一步,咱們先運行服務端,接着連續運行兩個客戶端,看看它們的輸出分別是什麼:

你們能夠看到,在服務端,咱們能夠鏈接多個客戶端,同時爲它們服務;除此之外,由接收的字節數發現,兩個客戶端均有兩個請求被服務端合併成了一條請求,由於咱們在其中加入了特殊的協議,因此在服務端能夠對這種狀況進行良好的處理。

在客戶端,咱們沒有采起相似的處理,因此當客戶端收到應答時,仍然會發生請求合併的狀況。對於這種狀況,我想你們已經知道該如何處理了,就再也不多費口舌了。

使用這種定義協議的方式有它的優勢,但缺點也很明顯,若是客戶知道了這個協議,有意地輸入[length=xxx],可是後面的長度卻不匹配,此時程序就會出錯。可選的解決辦法是對「[」和「]」進行編碼,當客戶端有意輸入這兩個字符時,咱們將它替換成「\[」和「\]」或者別的字符,在讀取後再將它還原。

關於這個範例就到此結束了,剩下的兩個範例都將採用異步傳輸的方式,而且會加入更多的協議內容。下一篇咱們將介紹如何向服務端發送或接收文件。

 

出處:http://www.cnblogs.com/JimmyZhang/archive/2008/09/16/1291854.html

相關文章
相關標籤/搜索