本篇文章教你在Echo中应用状态监测Poll和多路复用Select。
除了异步程序,还有什么方法可以改善Echo的客户端和服务端呢?
本系列博文参考自《Unity3D网络游戏实战》一书。如有错误,欢迎评论区指正。
状态检测Poll 可以用同步方法来优化客户端和服务端程序吗?
什么是Poll Microsoft为Socket类提供了Poll实例方法,它的原型:
1 2 3 4 public bool Poll ( int microSeconds, SelectMode mode )
参数说明:
参数
说明
microSeconds
等待回应的时间,以微秒为单位,如果该参数为-1,表示一直等待,如果为0,表示非阻塞
mode
有3种可选的模式,如下: SelectRead:如果Socket可读(可以接受数据),返回true,否则返回false; SelectWrite:如果Socket可写,返回true,否则返回false; SelectError:如果连接失败,返回true,否则返回false
Poll的思想就是,循环中同步检测Socket的状态(可读、可写、错误),进而进行相应的逻辑。
Poll客户端 将Echo的同步客户端程序EchoSync.cs稍作更改,将Send方法中Receive相关代码删除,在Update方法中加入非阻塞的Poll方法检测Socket的可读状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Net.Sockets;using UnityEngine.UI;public class EchoPoll : MonoBehaviour { private Socket socket; public InputField input; public Text text; public void Connection () { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("127.0.0.1" , 8888 ); } public void Send () { string sendStr = input.text; byte [] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr); socket.Send(sendBytes); } private void Update () { if (socket == null ) return ; if (socket.Poll(0 , SelectMode.SelectRead)) { byte [] readBuffer = new byte [1024 ]; int count = socket.Receive(readBuffer); string receiveStr = System.Text.Encoding.UTF8.GetString(readBuffer, 0 , count); text.text = receiveStr; } } }
Poll服务端 将Echo的异步服务端程序稍作修改,将异步方法和回调函数删除,循环中使用Poll检测Socket的可读状态,进而完成相应逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Net;using System.Net.Sockets;namespace Echo_Server_Poll { class ClientState { public Socket socket; public byte [] readBuffer = new byte [1024 ]; } class Program { static Socket listenfd; static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); static void Main (string [] args ) { Console.WriteLine("Hello, World!" ); listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAddr = IPAddress.Parse("127.0.0.1" ); IPEndPoint ipEp = new IPEndPoint(ipAddr, 8888 ); listenfd.Bind(ipEp); listenfd.Listen(0 ); Console.WriteLine("[服务器]启动成功" ); while (true ) { if (listenfd.Poll(0 , SelectMode.SelectRead)) { ReadListenfd(listenfd); } foreach (ClientState s in clients.Values) { Socket clientfd = s.socket; if (clientfd.Poll(0 , SelectMode.SelectRead)) { if (!ReadClientfd(clientfd)) { break ; } } } System.Threading.Thread.Sleep(1 ); } } public static void ReadListenfd (Socket listenfd ) { Console.WriteLine("[服务器]Accept" ); Socket clientfd = listenfd.Accept(); ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); } public static void ReadClientfd (Socket clientfd ) { ClientState state = clients[clientfd]; int count = 0 ; try { count = clientfd.Receive(state.readBuffer); } catch (SocketException ex) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket接收失败 " + ex.ToString()); return false ; } if (count == 0 ) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket关闭" ); return false ; } string recvStr = System.Text.Encoding.UTF8.GetString(state.readBuffer, 0 , count); Console.WriteLine("[服务器接收]" + recvStr); byte [] sendBytes = System.Text.Encoding.UTF8.GetBytes("Echo " + recvStr); Console.WriteLine("[服务器发送]" + "Echo " + recvStr); foreach (ClientState cs in clients.Values) { cs.socket.Send(sendBytes); } return true ; } } }
Poll的逻辑很清晰,但是一直处于循环检测的状态,依旧耗费CPU资源,还有什么可以更加优化性能的方法呢?
多路复用Select 有没有可以同时对Socket的可读、可写、错误状态进行检测的方法呢?
什么是多路复用 多路复用,就是同时处理多路信号,比如同时检测多个Socket的状态。
微软为此提供了Socket类的Select静态方法,它的原型:
1 2 3 4 5 6 public static void Select ( IList checkRead, IList checkWrite, IList checkError, int microSeconds )
参数说明:
参数
说明
checkRead
检测是否有可读的Socket列表
checkWrite
检测是否有可写的Socket列表
checkError
检测是否有出错的Socket列表
microSeconds
等待回应的时间,以微秒为单位,如果该参数为-1表示一直等待,为0表示非阻塞
Select可以确定一个或多个Socket的状态,调用后,会修改传入的IList列表,仅保留满足条件的套接字。当没有任何满足要求的Socket时,Select将程序阻塞,不占用CPU。
Select服务端 对Poll方法的Echo服务端中的while循环稍作修改,其他不变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Net;using System.Net.Sockets;namespace Echo_Server_Select { class ClientState { public Socket socket; public byte [] readBuffer = new byte [1024 ]; } class Program { static Socket listenfd; static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>(); static void Main (string [] args ) { Console.WriteLine("Hello, World!" ); listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ipAddr = IPAddress.Parse("127.0.0.1" ); IPEndPoint ipEp = new IPEndPoint(ipAddr, 8888 ); listenfd.Bind(ipEp); listenfd.Listen(0 ); Console.WriteLine("[服务器]启动成功" ); List<Socket> checkRead = new List<Socket>(); while (true ) { checkRead.Clear(); checkRead.Add(listenfd); foreach (ClientState s in clients.Values) { checkRead.Add(s.socket); } Socket.Select(checkRead, null , null , 1000 ); foreach (Socket s in checkRead) { if (s == listenfd) { ReadListenfd(s); } else { ReadClientfd(s); } } } } public static void ReadListenfd (Socket listenfd ) { Console.WriteLine("[服务器]Accept" ); Socket clientfd = listenfd.Accept(); ClientState state = new ClientState(); state.socket = clientfd; clients.Add(clientfd, state); } public static bool ReadClientfd (Socket clientfd ) { ClientState state = clients[clientfd]; int count = 0 ; try { count = clientfd.Receive(state.readBuffer); } catch (SocketException ex) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket接收失败 " + ex.ToString()); return false ; } if (count == 0 ) { clientfd.Close(); clients.Remove(clientfd); Console.WriteLine("Socket关闭" ); return false ; } string recvStr = System.Text.Encoding.UTF8.GetString(state.readBuffer, 0 , count); Console.WriteLine("[服务器接收]" + recvStr); byte [] sendBytes = System.Text.Encoding.UTF8.GetBytes("Echo " + recvStr); Console.WriteLine("[服务器发送]" + "Echo " + recvStr); foreach (ClientState cs in clients.Values) { cs.socket.Send(sendBytes); } return true ; } } }
Select客户端 对Poll方法实现的客户端稍作修改,加入checkRead列表对象,修改Update逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 using System.Collections;using System.Collections.Generic;using UnityEngine;using System.Net.Sockets;using UnityEngine.UI;public class EchoSelect : MonoBehaviour { private Socket socket; public InputField input; public Text text; public List<Socket> checkRead = new List<Socket>(); public void Connection () { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("127.0.0.1" , 8888 ); } public void Send () { string sendStr = input.text; byte [] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr); socket.Send(sendBytes); } private void Update () { if (socket == null ) return ; checkRead.Clear(); checkRead.Add(socket); Socket.Select(checkRead, null , null , 0 ); foreach (Socket s in checkRead) { byte [] readBuffer = new byte [1024 ]; int count = socket.Receive(readBuffer); string receiveStr = System.Text.Encoding.UTF8.GetString(readBuffer, 0 , count); text.text = receiveStr; } } }
Select客户端的性能还是较差,因为Update不停检测。所以商业上一般采用异步客户端。
总结 Poll和Select写起来都比异步要简单,Select的性能优于Poll。
对于服务端而言,Select是一种不错的实现方式;对于客户端而言,最好还是使用异步方式。