TCP心跳消息

客户端主动断开连接

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson10 : MonoBehaviour
{
    void Start()
    {
        #region 知识点一 目前的客户端主动断开连接
        //目前在客户端主动退出时
        //我们会调用socket的 ShutDown和Close方法
        //但是通过调用这两个方法后 服务器端无法得知客户端已经主动断开
        //举例说明
        #endregion
        #region 知识点二 解决目前断开不及时的问题
        //1.客户端尝试使用Disconnect方法主动断开连接
        //Socket当中有一个专门在客户端使用的方法
        //Disconect方法
        //客户端调用该方法和服务器端断开连接
        //看是否是因为之前直接Close而没有调用Disconet造成服务器端无法及时获取状态
        //主要修改的逻辑:
        //客户端:
        //主动断开连接
        //服务端:
        //1.收发消息时判断socket是否已经断开
        //2.处理删除记录的socket的相关逻辑(会用到线程锁)
        //2.自定义退出消息 
        //让服务器端收到该消息就知道是客户端想要主动断开
        //然后服务器端处理释放socket相关工作
        #endregion
        #region 总结 
        //客户端可以通过Disconnect方法主动和服务器端断开连接
        //服务器端可以通过Conected属性判断连接状态决定是否释放Socket 
        //但是由于服务器端Conected变量表示的是上一次收发消息是否成功
        //所以服务器端无法准确判断客户端的连接状态
        //因此 我们需要自定义一条退出消息 用于准确断开和客户端之间的连接
        #endregion 
    }

    void Update()
    {
        
    }
}

服务器中封装三个方法

cs 复制代码
       //添加待移除的socket
        public void AddDelSocket(ClientSocket socket)
        {
            if(!delList .Contains (socket))
            {
                delList.Add(socket);
            }
        }
        //判断有没有断开连接的 将他移除
        public void CloseDelListSocket()
        {
            
            for (int i = 0; i < delList.Count; i++)
                CloseClientSocket(delList[i]);
            delList.Clear();
        }
        //关闭客户端的连接,从字典中移除
        public void CloseClientSocket(ClientSocket socket)
        {
            lock (clientDic)
            {
                this.socket.Close();
                if (clientDic.ContainsKey(socket.clientID))
                    clientDic.Remove(socket.clientID);
            }
            
        }

自定义消息类

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class quitMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override int GetID()
    {
        return 1003;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return base.Reading(bytes, beginIndex);
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }
}

实现心跳消息

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson11 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        #region 知识点一 什么是心跳消息?
        //所谓心跳消息,就是在长连接中,客户端和服务端之间定期发送的一种特殊的数据包 
        //用于通知对方自己还在线,以确保长连接的有效性
        //由于其发送的时间间隔往往是固定的持续的,就像是心跳一样一直存在
        //所以我们称之为心跳消息
        #endregion  
        #region 知识点二 为什么需要心跳消息?
        //1.避免非正常关闭客户端时,服务器无法正常收到关闭连接消息   
        //通过心跳消息我们可以自定义超时判断,如果超时没有收到客户端消息,证明客户端已经断开连接  
        //2.避免客户端长期不发送消息,防火墙或者路由器会断开连接,我们可以通过心跳消息一直保持活跃状态
        #endregion     
        #region 知识点三 实现心跳消息 
        //客户端  
        //主要功能:定时发送消息    
        //服务器 
        //主要功能:不停检测上次收到某客户端消息的时间,如果超时则认为连接已经断开
        #endregion 
        #region 总结     
        //心跳消息是长连接项目中必备的一套逻辑规则   
        //通过它可以帮助我们在服务器端及时的释放掉失效的socket    
        //可以有效避免当客户端非正常关闭时,服务器端不能及时判断连接已断开
        #endregion
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

自定义心跳消息类

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HeartMsg : BaseMsg
{
    public override int GetBytesNum()
    {
        return 8;
    }

    public override int GetID()
    {
        return 999;
    }

    public override int Reading(byte[] bytes, int beginIndex = 0)
    {
        return 0;
    }

    public override byte[] Writing()
    {
        int index = 0;
        byte[] bytes = new byte[GetBytesNum()];
        WriteInt(bytes, GetID(), ref index);
        WriteInt(bytes, 0, ref index);
        return bytes;
    }
}

修改服务器逻辑

cs 复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TeachServerTcpExe2
{
    class ClientSocket
    {
        private static int CLIENT_BEGIN_ID = 1;
        public int clientID;
        //是否是连接状态
        public bool Connect => this.socket.Connected;
        public Socket socket;
        //用于处理分包时缓存的字节数组和字节数组的长度
        private byte[] cacheBytes = new byte[1024 * 1024];
        private int cacheNum = 0;
        private long frontTime = -1;//上一次收到消息的时间
        private static int TIME_OUT_TIME = 10;//设置超时时间
        public ClientSocket (Socket socket)
        {
            this.clientID = CLIENT_BEGIN_ID;
            this.socket = socket;
            ++CLIENT_BEGIN_ID;
            //为了方便理解,所以开了一个线程专门计时,但是这种方式比较消耗性能,不建议这样使用
            //ThreadPool.QueueUserWorkItem(CheakTimeOut);
        }
        //间隔一段时间检测一次超时 如果超时就会主动断开客户端的连接
        public void CheakTimeOut(/*object obj*/)
        {
            //while (Connect)
            //{
                if (frontTime == -1 && DateTime.Now.Ticks / TimeSpan.TicksPerSecond >= TIME_OUT_TIME)
                {
                    Program.socket.AddDelSocket(this);
                    //break;
                }
                //Thread.Sleep(5000);
            //}
           
        }
        //我们应该封装一些方法
        //关闭
        public void Close()
        {
            if(socket!=null)
            {
                socket.Shutdown(SocketShutdown.Both);
                socket.Close();
                socket = null;
            }
        }
        //发送
        public void Send(BaseMsg info)
        {
            if (Connect)
            {
                try
                {
                    socket.Send(info.Writing());
                }
                catch (Exception e)
                {

                    Console.WriteLine("发消息失败:" + e.Message);
                    Program.socket.AddDelSocket(this);
                }
            }
            else
                Program.socket.AddDelSocket(this);
        }
        //接收
        public void Receive()
        {
            if (!Connect)
            {
                Program.socket.AddDelSocket(this);
                return;
            }
               
            try
             {
                 if(socket .Available >0)
                  {
                    byte[] result = new byte[1024 * 5];
                    int receiveNum = socket.Receive(result);
                    HandleReceiveMsg(result, receiveNum);
                    收到数据后先解析字节数组前四个字节
                    //int msgID = BitConverter.ToInt32(result, 0);
                    //BaseMsg msg = null;
                    //switch (msgID)
                    //{
                    //    case 1001:
                    //        msg = new PlayerMsg();
                    //        msg.Reading(result, 4);
                    //        break;
                    //}
                    //if (msg == null) return;
                    //ThreadPool.QueueUserWorkItem(MsgHandle, msg);
                  }
                //检测是否超时
                CheakTimeOut();
              }
            catch (Exception e)
             {
                Console.WriteLine("收消息失败:"+e.Message);
                //解析消息出错,也认为把socket断开了
                Program.socket.AddDelSocket(this);
             }
        }
        private void MsgHandle(object obj)
        {
            BaseMsg msg = obj as BaseMsg;
            if (msg is PlayerMsg)
            {
                PlayerMsg playerMsg = msg as PlayerMsg;
                Console.WriteLine(playerMsg.playerID);
                Console.WriteLine(playerMsg.playerData .name);
                Console.WriteLine(playerMsg.playerData .lev);
                Console.WriteLine(playerMsg.playerData .atk);
            }
            else if(msg is quitMsg)
            {
                //收到断开连接消息,将自己添加到待移除的列表
                Program.socket.AddDelSocket(this);
            }
            else if(msg is HeartMsg)
            {
                //收到心跳消息,记录收到心跳消息的时间
                frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
                Console.WriteLine("收到心跳消息");
            }
        }
        //处理分包、黏包问题的方法
        private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
        {
            int msgID = 0;
            int msgLength = 0;
            int nowIndex = 0;
            //收到消息时,应该看看之前有没有缓存的如果有的话 直接拼在后边
            receiveBytes.CopyTo(cacheBytes, cacheNum);
            cacheNum += receiveNum;
            //while循环解决黏包问题
            while (true)
            {
                //每次将长度设置为-1,是为了避免上一次解析的数据 影响这一次的判断
                msgLength = -1;
                if (cacheNum - nowIndex >= 8)
                {
                    //解析ID
                    msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                    //解析消息长度
                    msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
                    nowIndex += 4;
                }
                if (cacheNum - nowIndex >= msgLength && msgLength != -1)
                {
                    //解析消息体
                    BaseMsg baseMsg = null;
                    switch (msgID)
                    {
                        case 1001:
                            baseMsg = new PlayerMsg();
                            baseMsg .Reading(receiveBytes, nowIndex);
                            break;
                        case 1003:
                            baseMsg = new quitMsg();
                            //由于该消息没有消息体,不用进行反序列化 
                            break;
                        case 999:
                            baseMsg = new HeartMsg();
                            break;
                    }
                    if (baseMsg != null)
                        ThreadPool.QueueUserWorkItem(MsgHandle,baseMsg);
                    nowIndex += msgLength;
                    if (nowIndex == cacheNum)
                    {
                        break;
                    }
                }
                else
                {
                    //如果不满足,证明有分包
                    //那么我们需要把当前收到的消息存下来
                    //有待下次接收到消息后再做处理
                    //receiveBytes.CopyTo(cacheBytes, 0);
                    //cacheNum = receiveNum;
                    //如果进行了id和长度的解析,但是没有成功解析消息体,nowindex需要减去移动的位置
                    if (msgLength != -1)
                        nowIndex -= 8;
                    //就是把剩余没有解析的字节数组的内容 移到前面来 用于缓存下次继续解析
                    Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
                    cacheNum = cacheNum - nowIndex;
                    break;
                }
            }

        }
    }
}
相关推荐
混血哲谈1 小时前
如何使用webpack预加载 CSS 中定义的资源和预加载 CSS 文件
前端·css·webpack
浪遏3 小时前
我的远程实习(二) | git 持续更新版
前端
cllsse3 小时前
网络安全设备配置与管理-实验4-防火墙AAA服务配置
网络·网络安全
智商不在服务器3 小时前
XSS 绕过分析:一次循环与两次循环的区别
前端·xss
MonkeyKing_sunyuhua3 小时前
npm WARN EBADENGINE required: { node: ‘>=14‘ }
前端·npm·node.js
Hi-Jimmy4 小时前
【VolView】纯前端实现CT三维重建-CBCT
前端·架构·volview·cbct
janthinasnail4 小时前
编写一个简单的chrome截图扩展
前端·chrome
拉不动的猪4 小时前
刷刷题40(vue中计算属性不能异步,如何实现异步)
前端·javascript·vue.js
冴羽yayujs5 小时前
SvelteKit 最新中文文档教程(6)—— 状态管理
前端·javascript·vue.js·前端框架·react·svelte·sveltekit