UN2L6——客户端主动断开连接
UN2L6——客户端主动断开连接
本章代码关键字
1 | socket.Disconnect() //主动和服务器端断开连接方法,让服务器发现客户端主动断联不能只使用该方法 |
当前存在的问题
目前在客户端主动退出时,我们会调用socket
的ShutDown
和Close
方法
但是通过调用这两个方法后,服务器端无法得知客户端已经主动断开
解决目前断开不及时的问题
客户端可以通过Disconnect
方法主动和服务器端断开连接
服务器端可以通过Conected
属性判断连接状态决定是否释放Socket
但是由于服务器端Conected
变量表示的是上一次收发消息是否成功,所以服务器端无法准确判断客户端的连接状态
因此,我们需要自定义一条退出消息,用于准确断开和客户端之间的连接
-
客户端尝试使用
Disconnect
方法主动断开连接
Socket
当中有一个专门在客户端使用的方法,Disconect
方法,客户端调用该方法和服务器端断开连接
看是否是因为之前直接Close
而没有调用Disconet
造成服务器端无法及时获取状态主要修改的逻辑:
-
客户端:主动断开连接
Disconnent
有一个参数,代表接下来socket
是否还复用,一般是不再复用的1
2
3
4socket.Shutdown(SocketShutdown.Both);
socket.Disconnect(false);
socket.Close();
socket = null; -
服务端:
-
收发消息时判断
socket
是否已经断开,使用Connented
判断
一旦发现是断开的就直接将其放到一个待销毁的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//服务器端 ClientSocket类内部
//向客户端发送消息方法
public void Send(BaseMessage message)
{
if (Connented)
{
try
{
socket.Send(message.Writeing());
}
catch (Exception e)
{
Console.WriteLine($"向客户端{socket.RemoteEndPoint}发送消息失败:{e.Message}");
//如果发送报错,就把这个socket断开
//Program.socket是指向唯一ServerSocket的静态变量
Program.socket.AddDelSocket(this);
}
}
//如果检测到未连接,就把这个socket断开
else
Program.socket.AddDelSocket(this);
}
public void Receive()
{
//如果检测到未连接,就把这个socket断开,并跳过发消息逻辑
if (!Connented)
{
Program.socket.AddDelSocket(this);
return;
}
try
{
if (socket.Available > 0)
{
byte[] result = new byte[1024 * 5];
int receiveNum = socket.Receive(result);
//处理接收到的消息方法,此处省略
HandleReceiveMsg(result, receiveNum);
}
}
catch (Exception e)
{
Console.WriteLine($"从客户端{clientID}接收消息失败:{e.Message}");
//如果解析错误,就把这个socket断开
Program.socket.AddDelSocket(this);
}
}1
2
3
4
5
6
7
8
9
10//服务器端 ServerSocket类内部
//待移除的客户端Socket
private List<ClientSocket> delList = new List<ClientSocket>();
//添加待移除的socket内容
public void AddDelSocket(ClientSocket socket)
{
if (!delList.Contains(socket))
delList.Add(socket);
} -
处理删除记录的
socket
的相关逻辑(会用到线程锁)每次监听消息完毕后就检测是否待销毁的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//服务器端 ServerSocket类内部
//待移除的客户端Socket
private List<ClientSocket> delList = new List<ClientSocket>();
//接收客户端消息线程
private void Receive(object? obj)
{
while (!isClose)
{
lock (clientDic)
{
if (clientDic.Count > 0)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Receive();
}
//每次接收消息后,检查是否delList内是否存在内容
CloseDelListSocket();
}
}
}
}
//将存放到delList的ClientSocket关闭并释放掉
public void CloseDelListSocket()
{
//判断有没有断开连接的,将其移除掉
for (int i = 0; i < delList.Count; i++)
CloseClientSocket(delList[i]);
delList.Clear();
}
//关闭客户端Socket连接,并将其从字典中移除出去
public void CloseClientSocket(ClientSocket socket)
{
lock (clientDic)
{
socket.Close();
if (clientDic.ContainsKey(socket.clientID))
{
clientDic.Remove(socket.clientID);
Console.WriteLine($"客户端{socket.clientID}主动断开连接了");
}
}
}
-
-
-
自定义退出消息
根据运行结果,会发现服务器端还是无法准确判断客户端的连接状态
这是因为服务器端Conected
变量表示的是上一次收发消息是否成功让服务器端收到该消息就知道是客户端想要主动断开,然后服务器端处理释放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
28public class QuitMessage : BaseMessage
{
public override int GetBytesNum()
{
//客户端主动断联消息没有额外的数据,因此只包含头消息,ID和消息体长度
return 8;
}
public override int Reading(byte[] bytes, int BeginIndex = 0)
{
//客户端主动断联消息没有额外的数据,因此消息体长度是0,因此不需要做任何操作
return 0;
}
public override byte[] Writeing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteInt(bytes, GetID(), ref index);
WriteInt(bytes, 0, ref index);
return bytes;
}
public override int GetID()
{
return 1003;
}
}然后在客户端断开连接时发送该消息,直接使用在主线程同步发送方法,避免因为程序关闭线程中断导致未能发出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//客户端 NetManager类内部
public void Close()
{
if (socket != null)
{
print("客户端主动断开连接");
//主动发送一条断开连接的消息给服务端
QuitMessage quitMessage = new QuitMessage();
//直接使用在主线程同步发送方法,避免因为程序关闭线程中断导致未能发出
socket.Send(quitMessage.Writeing());
if (socket.Connected)
{
socket.Shutdown(SocketShutdown.Both);
socket.Disconnect(false);
}
socket.Close();
socket = null;
isConnented = false;
}
}服务器端实现收到该消息后的处理方法,将该客户端Socket添加到待移除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//服务器端 ClientSocket类内部
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息后,将收到的消息直接拼接到缓存字节数组的后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
while (true)
{
//..省略其他逻辑
//解析消息体
if (cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//消息体是完整的情况
BaseMessage? baseMsg = null;
switch (msgID)
{
case 1001:
//..省略其他逻辑
break;
case 1003:
//由于该消息都没有消息体,因此我们无需反序列化它
baseMsg = new QuitMessage();
break;
}
//处理消息
if (baseMsg != null)
ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
//..省略其他逻辑
}
else
//..省略其他逻辑
}
}
private void MsgHandle(object? obj)
{
if (obj == null)
return;
BaseMessage? message = obj as BaseMessage;
if (message is PlayerMessage)
{
//..省略其他逻辑
}
else if (message is QuitMessage)
{
//收到断开连接消息,把自己添加到待移除的列表中
Program.socket.AddDelSocket(this);
}
}