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服务端演示程序。
如果读者想要了解更多异步服务端的知识,欢迎阅读本书的第一版,第一版内容全程使用了异步服务端程序。
实践出真知,尽管还有一些“坑”没有处理,但最基本的知识都掌握了。先动手做一款简单的网络游戏吧!