三 异步套接字
虽然还有许多别的方法解决同步套接字中的问题的方法,但是综合比较来看,异步套接字无疑是大多数情况下最好的解决办法,这个问题稍后讨论。
1 原理
首先来说一下异步的原理(根据自己的理解写的,如果有不正确的地方,还望指正)。
思想其实很简单。在同步里面,当接收或者发送数据的时候,程序的进程是中止的,一直在等待,那么想要解决这个问题自然就会想到——接收或者发送数据的时候程序继续往下执行不就行了嘛!?(没打错,是两个标点。)
但是,这样想的话,问题就来了:我接受或者发送数据完毕之后本来是想执行一些针对这些数据或者另外相关的操作的,现在不理会数据的接受或者发送的话,那我的这些想要的操作怎么去执行啊?
举个例子,服务器端希望接受客户端的定制化的请求,客户端发送的数据是作为服务器端返回给客户端数据的参数的,这样当然要根据接收的数据返回特定的数据。现在不理会接收数据是否完毕程序继续执行的话怎么返回特定数据呢?
这个问题是这样解决的:你不是有许多操作吗,那好,我在该操作发生前定义该操作为之后的特定操作。
还是举例说明。服务器端希望在接收完客户端发送的数据后再将该数据发送回给客户端。那么,在这里我们就可以将把接收的数据发送回给客户端作为一个操作,程序里面自然就是一个函数了,将这个函数定义为服务器端接收完数据之后立即执行的函数。那么如何定义呢?.NET里面提供了专门的方法和委托,这个在代码里面说明。
原理就是这样,很简单,下面看代码示例吧。
2 代码示例
首先是服务器端代码。
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; // 这个对象只是将用到的四个变量包装一下 public class StateObject { public Socket workSocket = null; public const int BufferSize = 1024; public byte[] buffer = new byte[BufferSize]; public StringBuilder sb = new StringBuilder(); } public class AsynchronousSocketListener { // 控制线程的的对象 public static ManualResetEvent allDone = new ManualResetEvent(false); public AsynchronousSocketListener() { } public static void StartListening() { byte[] bytes = new Byte[1024]; // 这个应该很熟了,和同步的一样 IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName()); IPAddress ipAddress = ipHostInfo.AddressList[0]; IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000); Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp ); try { listener.Bind(localEndPoint); listener.Listen(100); while (true) { // Set the event to nonsignaled state. allDone.Reset(); Console.WriteLine("Waiting for a connection..."); //不同的地方开始了。这里采用的接收连接的方法不同 //就像上面说的,我想在BeginAccept方法接收连接之后执行AcceptCallback函数 //这里只是定义,程序到这还会继续往下执行,一直到allDone.WaitOne(); listener.BeginAccept( new AsyncCallback(AcceptCallback), listener ); // 这句代码将使程序不能继续往下执行,直到 allDone.Set();执行 // allDone.WaitOne(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } Console.WriteLine("\nPress ENTER to continue..."); Console.Read(); } public static void AcceptCallback(IAsyncResult ar) { // 当接收连接后,按照前面的定义会执行该函数,首先就是让主线程继续往下执行 allDone.Set(); //将接收的连接传递近来 Socket listener = (Socket) ar.AsyncState; //调用EndAccept方法表示连接已经建立,建立的连接就是该方法的返回的对象 Socket handler = listener.EndAccept(ar); StateObject state = new StateObject(); state.workSocket = handler; //这里又出现了类似的定义。可以看出,BeginReceive函数的参数和同步里面Receive函数的参数前面是相同的 //只是后面多出了两个:定义在BeginReceive函数执行完毕以后所要执行的操作 //这里定义的是ReadCallback函数 handler.BeginReceive( state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } public static void ReadCallback(IAsyncResult ar) { String content = String.Empty; StateObject state = (StateObject) ar.AsyncState; Socket handler = state.workSocket; //这里EndReceive函数不能理解为结束接收的意思,应该是至今为止接收到的意思 //因为即使使用了该函数,如果数据没有接收完需要再次接收的时候,数据是在前面接收的基础上 //接收剩余的部分 int bytesRead = handler.EndReceive(ar); //下面的if语句虽然没有用循环语句的表象,但是可以看到由于使用了递归的方法,因此整体可以看做是一个循环 //该循环确保接收到所有的数据 //当确定所有数据接受完毕后,会调用send方法 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)); content = state.sb.ToString(); if (content.IndexOf("<EOF>") > -1) { Console.WriteLine("Read {0} bytes from socket. \n Data : {1}", content.Length, content ); 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) { byte[] byteData = Encoding.ASCII.GetBytes(data); // 这里和前面的解释类似 handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler); } private static void SendCallback(IAsyncResult ar) { try { Socket handler = (Socket) ar.AsyncState; 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; } }
从学习的角度来说,代码稍微有点长,这是异步相对于同步比较繁琐的地方,相比于同步只需一句代码而言。但是,我们可以将每个函数抽象成一句代码来看,那逻辑还是很清晰的。
下面再对代码中的一些小细节进行一下说明。
handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler); 这样类似的函数,最后的两个参数,一个是委托,另一个是一个对象。委托很容易理解,就是之后要执行的函数操作,那后面的那个参数呢?
可以结合这个SendCallback(IAsyncResult ar)函数来理解。上面委托之后的对象实际上包含在SendCallback函数的参数IAsyncResult ar里面。vs帮助文档里是这样解释的:IAsyncResult 表示异步操作的状态,IAsyncResult 类有一个属性AsyncState,此属性返回一个对象,该对象是启动异步操作的方法的最后一个参数。那这样就很明显了,只要使用
Socket handler = (Socket) ar.AsyncState;
就可以将委托后面的对象提取出来使用。
到目前为止,我们可以这样理解异步,就是将一系列的操作预先定义好,之后就不需要重新接手,而让那程序按照预先的定义自行执行。
下面是异步客户端的代码。相比来说,异步客户端没有异步服务器端重要,因为客户端一般不需要同时接收许多连接,只要客户端与服务器端通信即可。
public class AsynchronousClient { private const int port = 11000; // 客户端多了一些线程的控制标识,为了在需要的时候控制线程 private static ManualResetEvent connectDone = new ManualResetEvent(false); private static ManualResetEvent sendDone = new ManualResetEvent(false); private static ManualResetEvent receiveDone = new ManualResetEvent(false); private static String response = String.Empty; private static void StartClient() { try { //这里还是一样 IPHostEntry ipHostInfo = Dns.Resolve("host.contoso.com"); IPAddress ipAddress = ipHostInfo.AddressList[0]; IPEndPoint remoteEP = new IPEndPoint(ipAddress, port); Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 不同的地方开始在这里,不过也是先连接,同时要定义好连接完毕后执行的操作 client.BeginConnect( remoteEP, new AsyncCallback(ConnectCallback), client); //等到连接成功后再继续执行 connectDone.WaitOne(); // 发送数据至服务器端,是先发送,与服务器端的先接收不同 Send(client,"This is a test<EOF>"); sendDone.WaitOne();//也需要等待 // 接收服务器端发送的数据 Receive(client); receiveDone.WaitOne(); Console.WriteLine("Response received : {0}", response); client.Shutdown(SocketShutdown.Both); client.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void ConnectCallback(IAsyncResult ar) { try { //这里都一样了 Socket client = (Socket) ar.AsyncState; client.EndConnect(ar); Console.WriteLine("Socket connected to {0}", client.RemoteEndPoint.ToString()); connectDone.Set(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void Receive(Socket client) { try { StateObject state = new StateObject(); state.workSocket = client; 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 { StateObject state = (StateObject) ar.AsyncState; Socket client = state.workSocket; int bytesRead = client.EndReceive(ar); if (bytesRead > 0) { state.sb.Append(Encoding.ASCII.GetString(state.buffer,0,bytesRead)); client.BeginReceive(state.buffer,0,StateObject.BufferSize,0, new AsyncCallback(ReceiveCallback), state); } else { if (state.sb.Length > 1) { response = state.sb.ToString(); } receiveDone.Set(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } } private static void Send(Socket client, String data) { byte[] byteData = Encoding.ASCII.GetBytes(data); client.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), client); } private static void SendCallback(IAsyncResult ar) { try { Socket client = (Socket) ar.AsyncState; int bytesSent = client.EndSend(ar); Console.WriteLine("Sent {0} bytes to server.", bytesSent); sendDone.Set(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } public static int Main(String[] args) { StartClient(); return 0; } }
总体来说,客户端和服务器端在原理上是一致的,只是执行的一些操作有些不同而已。
相信看到这里应该对异步套接字有了一定的了解了吧。