客户端主动断开连接
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;
}
}
}
}
}