首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 網路程式設計 Part 3:非同步傳輸字串與訊息邊界處理

     編輯:關於C#

這篇文章將前進一大步,使用非同步方式進行服務端程式設計,使其成為真正意義上的伺服器:可為多個客戶端的多次請求服務。但開始前,需解決上一節遺留的問題。

訊息發送時的問題:訊息邊界

客戶端分兩次寫入資料流(如字串),主觀視為兩次請求;但服務端可能合併視為一條請求,特別在請求間隔極短時。反之,客戶端發一條請求,服務端可能視為兩條。假設連續發送三個 "Welcome to TraceFact.Net!",資料到達服務端可能有三種情況:理想獨立接收、一條訊息被拆成兩條、兩條訊息合併成一條。

解決方案:訂立自定義協定,類似 HTTP 標頭。定義協定:[length=XXX],XXX 為實際字串長度(非位元組陣列長度)。發送資料為 [length=25]Welcome to TraceFact.Net!。服務端先讀取「中繼資料」,再根據長度讀取實際資料,處理三種情況:中括號完整可讀取 length、中括號不完整需緩存、資料長度與 length 比較決定是否完整/需緩存/有多餘資料遞迴處理。

RequestHandler 協定處理類別

public class RequestHandler {
    private string temp = string.Empty;
    public string[] GetActualString(string input) { return GetActualString(input, null); }
    private string[] GetActualString(string input, List outputList) {
        if (outputList == null) outputList = new List();
        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) {
                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.Substring(startIndex + length);
                GetActualString(input, outputList);
            }
        } else {
            temp = input;
        }
        return outputList.ToArray();
    }
}

測試驗證:完整訊息、多訊息合併、訊息截斷分批、中繼資料不完整等情況均能正確處理。

非同步傳輸字串實作

使用 BeginRead、BeginWrite 等非同步方法,讓 BeginRead 與回呼方法形成類似 while 的無限迴圈:外層迴圈接收客戶端後呼叫 BeginRead,提供讀取完成回呼,回呼中處理字串並再次呼叫 BeginRead。

服務端:RemoteClient 封裝類別

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];
        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);
            }
            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);
        }
    }
}

主程式極簡

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 wrapper = new RemoteClient(client);
        }
    }
}

客戶端: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);
        }
    }
}

測試結果

啟動服務端與兩個客戶端,服務端可同時服務多客戶端,並正確處理請求合併情況。客戶端未加協定處理,仍會發生回應合併,讀者可自行擴展。

附錄:ASCII、UTF-8、Unicode 編碼中英文字元大小比較

ASCII 無法儲存中文;UTF-8 變長編碼,ASCII 字元佔 1 位元組,中文佔 3 位元組;Unicode 固定 2 位元組,ASCII 字元第 2 位元組補零,中文佔 2 位元組。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved