国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制保活(心跳包)、斷線重連

這篇具有很好參考價值的文章主要介紹了Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制?;?心跳包)、斷線重連。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

8. 粘包Bug、心跳機制?;?心跳包)、斷線重連

粘包

bug1:下線后,如果發(fā)送多條消息,在客戶端上線時,一瞬間接收到,效果如同粘包,需要拆包。舉例,連續(xù)發(fā)送三條160長度消息,可能實際顯示2條消息,原因,第三條消息和第二條消息粘包,第二條消息長度變?yōu)?20,但是Receive方法沒有考慮這個問題,相當于這段代碼只運行了兩次,只接收了兩次消息

int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
if (length > 0)
{
    Debug.Log($"接收到的數(shù)據(jù)長度:{length}");
    MessageHelper.Instance.CopyToData(buff, length);//接收到處理CopyToData給MessageHelper處理信息  
}

需要在CopyToData方法中的Handle處理一下粘包。

private void Handle()
{
    //包體大小(4) 協(xié)議ID(4) 包體(byte[])
    if (msgLength >= 8)
    {
        byte[] _size = new byte[4];
        Array.Copy(data, 0, _size, 0, 4);//把包體大小從第0位緩存4位長度
        int size = BitConverter.ToInt32(_size, 0);//獲得包體大小

        //本次要拿的長度
        var _length = 8 + size;//實際完整消息的長度:包體大小(4)+協(xié)議ID(4)+包體(byte[])

        while (msgLength>=_length)//判斷數(shù)據(jù)緩沖區(qū)的長度是否大于一條完整消息的長度。
        {
            //拿出id
            byte[] _id = new byte[4];
            Array.Copy(data, 4, _id, 0, 4);//把協(xié)議ID從第4位緩存4位長度
            int id = BitConverter.ToInt32(_id, 0);//獲得協(xié)議ID

            //包體
            byte[] body = new byte[size];
            Array.Copy(data, 8, body, 0, size);//把包體從第8位緩存size位長度

            if (msgLength>_length)//如果接收到的數(shù)據(jù)長度大于這次取出的完整一條數(shù)據(jù)的長度,說明還有數(shù)據(jù)
            {
                for (int i = 0; i < msgLength - _length; i++)
                {
                    data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                }
            }
            msgLength -= _length;//減掉已經(jīng)取完的消息長度
            Debug.Log($"收到服務器響應:{id}");
            Debug.Log($"接收到的數(shù)據(jù)內容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            //根據(jù)id進行處理,,實際項目一般使用觀察者模式,監(jiān)聽id和Action事件綁定
            switch (id)
            {
                case 1001://注冊請求
                    RigisterMsgHandle(body);
                    break;
                case 1002://登錄業(yè)務
                    LoginMsgHandle(body);
                    break;
                case 1003://聊天業(yè)務
                    ChatMsgHandle(body);
                    break;
                case 1004://添加好友
                    AddFriendHandle(body);
                    break;
                case 1005://朋友上線下線
                    FriendOnOfflineHandle(body);
                    break;
            }
        }
    }
}

心跳機制,通過心跳包維持連接

TCP通信會自動斷開。造成這種情況的原因是保持連接的通道如果長時間不通信就會被路由關閉連接 。

  1. 長連接短連接概念

短連接:僅進行一次通信即關閉連接

長連接:每次通信完畢后不關閉連接

  1. 連接的?;?/p>

當雙方已經(jīng)建立了連接,但因為網(wǎng)絡問題,鏈路不通,這樣長連接就不能使用了。因此,需要使用一些機制對長連接進行保活

  1. 應用層心跳

客戶端會開啟一個定時任務,定時對已經(jīng)建立連接的對端應用發(fā)送請求(這里的請求是特殊的心跳請求),服務端則需要特殊處理該請求,返回響應。如果心跳持續(xù)多次沒有收到響應,客戶端會認為連接不可用,主動斷開連接。

使用服務器向客戶端發(fā)送心跳包,服務器每一個客戶端連接后根據(jù)前文都有一個Client保存,在Client構造函數(shù)中只有保存客戶端的tcpClient和Receive,需要加上PingPong心跳,維持客戶端連接。

public Client(TcpClient tcpClient)
{
    client = tcpClient;
    Receive();
    PingPong();
}
//Handle的Switch(id)最后一個PingMsg處理收到的客戶端Pong消息
private void Handle()
{
    //包體大小(4) 協(xié)議ID(4) 包體(byte[])
    if (msgLength >= 8)
    {
        byte[] _size = new byte[4];
        Array.Copy(data, 0, _size, 0, 4);//把包體大小從第0位緩存4位長度
        int size = BitConverter.ToInt32(_size, 0);//獲得包體大小
    
        //本次要拿的長度
        var _length = 8 + size;//實際完整消息的長度:包體大小(4)+協(xié)議ID(4)+包體(byte[])
        if (msgLength>=_length)//判斷數(shù)據(jù)緩沖區(qū)的長度是否大于一條完整消息的長度。
        {
            //拿出id
            byte[] _id = new byte[4];
            Array.Copy(data, 4, _id, 0, 4);//把協(xié)議ID從第4位緩存4位長度
            int id = BitConverter.ToInt32(_id, 0);//獲得協(xié)議ID
    
            //包體
            byte[] body = new byte[size];
            Array.Copy(data, 8, body, 0, size);//把包體從第8位緩存size位長度
    
            if (msgLength>_length)//如果接收到的數(shù)據(jù)長度大于這次取出的完整一條數(shù)據(jù)的長度,說明還有數(shù)據(jù)
            {
                for (int i = 0; i < msgLength - _length; i++)
                {
                    data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                }
            }
            msgLength -= _length;//減掉已經(jīng)取完的消息長度
            if (id != (int)MsgID.PingMsg)
            {
                Console.WriteLine($"{DateTime.Now} | Message | 從{client.Client.RemoteEndPoint} | 接收的消息類型:{id} | 接收的消息內容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            }
            else
            {
                Console.WriteLine($"{DateTime.Now} | Pong    | 從{client.Client.RemoteEndPoint} | 接收的消息內容:Pong");
            }
            //根據(jù)id進行處理,,實際項目一般使用觀察者模式,監(jiān)聽id和Action事件綁定
            switch (id)
            {
                case (int)MsgID.RegisterMsg://注冊請求
                    RegisterMsgHandle(body);
                    break;
                case (int)MsgID.LoginMsg://登錄業(yè)務
                    LoginMsgHandle(body);
                    break;
                case (int)MsgID.ChatMsg://聊天業(yè)務
                    ChatMsgHandle(body);
                    break;
                case (int)MsgID.AddFriend://添加好友
                    AddFriendHandle(body);
                    break;
                case (int)MsgID.OnOffline://賬號下線
                    OnOfflineHandle(body);
                    break;
                case (int)MsgID.PingMsg:
                    PingPongHandle(body);
                    break;
            }
        }
    }
}

//接收客戶端返回pong的處理,停止等待計時器,重置離線計數(shù)器
private void PingPongHandle(byte[] obj)
{
    waitTimer.Change(Timeout.Infinite, Timeout.Infinite);
    offlineCounter = 0;
}

//發(fā)送一個ping信號,維持鏈接
public void SendPing()
{
    SendToClient((int)MsgID.PingMsg, "ping");
}

bool waitPong = true;
int offlineCounter = 0;
Timer waitTimer;

//PingPong心跳維持客戶端連接
private async void PingPong()
{
    while (client.Connected && waitPong)
    {
        //await Task.Delay(5000);
        SendPing();
        //開啟計時器等待回復,若無回復,開始離線計數(shù)
        waitTimer = new Timer(CounterCallBack, null, 4000, Timeout.Infinite);
        await Task.Delay(6000);
    }
    //一旦退出循環(huán)說明客戶端斷開,移除客戶端
    PlayerData.Instance.RemoveDisconnectClient(this);
}

//每隔5s執(zhí)行一次,累計offlineCounter到3,表明沒有收到客戶端返回pong。說明離線
private void CounterCallBack(object state)
{
    ++offlineCounter;
    if (client.Client.Connected)
        Console.WriteLine($"{DateTime.Now} | Ping    | 等待客戶端{client.Client.RemoteEndPoint}的Pong回復 | 正在離線計數(shù)...{offlineCounter}");
    if (offlineCounter == 3)
    {
        offlineCounter = 0;
        waitTimer.Change(Timeout.Infinite, Timeout.Infinite);
        waitTimer.Dispose();
        waitPong = false;
        Console.WriteLine($"{DateTime.Now} | Ping    | 客戶端{client.Client.RemoteEndPoint}已斷開連接...");
    }
}

客戶端登錄后,服務器開始發(fā)送ping維持連接,客戶端回復pong。斷開網(wǎng)絡連接。服務器向客戶端發(fā)送ping,沒有等到pong開始進行離線計數(shù),計數(shù)到3沒收到pong說明客戶端離線。

Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制?;?心跳包)、斷線重連

如果計數(shù)到3之前客戶端重新連接,服務器將不移除登錄客戶端。

Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制?;?心跳包)、斷線重連

斷線重連

Unity-Client運行時,Start就連接一次服務器(這里不論連接成功與否,不影響后面重連),下面都是客戶端的腳本。

斷線重連邏輯:當使用到的業(yè)務發(fā)送消息到服務器,等待回復,等待超時開始重連,等待回復的過程收到無論什么回復,只要收到了消息,說明連接到了服務器;如果沒接收到消息,繼續(xù)嘗試重連,并再次發(fā)送消息等待回復。

private void Handle()
{
    //包體大小(4) 協(xié)議ID(4) 包體(byte[])
    if (msgLength >= 8)
    {
        byte[] _size = new byte[4];
        Array.Copy(data, 0, _size, 0, 4);//把包體大小從第0位緩存4位長度
        int size = BitConverter.ToInt32(_size, 0);//獲得包體大小

        //本次要拿的長度
        var _length = 8 + size;//實際完整消息的長度:包體大小(4)+協(xié)議ID(4)+包體(byte[])

        while (msgLength>=_length)//判斷數(shù)據(jù)緩沖區(qū)的長度是否大于一條完整消息的長度。
        {
            //拿出id
            byte[] _id = new byte[4];
            Array.Copy(data, 4, _id, 0, 4);//把協(xié)議ID從第4位緩存4位長度
            int id = BitConverter.ToInt32(_id, 0);//獲得協(xié)議ID

            //包體
            byte[] body = new byte[size];
            Array.Copy(data, 8, body, 0, size);//把包體從第8位緩存size位長度

            if (msgLength>_length)//如果接收到的數(shù)據(jù)長度大于這次取出的完整一條數(shù)據(jù)的長度,說明還有數(shù)據(jù)
            {
                for (int i = 0; i < msgLength - _length; i++)
                {
                    data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                }
            }
            msgLength -= _length;//減掉已經(jīng)取完的消息長度
            if (id != (int)MsgID.PingMsg)
            {
                Debug.Log($"{DateTime.Now} | Message | 發(fā)送的消息類型:{id} | 接收的消息內容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            }
            else
            {
                Debug.Log($"{DateTime.Now} | Ping | 接收的消息內容:Ping");
            }
            WaitHandle?.Invoke(id, false, Encoding.UTF8.GetString(body, 0, body.Length));
            //根據(jù)id進行處理,,實際項目一般使用觀察者模式,監(jiān)聽id和Action事件綁定
            switch (id)
            {
                case (int)MsgID.RegisterMsg://注冊請求
                    RigisterMsgHandle(body);
                    break;
                case (int)MsgID.LoginMsg://登錄業(yè)務
                    LoginMsgHandle(body);
                    break;
                case (int)MsgID.ChatMsg://聊天業(yè)務
                    ChatMsgHandle(body);
                    break;
                case (int)MsgID.AddFriend://添加好友
                    AddFriendHandle(body);
                    break;
                case (int)MsgID.OnOffline://朋友上線下線
                    FriendOnOfflineHandle(body);
                    break;
                case (int)MsgID.PingMsg://維持連接
                    PingHandle(body);
                    break;
            }
        }
    }
}

//一旦開始發(fā)送消息,就讓客戶端等待消息回復,開啟定時器,如果定時器結束前沒有收到回復,說明斷開連接,在GameManager中進行重連
public event Action<int, bool, string> WaitHandle;
//按格式封裝消息,發(fā)送到服務器
public void SendToServer(int id, string str)
{
    //Debug.Log("ID:" + id);
    var body = Encoding.UTF8.GetBytes(str);
    byte[] send_buff = new byte[body.Length + 8];

    int size = body.Length;

    var _size = BitConverter.GetBytes(size);
    var _id = BitConverter.GetBytes(id);

    Array.Copy(_size, 0, send_buff, 0, 4);
    Array.Copy(_id, 0, send_buff, 4, 4);
    Array.Copy(body, 0, send_buff, 8, body.Length);
    if (id != (int)MsgID.PingMsg)
    {
        Debug.Log($"{DateTime.Now} | Message | 發(fā)送的消息類型:{id} | 發(fā)送的消息內容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
    }
    else
    {
        Debug.Log($"{DateTime.Now} | Pong | 發(fā)送的消息內容:Pong");
    }
    
    Client.Instance.Send(send_buff);
    //把發(fā)送的消息和id傳遞給訂閱WaitHandle的方法,一旦斷聯(lián),需要重連并重新發(fā)送消息。
    WaitHandle?.Invoke(id, true, Encoding.UTF8.GetString(body, 0, body.Length));
}

GameManager作用是監(jiān)聽是否發(fā)送了消息WaitHandle是否執(zhí)行,發(fā)送了消息開始計時器(只要不是退出業(yè)務和pong業(yè)務,都不需要服務器回復,不需要即使),并且緩存WaitHandle傳進來剛剛嘗試發(fā)送的消息。計時結束前,如果收到了消息WaitHandle執(zhí)行,就停止等待,說明沒有斷網(wǎng),,,如果計時結束前,沒收到消息WaitHandle沒有執(zhí)行,說明斷網(wǎng),調用Client.Instance.ReConnect重連,重連時傳進去剛剛未發(fā)送成功的消息和id,重連-再次發(fā)送-等待(計時-失敗-重連-再次發(fā)送-等待),知道重連成功,發(fā)送消息-收到回復。

public class GameManager : MonoBehaviour
{
    public bool beginWait = false;
    private int id = 0;
    private string msg = "";
    // Start is called before the first frame update
    void Start()
    {
        MessageHelper.Instance.WaitHandle += StartTimer;
        Client.Instance.Start();
        //打開登錄界面
        var loginPrefab = Resources.Load<GameObject>("LoginView");
        var loginView = GameObject.Instantiate<GameObject>(loginPrefab);
        loginView.AddComponent<LoginView>();
    }

    private void StartTimer(int msgID, bool wait, string body)
    {
        //只有timer沒滿以及不是發(fā)送下線信息時和ping消息,才等待,超時需要重連
        if (currentCount != waitCount && msgID != (int)MsgID.OnOffline && msgID != (int)MsgID.PingMsg)
        {
            beginWait = wait;
            currentCount = 0;
            id = msgID;
            msg = body;
        }
    }

    private void FixedUpdate()
    {
        if (beginWait)
        {
            Timer(id, msg);
        }
    }
    
    public int currentCount;
    public int waitCount = 100;
    public void Timer(int msgID, string body)
    {
        currentCount++;
        if (currentCount == waitCount)
        {
            currentCount = 0;
            beginWait = false;
            Client.Instance.IsConnected = false;
            Client.Instance.ReConnect(msgID, body);
        }
    }

    private void OnDestroy()
    {
        Client.Instance.isRunning = false;
        MessageHelper.Instance.WaitHandle -= StartTimer;
        //退出賬號
        if (PlayerData.Instance.LoginMsgS2C != null)
        {
            MessageHelper.Instance.SendOnOfflineMsg(PlayerData.Instance.LoginMsgS2C.account, 0);
        }
        Client.Instance.CloseClient();
    }
}

Client主要負責ReConnect,嘗試重連成功會發(fā)送剛剛未發(fā)送成功的消息,再次等待回復,計時,若失敗繼續(xù)調用重連。文章來源地址http://www.zghlxwxcb.cn/news/detail-502813.html

public class Client
{
    private static Client instance = new Client();
    public static Client Instance => instance;//單例模式便于調用

    private TcpClient client;//跟服務器通信需要調用client
    private static bool isConnected = false;
    private Thread checkStateThread;
    public bool isRunning = true;
    public bool IsConnected
    {
        get { return isConnected; }
        set { isConnected = value; }
    }

    public void Start()
    {
        //client = new TcpClient();
        Connect();
    }

    //連接服務器接口,開始時調用
    public async void Connect()
    {
        while (!isConnected)
        {
            try
            {
                if (client != null)
                {
                    client.Close();
                }
                client = new TcpClient();
                await client.ConnectAsync("6517382f5e.zicp.fun", 39047);
                if (client.Connected)
                {
                    Debug.Log("TCP 連接成功");
                    isConnected = true;

                    Receive();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                client.Close();
            }
        }
    }
    
    public async void ReConnect(int msgID, string body)
    {
        while (!isConnected)
        {
            try
            {
                if (client != null)
                {
                    client.Close();
                }
                client = new TcpClient();
                await client.ConnectAsync("6517382f5e.zicp.fun", 39047);
                isConnected = true;
                if (client.Connected)
                {
                    Debug.Log("重連成功");
                    //根據(jù)對應界面的功能,實現(xiàn)不同的重連網(wǎng)絡需求
                    MessageHelper.Instance.SendToServer(msgID, body);

                    Receive();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                client.Close();
            }
        }
    }

    //接收接口
    public async void Receive()
    {
        while (client.Connected)
        {
            try
            {
                byte[] buff = new byte[4096];
                int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
                if (length > 0)
                {
                    //Debug.Log($"{DateTime.Now} | 接收到的數(shù)據(jù)長度:{length}");
                    MessageHelper.Instance.CopyToData(buff, length);//接收到處理CopyToData給MessageHelper處理信息
                }
                else
                {
                    client.Close();
                    isConnected = false;
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                isConnected = false;
                client.Close();
            }
        }
    }
    
    //發(fā)送接口
    public async void Send(byte[] data)
    {
        try
        {
            await client.GetStream().WriteAsync(data, 0, data.Length);
            //Debug.Log("發(fā)送成功! " + $"發(fā)送的消息內容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            isConnected = false;
            client.Close();
        }
    }

    public void CloseClient()
    {
        client?.Close();
    }
}

輸出消息格式化

Console.WriteLine($"{DateTime.Now} | Message | 向{client.Client.RemoteEndPoint} | 發(fā)送的消息類型:{id} | 發(fā)送的消息內容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制?;?心跳包)、斷線重連
Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制?;?心跳包)、斷線重連

到了這里,關于Unity-TCP-網(wǎng)絡聊天功能(四): 消息粘包、心跳機制?;?心跳包)、斷線重連的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉載,請注明出處: 如若內容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包