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程序运行结果