2.6 多路复用Select

2.6.1 什么是多路复用

此节内容为重点知识,因为后面章节的服务端程序将全部使用Select模式。多路复用,就是同时处理多路信号,比如同时检测多个Socket的状态。

又是辛勤的人们,经过没日没夜的加班,终于灵光一闪,想到了解决Poll服务端中CPU占用率过高的方法,那就是:同时检测多个Socket的状态。在设置要监听的Socket列表后,如果有一个(或多个)Socket可读(或可写,或发生错误信息),那就返回这些可读的Socket,如果没有可读的,那就阻塞。

Select方法便是实现多路复用的关键,它的原型如下:

        public static void Select(
            IList checkRead,
            IList check Write,
            IList checkError,
            int microSeconds
        )

表2-6对Select的参数进行了说明。

表2-6 Select的参数说明

Select可以确定一个或多个Socket对象的状态,如图2-13所示。使用它时,须先将一个或多个套接字放入IList中。通过调用Select(将IList作为checkRead参数),可检查Socket是否具有可读性。若要检查套接字是否具有可写性,可使用checkWrite参数。若要检测错误条件,可使用checkError。在调用Select之后,Select将修改IList列表,仅保留那些满足条件的套接字。如图2-13所示,把包含6个Socket的列表传给Select, Select方法将会阻塞,等到超时或某个(或多个)Socket可读时返回,并且修改checkRead列表,仅保存可读的socket A和socket C。当没有任何可读Socket时,程序将会阻塞,不占用CPU资源。

图2-13 Select示意图

2.6.2 Select服务端

服务端调用Select,等待可读取的Socket,流程如下。

        初始化listenfd
        初始化clients列表
        while(true) {
            checkList = 待检测Socket列表
            Select(checkList ...)
            for(遍历可读checkList 列表){
                if(listenfd可读)  Accept;
                if(这个客户端可读)  消息处理;
            }
        }

服务端使用主循环结构while(true){…},不断地调用Select检测Socket状态,其步骤如下:

❑ 将监听Socket(listenfd)和客户端Socket(遍历clients列表)添加到待检测Socket可读状态的列表checkList中。

❑ 调用Select,程序中设置超时时间为1秒,若1秒内没有任何可读信息,Select方法将checkList列表变成空列表,然后返回。

❑ 对Select处理后的每个Socket做处理,如果监听Socket(listenfd)可读,说明有客户端连接,需调用Accept。如果客户端Socket可读,说明客户端发送了消息(或关闭),将消息广播给所有客户端。

上述过程的示例代码如下:

        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)
            {
                //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("[服务器]启动成功");
                //checkRead
                List<Socket> checkRead = new List<Socket>();
                //主循环
                while(true){
                    //填充checkRead列表
                    checkRead.Clear();
                    checkRead.Add(listenfd);
                    foreach (ClientState s in clients.Values){
                        checkRead.Add(s.socket);
                    }
                    //select
                    Socket.Select(checkRead, null, null, 1000);
                    //检查可读对象
                    foreach (Socket s in checkRead){
                        if(s == listenfd){
                            ReadListenfd(s);
                        }
                        else{
                            ReadClientfd(s);
                        }
                    }
                }
            }
        }

其中ReadListenfd和ReadClientfd与2.5.3节的实现相同,这里不再重复。

2.6.3 Select客户端

使用Select方法的客户端和使用Poll方法的客户端极其相似,因为只需检测一个Socket的状态,将连接服务端的socket输入到checkRead列表即可。为了不卡住客户端,Select的超时时间设置为0,永不阻塞。示例代码如下:

            public void Update(){
                if(socket == null) {
                    return;
                }
                //填充checkRead列表
                checkRead.Clear();
                checkRead.Add(socket);
                //select
                Socket.Select(checkRead, null, null, 0);
                //check
                foreach (Socket s in checkRead){
                    byte[] readBuff = new byte[1024];
                    int count = socket.Receive(readBuff);
                    string recvStr =
                        System.Text.Encoding.Default.GetString(readBuff, 0, count);
                    text.text = recvStr;
                }
            }

        }

由于程序在Update中不停地检测数据,性能较差。商业上为了做到性能上的极致,大多使用异步(或使用多线程模拟异步程序)。本书将会使用异步客户端、Select服务端演示程序。

如果读者想要了解更多异步服务端的知识,欢迎阅读本书的第一版,第一版内容全程使用了异步服务端程序。

实践出真知,尽管还有一些“坑”没有处理,但最基本的知识都掌握了。先动手做一款简单的网络游戏吧!