概述
心跳消息
在长连接中,客户端和服务端之间定期发送的一种特殊的数据包
用于通知对方自己还在线,以确保长连接的有效性
由于其发送的时间间隔往往是固定的持续的,就像是心跳一样一直存在
所以我们称之为心跳消息
心跳消息的作用
1.避免非正常关闭客户端时,服务器无法正常收到关闭连接消息
通过心跳消息我们可以自定义超时判断,如果超时没有收到客户端消息,证明客户端已经
断开连接
2.避免客户端长期不发生消息,防火墙或者路由器会断开连接,我们可以通过心跳消息一
直保持活跃状态
实现心跳消息
客户端:
主要功能:定时发送消息
//发送心跳消息的间隔时间
private int SEND_HEART_MSG_TIME = 2;
private HeartMsg heartMsg = new HeartMsg();
InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME); //InvokeRepeating代表执行该函数每间隔SEND_HEART_MSG_TIME就执行一次,等待0s
/// <summary>
/// 发送心跳消息
/// </summary>
private void SendHeartMsg()
{
if (isConnected) //只有在连接状态下才能发送
Send(heartMsg);
}
服务端:
主要功能:不停检测上次收到某客户端消息的时间,如果超时则认为连接已经断开
//上一次收到消息的时间
private long frontTime = -1;
//超越时间
private static int TIME_OUT_TIME = 10;
/// <summary>
/// 如果有超时的客户端就加入待删除的字典
/// </summary>
private void CheckTimeOut()
{
if(frontTime != -1 &&
DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
{
Program.socket.AddDelSocket(this);
}
}
//在Receive()接收消息函数中检查完网络是否在连接状态之后加入CheckTimeOut();
case 999:
baseMsg = new HeartMsg();
break;
else if(msg is HeartMsg)
{
//收到心跳消息 记录收到消息的时间
frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
Console.WriteLine("收到心跳消息");
}
消息类
HeartMsg
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HeartMsg : BaseMsg
{
public override int GetBytesNum()
{
return 8;
}
public override int Reading(byte[] bytes, int beginIndex = 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 999;
}
}
客户端
NetMgr
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
public class NetMgr : MonoBehaviour
{
private static NetMgr instance;
public static NetMgr Instance => instance;
//客户端Socket
private Socket socket;
//用于发送消息的队列 公共容器 主线程往里面放 发送线程从里面取
private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
//用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取
private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();
用于收消息的水桶(容器)
//private byte[] receiveBytes = new byte[1024 * 1024];
返回收到的字节数
//private int receiveNum;
//用于处理分包时 缓存的 字节数组 和 字节数组长度
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
//是否连接
private bool isConnected = false;
//发送心跳消息的间隔时间
private int SEND_HEART_MSG_TIME = 2;
private HeartMsg heartMsg = new HeartMsg();
void Awake()
{
instance = this;
DontDestroyOnLoad(this.gameObject);
//客户端循环定时给服务端发送心跳消息
InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME); //InvokeRepeating代表执行该函数每间隔SEND_HEART_MSG_TIME就执行一次,等待0s
}
/// <summary>
/// 发送心跳消息
/// </summary>
private void SendHeartMsg()
{
if (isConnected) //只有在连接状态下才能发送
Send(heartMsg);
}
// Update is called once per frame
void Update()
{
if (receiveQueue.Count > 0)
{
BaseMsg msg = receiveQueue.Dequeue();
if (msg is PlayerMsg)
{
PlayerMsg playerMsg = (msg as PlayerMsg);
print(playerMsg.playerID);
print(playerMsg.playerData.name);
print(playerMsg.playerData.lev);
print(playerMsg.playerData.atk);
}
}
}
//连接服务端
public void Connect(string ip, int port)
{
//如果是连接状态 直接返回
if (isConnected)
return;
if (socket == null)
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//连接服务端
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
try
{
socket.Connect(ipPoint);
isConnected = true;
//开启发送线程
ThreadPool.QueueUserWorkItem(SendMsg);
//开启接收线程
ThreadPool.QueueUserWorkItem(ReceiveMsg);
}
catch (SocketException e)
{
if (e.ErrorCode == 10061)
print("服务器拒绝连接");
else
print("连接失败" + e.ErrorCode + e.Message);
}
}
//发送消息
public void Send(BaseMsg msg)
{
sendMsgQueue.Enqueue(msg);
}
/// <summary>
/// 用于测试 直接发字节数组的方法
/// </summary>
/// <param name="bytes"></param>
public void SendTest(byte[] bytes)
{
socket.Send(bytes);
}
private void SendMsg(object obj)
{
while (isConnected)
{
if (sendMsgQueue.Count > 0)
{
socket.Send(sendMsgQueue.Dequeue().Writeing());
}
}
}
//不停的接受消息
private void ReceiveMsg(object obj)
{
while (isConnected)
{
if (socket.Available > 0)
{
byte[] receiveBytes = new byte[1024 * 1024];
int receiveNum = socket.Receive(receiveBytes);
HandleReceiveMsg(receiveBytes, receiveNum);
}
}
}
//处理接受消息 分包、黏包问题的方法
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
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:
PlayerMsg msg = new PlayerMsg();
msg.Reading(cacheBytes, nowIndex);
baseMsg = msg;
break;
}
if (baseMsg != null)
receiveQueue.Enqueue(baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
public void Close()
{
if (socket != null)
{
print("客户端主动断开连接");
//QuitMsg msg = new QuitMsg();
//socket.Send(msg.Writeing());
//socket.Shutdown(SocketShutdown.Both);
//socket.Disconnect(false);
//socket.Close();
socket = null;
isConnected = false;
}
}
private void OnDestroy()
{
Close();
}
}
服务端
ClientSocket
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace Test04
{
class ClientSocket
{
private static int CLIENT_BEGIN_ID = 1;
public int clientID;
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(CheckTimeOut);
}
/// <summary>
/// 如果有超时的客户端就加入待删除的字典
/// </summary>
private void CheckTimeOut()
{
if(frontTime != -1 &&
DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
{
Program.socket.AddDelSocket(this);
}
}
/// <summary>
/// 是否是连接状态
/// </summary>
public bool Connected => this.socket.Connected;
//我们应该封装一些方法
//关闭
public void Close()
{
if (socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
//发送
public void Send(BaseMsg info)
{
if (Connected)
{
try
{
socket.Send(info.Writeing());
}
catch (Exception e)
{
Console.WriteLine("发消息出错" + e.Message);
//将这个错误的客户端装入待删除的字典
Program.socket.AddDelSocket(this);
}
}
else
Program.socket.AddDelSocket(this);
}
//接收
public void Receive()
{
if (!Connected)
{
Program.socket.AddDelSocket(this);
return;
}
try
{
if (socket.Available > 0)
{
byte[] result = new byte[1024 * 5];
int receiveNum = socket.Receive(result);
HandleReceiveMsg(result, receiveNum);
}
//检测 是否超时
CheckTimeOut();
}
catch (Exception e)
{
Console.WriteLine("收消息出错" + e.Message);
Program.socket.AddDelSocket(this);
}
}
//处理接受消息 分包、黏包问题的方法
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
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:
PlayerMsg msg = new PlayerMsg();
msg.Reading(cacheBytes, nowIndex);
baseMsg = msg;
break;
case 0000:
baseMsg = new QuitMsg();
//由于该消息没有消息体 所以都不用反序列化
break;
case 999:
baseMsg = new HeartMsg();
break;
}
if (baseMsg != null)
ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
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("收到心跳消息");
}
}
}
}
最后
以上就是发嗲咖啡为你收集整理的Unity——心跳消息的全部内容,希望文章能够帮你解决Unity——心跳消息所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复