想要使用多路复用Select,就必须让服务器连接的Socket都交给Select处理。

服务器获取客户端Socket

首先,在Net文件夹新建一个ClientSocket类:

1
2
3
4
5
6
7
8
9
10
using System.Net.Sockets;

namespace SimpleServer.Net
{
public class ClientSocket
{
public Socket? Socket { get; set; }
public long LastPingTime { get; set; }
}
}

然后,在ServerSocket声明一个字典,用来缓存所有连接到服务器的客户端Socket

1
2
3
4
//所有客户端的字典
public static Dictionary<Socket,ClientSocket> m_ClientDic = new Dictionary<Socket,ClientSocket>();
//临时保存所有Select过后可读Socket集合
private static List<Socket> m_CheckReadList = new List<Socket>();

最后,修改ServerSocket,添加服务器获取客户端的逻辑

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
        public void Init()
{
//...

if (endPoint != null )
Debug.LogInfo("服务器启动监听{0}成功",endPoint);

while (true)
{
//检查是否有读取的Socket

//处理找出所有socket
ResetCheckRead();

try
{
//最后一个参数等待时间为微秒
Socket.Select(m_CheckReadList, null, null, 1000);
}
catch (Exception e)
{

Debug.LogError(e);
}
for (global::System.Int32 i = (m_CheckReadList.Count) - (1); i >= 0; i--)
{
Socket s = m_CheckReadList[i];
if (s == m_ListenSocket)
{
//当服务器Socket在可读列表内,说明此时有客户端连接进来了
ReadListen(s);
}
else
{
//剩下的可读Socket表示给服务器发过消息来的客户端
ReadClient(s);
}
}
//检测心跳包是否超时

}
}
/// <summary>
/// 还原加检查可以读取的Socket
/// </summary>
public void ResetCheckRead()
{
m_CheckReadList.Clear();
if(m_ListenSocket != null)
m_CheckReadList.Add(m_ListenSocket);//服务器本身的Socket也要加进去
foreach (Socket socket in m_ClientDic.Keys)
{
m_CheckReadList.Add(socket);
}
}
/// <summary>
/// 获取正在连接的客户端
/// </summary>
/// <param name="listenSocket"></param>
void ReadListen(Socket listenSocket)
{
try
{
Socket client = listenSocket.Accept();
ClientSocket clientSocket = new ClientSocket();
clientSocket.Socket = client;
clientSocket.LastPingTime = GetTimeStamp();
m_ClientDic.Add(client, clientSocket);
EndPoint? clientEP = client.LocalEndPoint;
if(clientEP != null)
Debug.Log("一个客户端连接:{0},当前{1}个客户端在线!",clientEP,m_ClientDic.Count);
}
catch (SocketException se)
{
Debug.LogError("Accept fail: " + se);
}
}
void ReadClient(Socket client)
{
ClientSocket clientSocket = m_ClientDic[client];
//接收客户端信息,根据信息解析协议,根据协议内容处理消息再下发
}
public void CloseClient(ClientSocket client)
{
client.Socket?.Close();
if(client.Socket != null)
m_ClientDic.Remove(client.Socket);
Debug.Log("一个客户端断开连接,当前{0}个客户端在线!",m_ClientDic.Count);
}
/// <summary>
/// 获取时间戳
/// </summary>
/// <returns></returns>
public static long GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds);
}

当进入while循环时:首先会调用ResetCheckRead,它会清空m_CheckReadList并将其重置。

然后再调用Socket.Select(m_CheckReadList, null, null, 1000);这个关键函数,它将处理重置后的m_CheckReadList,从其中找出

进入“Readable”状态的Socket。

接下来我们倒序遍历处理后的m_CheckReadList,如果里面的“Readable”Socket是服务器的Socket,就说明此时有客户端连接进来了,那么我们就调用ReadListen方法,将此时和服务器连接的客户端缓存进m_ClientDic。如果里面的“Readable”Socket是客户端的Socket,我们就需要先检查一下这个客户端Socket是否已经缓存进了m_ClientDic里面,如果在里面,就处理客户端的数据。

我们每次while循环只能缓存一个客户端Socket,在一开始循环时,没有客户端缓存过来,并且一开始时,大部分客户端也并不是“Readable”状态(客户端并不会马上向服务器发送消息),只有服务器的Socket是“Readable”状态(因为服务器要回应大量客户端的连接请求),这样客户端的Socket就会被逐渐缓存起来了。

心跳包计算

ServerSocket中添加心跳包间隔时间,还有已经断开连接的客户端缓存列表(用来从客户端缓存字典中移除)

1
2
3
public static long m_PingInterval = 30;
//已经断连的客户端临时列表
public static List<ClientSocket> m_TempList = new List<ClientSocket>();

在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
public void Init()
{
//...

while (true)
{
//...
//检测心跳包是否超时
long timeNow = GetTimeStamp();

m_TempList.Clear();
foreach(ClientSocket clientSocket in m_ClientDic.Values)
{
if (timeNow - clientSocket.LastPingTime > m_PingInterval * 4)//客户端四次没有发送心跳数据
{
Debug.Log("Ping Close" + clientSocket.Socket?.RemoteEndPoint?.ToString());
m_TempList.Add(clientSocket);
}
}
foreach (var client in m_TempList)
{
CloseClient(client);
}
m_TempList.Clear();
}
}