一、摘要
本篇闡述基於TCP通訊協定的非同步實現。
二、實驗平台
Visual Studio 2010
三、非同步通訊實現原理及常用方法
3.1 建立串連
在同步模式中,在伺服器上使用Accept方法接入串連請求,而在用戶端則使用Connect方法來串連伺服器。相對地,在非同步模式下,伺服器可以使用BeginAccept方法和EndAccept方法來完成串連到用戶端的任務,在用戶端則通過BeginConnect方法和EndConnect方法來實現與伺服器的串連。
BeginAccept在非同步方式下傳入的串連嘗試,它允許其他動作而不必等待串連建立才繼續執行後面程式。在調用BeginAccept之前,必須使用Listen方法來偵聽是否有串連請求,BeginAccept的函數原型為:
BeginAccept(AsyncCallback AsyncCallback, Ojbect state)
參數:
AsyncCallBack:代表回呼函數
state:表示狀態資訊,必須保證state中包含socket的控制代碼
使用BeginAccept的基本流程是:
(1)建立本地終節點,並建立通訊端與本地終節點進行綁定;
(2)在連接埠上偵聽是否有新的串連請求;
(3)請求開始接入新的串連,傳入Socket的執行個體或者StateOjbect的執行個體。
參考代碼:
//定義IP地址IPAddress local = IPAddress.Parse("127.0,0,1");IPEndPoint iep = new IPEndPoint(local,13000);//建立伺服器的socket對象Socket server = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);server.Bind(iep);server.Listen(20);server.BeginAccecpt(new AsyncCallback(Accept),server);
當BeginAccept()方法調用結束後,一旦新的串連發生,將調用回呼函數,而該回呼函數必須包括用來結束接入串連操作的EndAccept()方法。
該方法參數列表為 Socket EndAccept(IAsyncResult iar)
下面為回呼函數的執行個體:
void Accept(IAsyncResult iar){ //還原傳入的原始通訊端 Socket MyServer = (Socket)iar.AsyncState; //在原始通訊端上調用EndAccept方法,返回新的通訊端 Socket service = MyServer.EndAccept(iar);}
至此,伺服器端已經準備好了。用戶端應通過BeginConnect方法和EndConnect來遠端連線主機。在調用BeginConnect方法時必須註冊相應的回呼函數並且至少傳遞一個Socket的執行個體給state參數,以保證EndConnect方法中能使用原始的通訊端。下面是一段是BeginConnect的調用:
Socket socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp)IPAddress ip=IPAddress.Parse("127.0.0.1");IPEndPoint iep=new IPEndPoint(ip,13000);socket.BeginConnect(iep, new AsyncCallback(Connect),socket);
EndConnect是一種阻塞方法,用於完成BeginConnect方法的非同步串連誒遠程主機的請求。在註冊了回呼函數後必須接收BeginConnect方法返回的IASynccReuslt作為參數。下面為代碼示範:
void Connect(IAsyncResult iar){ Socket client=(Socket)iar.AsyncState; try { client.EndConnect(iar); } catch (Exception e) { Console.WriteLine(e.ToString()); } finally { }}
除了採用上述方法建立串連之後,也可以採用TcpListener類裡面的方法進行串連建立。下面是伺服器端對關於TcpListener類使用BeginAccetpTcpClient方法處理一個傳入的串連嘗試。以下是使用BeginAccetpTcpClient方法和EndAccetpTcpClient方法的代碼:
public static void DoBeginAccept(TcpListener listner){ //開始從用戶端監聽串連 Console.WriteLine("Waitting for a connection"); //接收串連 //開始準備接入新的串連,一旦有新串連嘗試則調用回呼函數DoAcceptTcpCliet listner.BeginAcceptTcpClient(new AsyncCallback(DoAcceptTcpCliet), listner);}//處理用戶端的串連public static void DoAcceptTcpCliet(IAsyncResult iar){ //還原原始的TcpListner對象 TcpListener listener = (TcpListener)iar.AsyncState; //完成串連的動作,並返回新的TcpClient TcpClient client = listener.EndAcceptTcpClient(iar); Console.WriteLine("串連成功");}
代碼的處理邏輯為:
(1)調用BeginAccetpTcpClient方法開開始串連新的串連,當串連視圖發生時,回呼函數被調用以完成串連操作;
(2)上面DoAcceptTcpCliet方法通過AsyncState屬性獲得由BeginAcceptTcpClient傳入的listner執行個體;
(3)在得到listener對象後,用它調用EndAcceptTcpClient方法,該方法返回新的包含用戶端資訊的TcpClient。
BeginConnect方法和EndConnect方法可用於用戶端嘗試建立與服務端的串連,這裡和第一種方法並無區別。下面看執行個體:
public void doBeginConnect(IAsyncResult iar){ Socket client=(Socket)iar.AsyncState; //開始與遠程主機進行串連 client.BeginConnect(serverIP[0],13000,requestCallBack,client); Console.WriteLine("開始與伺服器進行串連");}private void requestCallBack(IAsyncResult iar){ try { //還原原始的TcpClient對象 TcpClient client=(TcpClient)iar.AsyncState; // client.EndConnect(iar); Console.WriteLine("與伺服器{0}串連成功",client.Client.RemoteEndPoint); } catch(Exception e) { Console.WriteLine(e.ToString()); } finally { }}
以上是建立串連的兩種方法。可根據需要選擇使用。
3.2 發送與接受資料
在建立了通訊端的串連後,就可以伺服器端和用戶端之間進行資料通訊了。非同步通訊端用BeginSend和EndSend方法來負責資料的發送。注意在調用BeginSend方法前要確保雙方都已經建立串連,否則會出異常。下面示範代碼:
private static void Send(Socket handler, String data){ // Convert the string data to byte data using ASCII encoding. byte[] byteData = Encoding.ASCII.GetBytes(data); // Begin sending the data to the remote device. handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler);}private static void SendCallback(IAsyncResult ar){ try { // Retrieve the socket from the state object. Socket handler = (Socket)ar.AsyncState; // Complete sending the data to the remote device. int bytesSent = handler.EndSend(ar); Console.WriteLine("Sent {0} bytes to client.", bytesSent); handler.Shutdown(SocketShutdown.Both); handler.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); }}
接收資料是通過BeginReceive和EndReceive方法:
private static void Receive(Socket client){ try { // Create the state object. StateObject state = new StateObject(); state.workSocket = client; // Begin receiving the data from the remote device. client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } catch (Exception e) { Console.WriteLine(e.ToString()); }}private static void ReceiveCallback(IAsyncResult ar){ try { // Retrieve the state object and the client socket // from the asynchronous state object. StateObject state = (StateObject)ar.AsyncState; Socket client = state.workSocket; // Read data from the remote device. int bytesRead = client.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead)); // Get the rest of the data. client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } else { // All the data has arrived; put it in response. if (state.sb.Length > 1) { response = state.sb.ToString(); } // Signal that all bytes have been received. receiveDone.Set(); } } catch (Exception e) { Console.WriteLine(e.ToString()); }}
上述代碼的處理邏輯為:
(1)首先處理串連的回呼函數裡得到的通訊通訊端client,接著開始接收資料;
(2)當資料發送到緩衝區中,BeginReceive方法試圖從buffer數組中讀取長度為buffer.length的資料區塊,並返回接收到的資料量bytesRead。最後接收並列印資料。
除了上述方法外,還可以使用基於NetworkStream相關的非同步發送和接收方法,下面是基於NetworkStream相關的非同步發送和接收方法的使用介紹。
NetworkStream使用BeginRead和EndRead方法進行讀操作,使用BeginWreite和EndWrete方法進行寫操作,下面看執行個體:
static void DataHandle(TcpClient client){ TcpClient tcpClient = client; //使用TcpClient的GetStream方法擷取網路流 NetworkStream ns = tcpClient.GetStream(); //檢查網路流是否可讀 if(ns.CanRead) { //定義緩衝區 byte[] read = new byte[1024]; ns.BeginRead(read,0,read.Length,new AsyncCallback(myReadCallBack),ns); } else { Console.WriteLine("無法從網路中讀取流資料"); }}public static void myReadCallBack(IAsyncResult iar){ NetworkStream ns = (NetworkStream)iar.AsyncState; byte[] read = new byte[1024]; String data = ""; int recv; recv = ns.EndRead(iar); data = String.Concat(data, Encoding.ASCII.GetString(read, 0, recv)); //接收到的訊息長度可能大於緩衝區總大小,反覆迴圈直到讀完為止 while (ns.DataAvailable) { ns.BeginRead(read, 0, read.Length, new AsyncCallback(myReadCallBack), ns); } //列印 Console.WriteLine("您收到的資訊是" + data);}
3.3 程式阻塞與非同步中的同步問題
.Net裡提供了EventWaitHandle類來表示一個線程的同步事件。EventWaitHandle即事件等待控制代碼,他允許線程通過作業系統互發訊號和等待彼此的訊號來達到線程同步的目的。這個類有2個子類,分別為AutoRestEevnt(自動重設)和ManualRestEvent(手動重設)。下面是線程同步的幾個方法:
(1)Rset方法:將事件狀態設為非終止狀態,導致線程阻塞。這裡的線程阻塞是指允許其他需要等待的線程進行阻塞即讓含WaitOne()方法的線程阻塞;
(2)Set方法:將事件狀態設為終止狀態,允許一個或多個等待線程繼續。該方法發送一個訊號給作業系統,讓處於等待的某個線程從阻塞狀態轉換為繼續運行,即WaitOne方法的線程不在阻塞;
(3)WaitOne方法:阻塞當前線程,直到當前的等待控制代碼收到訊號。此方法將一直使本線程處於阻塞狀態直到收到訊號為止,即當其他非阻塞進程調用set方法時可以繼續執行。
public static void StartListening(){ // Data buffer for incoming data. byte[] bytes = new Byte[1024]; // Establish the local endpoint for the socket. // The DNS name of the computer // running the listener is "host.contoso.com". //IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); //IPAddress ipAddress = ipHostInfo.AddressList[0]; IPAddress ipAddress = IPAddress.Parse("127.0.0.1"); IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000); // Create a TCP/IP socket. Socket listener = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp); // Bind the socket to the local //endpoint and listen for incoming connections. try { listener.Bind(localEndPoint); listener.Listen(100); while (true) { // Set the event to nonsignaled state. allDone.Reset(); // Start an asynchronous socket to listen for connections. Console.WriteLine("Waiting for a connection..."); listener.BeginAccept(new AsyncCallback(AcceptCallback),listener); // Wait until a connection is made before continuing. allDone.WaitOne(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } Console.WriteLine("\nPress ENTER to continue..."); Console.Read();}
上述代碼的邏輯為:
(1)試用了ManualRestEvent對象建立一個等待控制代碼,在調用BeginAccept方法前使用Rest方法允許其他線程阻塞;
(2)為了防止在串連完成之前對通訊端進行讀寫操作,務必要在BeginAccept方法後調用WaitOne來讓線程進入阻塞狀態。
當有串連接入後系統會自動調用會調用回呼函數,所以當代碼執行到回呼函數時說明串連已經成功,並在函數的第一句就調用Set方法讓處於等待的線程可以繼續執行。
四、執行個體
下面是一個執行個體,用戶端請求串連,伺服器端偵聽連接埠,當串連建立之後,伺服器發送字串給用戶端,用戶端收到後並回傳給伺服器端。
伺服器端代碼:
using System;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;// State object for reading client data asynchronously public class StateObject{ // Client socket. public Socket workSocket = null; // Size of receive buffer. public const int BufferSize = 1024; // Receive buffer. public byte[] buffer = new byte[BufferSize]; // Received data string. public StringBuilder sb = new StringBuilder();}public class AsynchronousSocketListener{ // Thread signal. public static ManualResetEvent allDone = new ManualResetEvent(false); public AsynchronousSocketListener() { } public static void StartListening() { // Data buffer for incoming data. byte[] bytes = new Byte[1024]; // Establish the local endpoint for the socket. // The DNS name of the computer // running the listener is "host.contoso.com". //IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); //IPAddress ipAddress = ipHostInfo.AddressList[0]; IPAddress ipAddress = IPAddress.Parse("127.0.0.1"); IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000); // Create a TCP/IP socket. Socket listener = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp); // Bind the socket to the local //endpoint and listen for incoming connections. try { listener.Bind(localEndPoint); listener.Listen(100); while (true) { // Set the event to nonsignaled state. allDone.Reset(); // Start an asynchronous socket to listen for connections. Console.WriteLine("Waiting for a connection..."); listener.BeginAccept(new AsyncCallback(AcceptCallback),listener); // Wait until a connection is made before continuing. allDone.WaitOne(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } Console.WriteLine("\nPress ENTER to continue..."); Console.Read(); } public static void AcceptCallback(IAsyncResult ar) { // Signal the main thread to continue. allDone.Set(); // Get the socket that handles the client request. Socket listener = (Socket)ar.AsyncState; Socket handler = listener.EndAccept(ar); // Create the state object. StateObject state = new StateObject(); state.workSocket = handler; handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } public static void ReadCallback(IAsyncResult ar) { String content = String.Empty; // Retrieve the state object and the handler socket // from the asynchronous state object. StateObject state = (StateObject)ar.AsyncState; Socket handler = state.workSocket; // Read data from the client socket. int bytesRead = handler.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead)); // Check for end-of-file tag. If it is not there, read // more data. content = state.sb.ToString(); if (content.IndexOf("<EOF>") > -1) { // All the data has been read from the // client. Display it on the console. Console.WriteLine("Read {0} bytes from socket. \n Data : {1}", content.Length, content); // Echo the data back to the client. Send(handler, content); } else { // Not all data received. Get more. handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } } } private static void Send(Socket handler, String data) { // Convert the string data to byte data using ASCII encoding. byte[] byteData = Encoding.ASCII.GetBytes(data); // Begin sending the data to the remote device. handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler); } private static void SendCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket handler = (Socket)ar.AsyncState; // Complete sending the data to the remote device. int bytesSent = handler.EndSend(ar); Console.WriteLine("Sent {0} bytes to client.", bytesSent); handler.Shutdown(SocketShutdown.Both); handler.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } public static int Main(String[] args) { StartListening(); return 0; }}
用戶端代碼:
using System;using System.Net;using System.Net.Sockets;using System.Threading;using System.Text;// State object for receiving data from remote device. public class StateObject{ // Client socket. public Socket workSocket = null; // Size of receive buffer. public const int BufferSize = 256; // Receive buffer. public byte[] buffer = new byte[BufferSize]; // Received data string. public StringBuilder sb = new StringBuilder();}public class AsynchronousClient{ // The port number for the remote device. private const int port = 11000; // ManualResetEvent instances signal completion. private static ManualResetEvent connectDone = new ManualResetEvent(false); private static ManualResetEvent sendDone = new ManualResetEvent(false); private static ManualResetEvent receiveDone = new ManualResetEvent(false); // The response from the remote device. private static String response = String.Empty; private static void StartClient() { // Connect to a remote device. try { // Establish the remote endpoint for the socket. // The name of the // remote device is "host.contoso.com". //IPHostEntry ipHostInfo = Dns.Resolve("user"); //IPAddress ipAddress = ipHostInfo.AddressList[0]; IPAddress ipAddress = IPAddress.Parse("127.0.0.1"); IPEndPoint remoteEP = new IPEndPoint(ipAddress, port); // Create a TCP/IP socket. Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // Connect to the remote endpoint. client.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), client); connectDone.WaitOne(); // Send test data to the remote device. Send(client, "This is a test<EOF>"); sendDone.WaitOne(); // Receive the response from the remote device. Receive(client); receiveDone.WaitOne(); // Write the response to the console. Console.WriteLine("Response received : {0}", response); // Release the socket. client.Shutdown(SocketShutdown.Both); client.Close(); Console.ReadLine(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void ConnectCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket client = (Socket)ar.AsyncState; // Complete the connection. client.EndConnect(ar); Console.WriteLine("Socket connected to {0}", client.RemoteEndPoint.ToString()); // Signal that the connection has been made. connectDone.Set(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void Receive(Socket client) { try { // Create the state object. StateObject state = new StateObject(); state.workSocket = client; // Begin receiving the data from the remote device. client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void ReceiveCallback(IAsyncResult ar) { try { // Retrieve the state object and the client socket // from the asynchronous state object. StateObject state = (StateObject)ar.AsyncState; Socket client = state.workSocket; // Read data from the remote device. int bytesRead = client.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead)); // Get the rest of the data. client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state); } else { // All the data has arrived; put it in response. if (state.sb.Length > 1) { response = state.sb.ToString(); } // Signal that all bytes have been received. receiveDone.Set(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void Send(Socket client, String data) { // Convert the string data to byte data using ASCII encoding. byte[] byteData = Encoding.ASCII.GetBytes(data); // Begin sending the data to the remote device. client.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), client); } private static void SendCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket client = (Socket)ar.AsyncState; // Complete sending the data to the remote device. int bytesSent = client.EndSend(ar); Console.WriteLine("Sent {0} bytes to server.", bytesSent); // Signal that all bytes have been sent. sendDone.Set(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } public static int Main(String[] args) { StartClient(); return 0; }}
五、實驗結果
圖1 伺服器端介面
圖2 用戶端介面