2.3 异步服务端
第1章的同步服务端程序同一时间只能处理一个客户端的请求,因为它会一直阻塞,等待某一个客户端的数据,无暇接应其他客户端。使用异步方法,可以让服务端同时处理多个客户端的数据,及时响应。
2.3.1 管理客户端
想象一下在聊天室里,某个用户说了一句话后,服务端需要把这句话发送给每一个人。所以服务端需要有个列表,保存所有连接上来的客户端信息。可以定义一个名为ClientState的类,用于保存一个客户端信息。ClientState包含TCP连接所需Socket,以及用于填充BeginReceive参数的读缓冲区readBuff。
class ClientState { public Socket socket; public byte[] readBuff = new byte[1024]; }
C#提供了List和Dictionary等容器类数据结构(System.Collections.Generic命名空间内),其中Dictionary(字典)是一个集合,每个元素都是一个键值对,它是常用于查找和排序的列表。可以通过Add方法给Dictionary添加元素,并通过ContainsKey方法判断Dictionary里面是否包含某个元素。这里假设读者对这些数据结构稍有了解,如果不是很了解,可以先搜索相关的资料。可以在服务端中定义一个Dictionary<Socket, ClientState>类型的Dictionary,以Socket作为Key,以ClientState作为Value。命令如下:
static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
clients的结构如图2-8所示,通过clientState = clients[socket]能够很方便地获取客户端的信息。
图2-8 clients列表示意图
2.3.2 异步Accept
除了BeginSend、BeginReceive等方法外,异步服务端还会用到异步Accept方法BeginAccept和EndAccept。BeginAccept的函数原型如下。
public IAsyncResult BeginAccept( AsyncCallback callback, object state )
表2-4对BeginAccept的参数进行了说明。
表2-4 BeginAccept参数说明
调用BeginAccecpt后,程序继续执行而不是阻塞在该语句上。等到客户端连接上来,回调函数AsyncCallback将被执行。在回调函数中,开发者可以使用EndAccept获取新客户端的套接字(Socket),还可以获取state参数传入的数据。其中EndAccept的原型如下,它会返回一个客户端Socket。
public Socket EndAccept( IAsyncResult asyncResult )
2.3.3 程序结构
图2-9展示了异步服务端的程序结构,服务器经历Socket、Bind、Listen三个步骤初始化监听Socket,然后调用BeginAccept开始异步处理客户端连接。如果有客户端连接进来,异步Accept的回调函数AcceptCallback被调用,会让客户端开始接收数据,然后继续调用BeginAccept等待下一个客户端的连接。
图2-9 异步服务端的程序结构
2.3.4 代码展示
“读万卷书不如行万里路”,直接来看看代码吧!服务端程序的主体结构中,定义客户端状态类ClientState,客户端管理列表clients。除了调用BeginAccept外,其大体与同步服务端相似。具体代码如下。
using System; using System.Net; using System.Net.Sockets; using System.Collections.Generic; class ClientState { public Socket socket; public byte[] readBuff = new byte[1024]; } class MainClass { //监听Socket static Socket listenfd; //客户端Socket及状态信息 static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); public static void Main (string[] args) { Console.WriteLine ("Hello World! "); //Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //Bind IPAddress ipAdr = IPAddress.Parse("127.0.0.1"); IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888); listenfd.Bind(ipEp); //Listen listenfd.Listen(0); Console.WriteLine("[服务器]启动成功"); //Accept listenfd.BeginAccept (AcceptCallback, listenfd); //等待 Console.ReadLine(); } }
AcceptCallback是BeginAccept的回调函数,它处理了三件事情:
1)给新的连接分配ClientState,并把它添加到clients列表中;
2)异步接收客户端数据;
3)再次调用BeginAccept实现循环。
注意BeginReceive的最后一个参数,这里以ClientState代替了原来的Socket。
//Accept回调 public static void AcceptCallback(IAsyncResult ar){ try { Console.WriteLine ("[服务器]Accept"); Socket listenfd = (Socket) ar.AsyncState; Socket clientfd = listenfd.EndAccept(ar); //clients列表 ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); //接收数据BeginReceive clientfd.BeginReceive(state.readBuff, 0, 1024, 0, ReceiveCallback, state); //继续Accept listenfd.BeginAccept (AcceptCallback, listenfd); } catch (SocketException ex){ Console.WriteLine("Socket Accept fail" + ex.ToString()); } }
ReceiveCallback是BeginReceive的回调函数,它也处理了三件事情:
1)服务端收到消息后,回应客户端;
2)如果收到客户端关闭连接的信号“if(count == 0)”,断开连接;
3)继续调用BeginReceive接收下一个数据。
//Receive回调 public static void ReceiveCallback(IAsyncResult ar){ try { ClientState state = (ClientState) ar.AsyncState; Socket clientfd = state.socket; int count = clientfd.EndReceive(ar); //客户端关闭 if(count == 0){ clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket Close"); return; } string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0, count); byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo" + recvStr); clientfd.Send(sendBytes); //减少代码量,不用异步 clientfd.BeginReceive( state.readBuff, 0, 1024, 0, ReceiveCallback, state); } catch (SocketException ex){ Console.WriteLine("Socket Receive fail" + ex.ToString()); } }
更多知识点
收到0字节
当Receive返回值小于等于0时,表示Socket连接断开,可以关闭Socket。但也有一种特例,上述程序没有处理,后面章节再做介绍。
开始测试程序吧!导出exe文件(如图2-10所示),运行多个客户端,便可以愉快地聊天了。读者可以试着完善这个聊天工具,做一款QQ软件。
图2-10 导出exe文件
程序运行结果如图2-11所示。
图2-11 Echo程序运行结果