這篇文章將前進一大步,使用非同步方式進行服務端程式設計,使其成為真正意義上的伺服器:可為多個客戶端的多次請求服務。但開始前,需解決上一節遺留的問題。
客戶端分兩次寫入資料流(如字串),主觀視為兩次請求;但服務端可能合併視為一條請求,特別在請求間隔極短時。反之,客戶端發一條請求,服務端可能視為兩條。假設連續發送三個 "Welcome to TraceFact.Net!",資料到達服務端可能有三種情況:理想獨立接收、一條訊息被拆成兩條、兩條訊息合併成一條。
解決方案:訂立自定義協定,類似 HTTP 標頭。定義協定:[length=XXX],XXX 為實際字串長度(非位元組陣列長度)。發送資料為 [length=25]Welcome to TraceFact.Net!。服務端先讀取「中繼資料」,再根據長度讀取實際資料,處理三種情況:中括號完整可讀取 length、中括號不完整需緩存、資料長度與 length 比較決定是否完整/需緩存/有多餘資料遞迴處理。
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。
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);
}
}
}
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 變長編碼,ASCII 字元佔 1 位元組,中文佔 3 位元組;Unicode 固定 2 位元組,ASCII 字元第 2 位元組補零,中文佔 2 位元組。