程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#網絡編程(異步傳輸字符串) - Part.3

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

編輯:關於C#

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

消息發送時的問題

這個問題就是:客戶端分兩次向流中寫入數據(比如字符串)時,我們主觀上將這兩次寫入視為兩次 請求;然而服務端有可能將這兩次合起來視為一條請求,這在兩個請求間隔時間比較短的情況下尤其如此 。同樣,也有可能客戶端發出一條請求,但是服務端將其視為兩條請求處理。下面列出了可能的情況,假 設我們在客戶端連續發送兩條“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;
    }
}

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

可以看到,盡管上面將消息分成了三條單獨發送,但是服務端卻將後兩條合並成了一條。對於這些情 況,我們可以這樣處理:就好像HTTP協議一樣,在實際的請求和應答內容之前包含了HTTP頭,其中是一些 與請求相關的信息。我們也可以訂立自己的協議,來解決這個問題,比如說,對於上面的情況,我們就可 以定義這樣一個協議:

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

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],但是後面的長度卻不匹配,此時程序就會出錯。可選的解決辦法是對“[”和“]”進行編 碼,當客戶端有意輸入這兩個字符時,我們將它替換成“\[”和“\]”或者別的字符,在讀取後再將它還 原 。

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

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved