UN2L6——客户端主动断开连接

本章代码关键字

1
socket.Disconnect()                //主动和服务器端断开连接方法,让服务器发现客户端主动断联不能只使用该方法

当前存在的问题

目前在客户端主动退出时,我们会调用socket​的ShutDown​和Close​方法
但是通过调用这两个方法后,服务器端无法得知客户端已经主动断开

image

解决目前断开不及时的问题

客户端可以通过Disconnect​方法主动和服务器端断开连接
服务器端可以通过Conected​属性判断连接状态决定是否释放Socket

但是由于服务器端Conected​变量表示的是上一次收发消息是否成功,所以服务器端无法准确判断客户端的连接状态
因此,我们需要自定义一条退出消息,用于准确断开和客户端之间的连接

  1. 客户端尝试使用Disconnect​方法主动断开连接

    Socket​当中有一个专门在客户端使用的方法,Disconect​方法,客户端调用该方法和服务器端断开连接
    看是否是因为之前直接Close​而没有调用Disconet​造成服务器端无法及时获取状态

    主要修改的逻辑:

    • 客户端:主动断开连接

      Disconnent​有一个参数,代表接下来socket​是否还复用,一般是不再复用的

      1
      2
      3
      4
      socket.Shutdown(SocketShutdown.Both);
      socket.Disconnect(false);
      socket.Close();
      socket = null;
    • 服务端:

      1. 收发消息时判断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);
        }
      2. 处理删除记录的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}主动断开连接了");
        }
        }
        }
  2. 自定义退出消息

    根据运行结果,会发现服务器端还是无法准确判断客户端的连接状态
    这是因为服务器端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
    28
    public 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);
    }
    }