报错修改
This commit is contained in:
14
Assets/Main/HotfixUpdateScript/Runtime/UnitySocket/.gitignore
vendored
Normal file
14
Assets/Main/HotfixUpdateScript/Runtime/UnitySocket/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
*.sln
|
||||
*.csproj
|
||||
SocketClient/.vscode/
|
||||
SocketClient/Packages/
|
||||
SocketClient/Logs/
|
||||
SocketClient/Library/
|
||||
SocketServer/Packages/
|
||||
SocketServer/obj/
|
||||
SocketServer/Logs/
|
||||
SocketServer/Library/
|
||||
SocketServer/.vscode/
|
||||
SocketClient/obj/
|
||||
SocketClient/Temp/
|
||||
SocketServer/Temp/
|
||||
71
Assets/Main/HotfixUpdateScript/Runtime/UnitySocket/README.md
Normal file
71
Assets/Main/HotfixUpdateScript/Runtime/UnitySocket/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# UnitySocket
|
||||
该项目是基于Unity平台 纯C#编写的Socket网络模块,包括客户端与服务端的实现
|
||||
|
||||
## 环境依赖
|
||||
|
||||
- 项目运行平台: Unity2022.3.22f1
|
||||
|
||||
该项目仅包含网络底层的几个C#脚本与一个测试脚本,可以随意选择Unity版本测试。
|
||||
|
||||
## 特点
|
||||
1. 回调事件完善,包含服务器主动踢出、客户端主动断开、心跳包等底层功能逻辑。
|
||||
2. 对象形式,非单例模式,一个项目可new多个连接
|
||||
3. 代码量少,纯C#编写,未使用插件、DLL及工具类,便于二次封装,利于移植
|
||||
## 报文组成
|
||||
- 通用
|
||||
|
||||
报文总长度(4byte) + 报文类型(2byte) + 数据(N byte)
|
||||
|
||||
- 心跳包
|
||||
|
||||
报文总长度(4byte) + 报文类型-心跳包(2byte)
|
||||
|
||||
- 客户端断开
|
||||
|
||||
报文总长度(4byte) + 报文类型-客户端断开(2byte)
|
||||
|
||||
- 服务端踢出
|
||||
|
||||
报文总长度(4byte) + 报文类型-服务端踢出(2byte)
|
||||
|
||||
## 工作流
|
||||
- 建立连接
|
||||
1. 服务端通过 new SocketServer(string ip,int port) 建立连接
|
||||
2. 客户端通过 new SocketClient(string ip,int port) 建立实例,通过Connect函数尝试连接。
|
||||
3. 客户端连接成功后,开始定时发送心跳包报文;服务端接收后记录时间,定时检测判断是否超时。
|
||||
4. 如存在以下情况,则断开连接:
|
||||
- 服务端主动踢出连接,即时响应,回调DisConnect
|
||||
- 客户端主动断开连接,即时响应,回调DisConnect
|
||||
- 客户端关闭连接,即时响应,回调DisConnect
|
||||
- 服务端检测到客户端太久没有发送心跳包,此处需要响应时间,根据心跳包超时间隔、心跳包发送间隔及服务端心跳包超时检测间隔来决定,发现超时则回调DisConnect
|
||||
- 通信逻辑过程中报错,即时响应,自动重连,默认重连上限为三次,达到重连上限并未成功,则回调DisConnect
|
||||
- 接收数据
|
||||
1. 接收线程收到数据,存放入DataBuff数据缓存区。
|
||||
2. DataBuff中不断通过SocketDataPack尝试解包,解包成功后将报文发送到接收线程。
|
||||
3. 接收线程收到报文后,触发OnReceive回调
|
||||
4. 业务层通过OnReceive回调接收到报文后,反序列化报文体得到数据
|
||||
- 发送数据
|
||||
1. 业务层序列化数据得到报文体字节集,通过Send函数发送数据。
|
||||
2. Send函数中,通过SocketDataPack装包后,以字节集的形式发送出去。
|
||||
|
||||
## 代码说明
|
||||
- DataBuffer:Socket传输数据缓存区,此处主要处理Socket传输时粘包、分包的情况
|
||||
- SocketEvent:Socket报文类型枚举,此处只枚举了网络底层需要发送的报文类型,业务逻辑层所使用的报文类型,建议封装至报文体序列化类中
|
||||
- SocketDataPack:Socket报文类,处理具体的拆包、装包逻辑
|
||||
- SocketServer:Socket服务端
|
||||
- SocketClient:Socket客户端
|
||||
|
||||
## 回调事件
|
||||
- SocketServer 服务端
|
||||
- public event Action<Socket> OnConnect; //客户端建立连接回调
|
||||
- public event Action<Socket> OnDisconnect; // 客户端断开连接回调
|
||||
- public event Action<Socket, SocketDataPack> OnReceive; // 接收报文回调
|
||||
- SocketClient 客户端
|
||||
- public event Action OnConnectSuccess; // 连接成功回调
|
||||
- public event Action OnConnectError; // 连接失败回调
|
||||
- public event Action OnDisconnect; // 断开回调
|
||||
- public event Action<SocketDataPack> OnReceive; // 接收报文回调
|
||||
- public event Action<SocketException> OnError; // 异常捕获回调
|
||||
- public event Action<int> OnReConnectSuccess; // 重连成功回调
|
||||
- public event Action<int> OnReConnectError; // 单次重连失败回调
|
||||
- public event Action<int> OnReconnecting; // 单次重连中回调
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0c65b3aa2616440978bf5d9f804e7c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b43e2f4bdef74642999b4d5530a9b3d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e04a0e82198abe4fb1796dc44bc9e9d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3cdc884284e12f48a350017446975ca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
/// <summary>
|
||||
/// Socket传输过程的缓冲区,尝试拆包获得数据
|
||||
/// </summary>
|
||||
public class DataBuffer
|
||||
{
|
||||
// 缓存区长度
|
||||
private const int MIN_BUFF_LEN = 1024;
|
||||
|
||||
private byte[] _buff;
|
||||
private int _buffLength = 0;
|
||||
|
||||
public DataBuffer(int minBuffLen = MIN_BUFF_LEN)
|
||||
{
|
||||
if (minBuffLen <= 0)
|
||||
{
|
||||
minBuffLen = MIN_BUFF_LEN;
|
||||
}
|
||||
_buff = new byte[minBuffLen];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加缓存数据
|
||||
/// </summary>
|
||||
public void AddBuffer(byte[] data, int len)
|
||||
{
|
||||
byte[] buff = new byte[len];
|
||||
Array.Copy(data, buff, len);
|
||||
if (len > _buff.Length - _buffLength) //超过当前缓存
|
||||
{
|
||||
byte[] temp = new byte[_buffLength + len];
|
||||
Array.Copy(_buff, 0, temp, 0, _buffLength);
|
||||
Array.Copy(buff, 0, temp, _buffLength, len);
|
||||
_buff = temp;
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(data, 0, _buff, _buffLength, len);
|
||||
}
|
||||
_buffLength += len;//修改当前数据标记
|
||||
}
|
||||
|
||||
public bool TryUnpack(out SocketDataPack dataPack)
|
||||
{
|
||||
dataPack = SocketDataPack.Unpack(_buff);
|
||||
if (dataPack == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 清理旧缓存
|
||||
_buffLength -= dataPack.BuffLength;
|
||||
byte[] temp = new byte[_buffLength < MIN_BUFF_LEN ? MIN_BUFF_LEN : _buffLength];
|
||||
Array.Copy(_buff, dataPack.BuffLength, temp, 0, _buffLength);
|
||||
_buff = temp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e879d0a89aac39f4a8320fd3857d0012
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,332 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Socket客户端
|
||||
/// </summary>
|
||||
public class SocketClient
|
||||
{
|
||||
/// <summary>
|
||||
/// 主线程
|
||||
/// </summary>
|
||||
private readonly SynchronizationContext _mainThread;
|
||||
|
||||
|
||||
private readonly string _ip;
|
||||
private readonly int _port;
|
||||
|
||||
private const int TIMEOUT_CONNECT = 3000; // 连接超时时间 毫秒
|
||||
private const int TIMEOUT_SEND = 3000; // 发送超时时间 毫秒
|
||||
private const int TIMEOUT_RECEIVE = 3000; //接收超时时间 毫秒
|
||||
|
||||
private const int HEAD_OFFSET = 2000; //心跳包发送间隔 毫秒
|
||||
private const int RECONN_MAX_SUM = 3; //最大重连次数
|
||||
|
||||
private Socket _client;
|
||||
private Thread _receiveThread;
|
||||
private System.Timers.Timer _connTimeoutTimer;
|
||||
private System.Timers.Timer _headTimer;
|
||||
private readonly DataBuffer _dataBuffer = new DataBuffer();
|
||||
|
||||
public event Action OnConnectSuccess; // 连接成功回调
|
||||
public event Action OnConnectError; // 连接失败回调
|
||||
public event Action OnDisconnect; // 断开回调
|
||||
public event Action<SocketDataPack> OnReceive; // 接收报文回调
|
||||
public event Action<SocketDataPack> OnSend; // 发送报文回调
|
||||
public event Action<SocketException> OnError; // 异常捕获回调
|
||||
public event Action<int> OnReConnectSuccess; // 重连成功回调
|
||||
public event Action<int> OnReConnectError; // 单次重连失败回调
|
||||
public event Action<int> OnReconnecting; // 单次重连中回调
|
||||
|
||||
private bool _isConnect = false;
|
||||
private bool _isReconnect = false;
|
||||
|
||||
|
||||
public SocketClient(string ip, int port)
|
||||
{
|
||||
_mainThread = SynchronizationContext.Current;
|
||||
|
||||
_ip = ip;
|
||||
_port = port;
|
||||
}
|
||||
public void Connect(Action success = null, Action error = null)
|
||||
{
|
||||
OnDisconnect += () => { UnityEngine.Debug.Log("UnityEvo:断开连接"); };
|
||||
|
||||
OnReceive += (dataPack) => { UnityEngine.Debug.LogFormat("UnityEvo:接收数据>>>{0}", (SocketEvent)dataPack.Type); };
|
||||
OnSend += (dataPack) => { UnityEngine.Debug.LogFormat("UnityEvo:发送数据>>>{0}", (SocketEvent)dataPack.Type); };
|
||||
OnError += (ex) => { UnityEngine.Debug.LogFormat("UnityEvo:出现异常>>>{0}", ex); };
|
||||
OnReConnectSuccess += (num) => { UnityEngine.Debug.LogFormat("UnityEvo:第{0}次重连成功", num); };
|
||||
OnReConnectError += (num) => { UnityEngine.Debug.LogFormat("UnityEvo:第{0}次重连失败", num); };
|
||||
OnReconnecting += (num) => { UnityEngine.Debug.LogFormat("UnityEvo:正在进行第{0}次重连", num); };
|
||||
|
||||
void OnTrigger(bool flag)
|
||||
{
|
||||
if (flag)
|
||||
{
|
||||
PostMainThreadAction(success);
|
||||
PostMainThreadAction(OnConnectSuccess);
|
||||
}
|
||||
else
|
||||
{
|
||||
PostMainThreadAction(error);
|
||||
PostMainThreadAction(OnConnectError);
|
||||
}
|
||||
|
||||
if (_connTimeoutTimer != null)
|
||||
{
|
||||
_connTimeoutTimer.Stop();
|
||||
_connTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//创建套接字
|
||||
_client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, TIMEOUT_SEND);
|
||||
_client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, TIMEOUT_RECEIVE);
|
||||
IPAddress ipAddress = IPAddress.Parse(_ip);//解析IP地址
|
||||
IPEndPoint ipEndpoint = new IPEndPoint(ipAddress, _port);
|
||||
IAsyncResult result = _client.BeginConnect(ipEndpoint, new AsyncCallback((iar) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Socket client = (Socket)iar.AsyncState;
|
||||
client.EndConnect(iar);
|
||||
|
||||
_isConnect = true;
|
||||
// 开始发送心跳包
|
||||
_headTimer = new System.Timers.Timer(HEAD_OFFSET);
|
||||
_headTimer.AutoReset = true;
|
||||
_headTimer.Elapsed += delegate (object sender, ElapsedEventArgs args)
|
||||
{
|
||||
Send((UInt16)SocketEvent.ClientHead);
|
||||
};
|
||||
_headTimer.Start();
|
||||
|
||||
// 开始接收数据
|
||||
_receiveThread = new Thread(new ThreadStart(ReceiveEvent));
|
||||
_receiveThread.IsBackground = true;
|
||||
_receiveThread.Start();
|
||||
|
||||
OnTrigger(true);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
OnTrigger(false);
|
||||
}
|
||||
}), _client);//异步连接
|
||||
|
||||
_connTimeoutTimer = new System.Timers.Timer(TIMEOUT_CONNECT);
|
||||
_connTimeoutTimer.AutoReset = false;
|
||||
_connTimeoutTimer.Elapsed += delegate (object sender, ElapsedEventArgs args)
|
||||
{
|
||||
OnTrigger(false);
|
||||
};
|
||||
_connTimeoutTimer.Start();
|
||||
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
OnTrigger(false);
|
||||
// throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断线重连
|
||||
/// </summary>
|
||||
/// <param name="num"></param>
|
||||
/// <param name="index"></param>
|
||||
public void ReConnect(int num = RECONN_MAX_SUM, int index = 0)
|
||||
{
|
||||
_isReconnect = true;
|
||||
|
||||
num--;
|
||||
index++;
|
||||
if (num < 0)
|
||||
{
|
||||
onDisconnect();
|
||||
_isReconnect = false;
|
||||
return;
|
||||
}
|
||||
PostMainThreadAction<int>(OnReconnecting, index);
|
||||
Connect(() =>
|
||||
{
|
||||
PostMainThreadAction<int>(OnReConnectSuccess, index);
|
||||
_isReconnect = false;
|
||||
}, () =>
|
||||
{
|
||||
PostMainThreadAction<int>(OnReConnectError, index);
|
||||
ReConnect(num, index);
|
||||
});
|
||||
|
||||
}
|
||||
public void Send(UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null)
|
||||
{
|
||||
buff = buff ?? new byte[] { };
|
||||
var dataPack = new SocketDataPack(e, buff);
|
||||
var data = dataPack.Buff;
|
||||
try
|
||||
{
|
||||
_client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback((asyncSend) =>
|
||||
{
|
||||
Socket c = (Socket)asyncSend.AsyncState;
|
||||
c.EndSend(asyncSend);
|
||||
PostMainThreadAction<SocketDataPack>(onTrigger, dataPack);
|
||||
PostMainThreadAction<SocketDataPack>(OnSend, dataPack);
|
||||
}), _client);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
onError(ex);
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 线程内接收数据的函数
|
||||
/// </summary>
|
||||
private void ReceiveEvent()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isConnect) break;
|
||||
if (_client.Available <= 0) continue;
|
||||
byte[] rbytes = new byte[8 * 1024];
|
||||
int len = _client.Receive(rbytes);
|
||||
if (len > 0)
|
||||
{
|
||||
_dataBuffer.AddBuffer(rbytes, len); // 将收到的数据添加到缓存器中
|
||||
if (_dataBuffer.TryUnpack(out var dataPack)) // 尝试解包
|
||||
{
|
||||
if (dataPack.Type == (UInt16)SocketEvent.ServerKickout)
|
||||
{
|
||||
// 服务端踢出
|
||||
onDisconnect();
|
||||
}
|
||||
else if (dataPack.Type == (UInt16)SocketEvent.ServerMessage)
|
||||
{
|
||||
// 收到消息
|
||||
PostMainThreadAction<SocketDataPack>(OnReceive, dataPack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
onError(ex);
|
||||
// throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 业务逻辑 - 客户端主动断开
|
||||
/// </summary>
|
||||
public void DisConnect()
|
||||
{
|
||||
Send((UInt16)SocketEvent.ClientDisconn);
|
||||
onDisconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓存数据清理
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (!_isConnect) return;
|
||||
_isConnect = false;
|
||||
if (_headTimer != null)
|
||||
{
|
||||
_headTimer.Stop();
|
||||
_headTimer = null;
|
||||
}
|
||||
// if (_receiveThread != null)
|
||||
// {
|
||||
// _receiveThread.Abort();
|
||||
// _receiveThread = null;
|
||||
// }
|
||||
if (_connTimeoutTimer != null)
|
||||
{
|
||||
_connTimeoutTimer.Stop();
|
||||
_connTimeoutTimer = null;
|
||||
}
|
||||
if (_client != null)
|
||||
{
|
||||
_client.Close();
|
||||
_client = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 错误回调
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
private void onError(SocketException ex)
|
||||
{
|
||||
Close();
|
||||
|
||||
PostMainThreadAction<SocketException>(OnError, ex);
|
||||
|
||||
if (!_isReconnect)
|
||||
{
|
||||
ReConnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 断开回调
|
||||
/// </summary>
|
||||
private void onDisconnect()
|
||||
{
|
||||
|
||||
Close();
|
||||
|
||||
PostMainThreadAction(OnDisconnect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知主线程回调
|
||||
/// </summary>
|
||||
private void PostMainThreadAction(Action action)
|
||||
{
|
||||
_mainThread.Post(new SendOrPostCallback((o) =>
|
||||
{
|
||||
Action e = (Action)o.GetType().GetProperty("action")?.GetValue(o);
|
||||
if (e != null) e();
|
||||
}), new { action = action });
|
||||
}
|
||||
private void PostMainThreadAction<T>(Action<T> action, T arg1)
|
||||
{
|
||||
_mainThread.Post(new SendOrPostCallback((o) =>
|
||||
{
|
||||
Action<T> e = (Action<T>)o.GetType().GetProperty("action")?.GetValue(o);
|
||||
T t1 = (T)o.GetType().GetProperty("arg1")?.GetValue(o);
|
||||
if (e != null) e(t1);
|
||||
}), new { action = action, arg1 = arg1 });
|
||||
}
|
||||
public void PostMainThreadAction<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2)
|
||||
{
|
||||
_mainThread.Post(new SendOrPostCallback((o) =>
|
||||
{
|
||||
Action<T1, T2> e = (Action<T1, T2>)o.GetType().GetProperty("action")?.GetValue(o);
|
||||
T1 t1 = (T1)o.GetType().GetProperty("arg1")?.GetValue(o);
|
||||
T2 t2 = (T2)o.GetType().GetProperty("arg2")?.GetValue(o);
|
||||
if (e != null) e(t1, t2);
|
||||
}), new { action = action, arg1 = arg1, arg2 = arg2 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e91098b3165d6b478e1a1ab72aeb94c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
/// <summary>
|
||||
/// Socket通信过程中的数据包 处理具体拆包装包逻辑
|
||||
/// </summary>
|
||||
public class SocketDataPack
|
||||
{
|
||||
// 消息:数据总长度(4byte) + 数据类型(2byte) + 数据(N byte)
|
||||
public const int HEAD_DATA_LEN = 4;
|
||||
public const int HEAD_TYPE_LEN = 2;
|
||||
|
||||
public static int HeadLen
|
||||
{
|
||||
get { return HEAD_DATA_LEN + HEAD_TYPE_LEN; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据包类型
|
||||
/// </summary>
|
||||
public UInt16 Type;
|
||||
/// <summary>
|
||||
/// 数据包数据
|
||||
/// </summary>
|
||||
public byte[] Data;
|
||||
public byte[] Buff;
|
||||
|
||||
public int BuffLength
|
||||
{
|
||||
get { return Buff.Length; }
|
||||
}
|
||||
public int DataLength
|
||||
{
|
||||
get { return Data.Length; }
|
||||
}
|
||||
|
||||
public SocketDataPack()
|
||||
{
|
||||
|
||||
}
|
||||
public SocketDataPack(UInt16 type, byte[] data)
|
||||
{
|
||||
Type = type;
|
||||
Data = data;
|
||||
|
||||
Buff = GetBuff(Type, Data);
|
||||
}
|
||||
|
||||
public static byte[] GetBuff(UInt16 type, byte[] data)
|
||||
{
|
||||
byte[] buff = new byte[data.Length + HeadLen];
|
||||
byte[] temp;
|
||||
temp = BitConverter.GetBytes(buff.Length);
|
||||
Array.Copy(temp, 0, buff, 0, HEAD_DATA_LEN);
|
||||
temp = BitConverter.GetBytes(type);
|
||||
Array.Copy(temp, 0, buff, HEAD_DATA_LEN, HEAD_TYPE_LEN);
|
||||
|
||||
Array.Copy(data, 0, buff, HeadLen, data.Length);
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
public static SocketDataPack Unpack(byte[] buff)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (buff.Length < HeadLen)
|
||||
{
|
||||
// 头部没取完则返回
|
||||
return null;
|
||||
}
|
||||
byte[] temp;
|
||||
// 取数据长度
|
||||
temp = new byte[HEAD_DATA_LEN];
|
||||
Array.Copy(buff, 0, temp, 0, HEAD_DATA_LEN);
|
||||
int buffLength = BitConverter.ToInt32(temp, 0);
|
||||
if (buffLength <= 0) return null;
|
||||
if (buffLength > buff.Length)
|
||||
{
|
||||
// 数据没取完
|
||||
return null;
|
||||
}
|
||||
int dataLength = buffLength - HeadLen;
|
||||
// 取数据类型
|
||||
temp = new byte[HEAD_TYPE_LEN];
|
||||
Array.Copy(buff, HEAD_DATA_LEN, temp, 0, HEAD_TYPE_LEN);
|
||||
UInt16 dataType = BitConverter.ToUInt16(temp, 0);
|
||||
// 取数据
|
||||
byte[] data = new byte[dataLength];
|
||||
Array.Copy(buff, HeadLen, data, 0, dataLength);
|
||||
|
||||
var dataPack = new SocketDataPack(dataType, data);
|
||||
// UnityEngine.Debug.LogFormat("buffLen:{0} type:{1} dataLength:{2}", buffLength, dataType, data.Length);
|
||||
|
||||
return dataPack;
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 存在不完整数据解包 则返回null
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e0c4c6bf95d3324a975f6ce0cdee4c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
/// <summary>
|
||||
/// 网络事件协议ID枚举
|
||||
/// </summary>
|
||||
public enum SocketEvent
|
||||
{
|
||||
ClientHead = 0x0001, //心跳包
|
||||
ClientDisconn = 0x0002, //客户端主动断开
|
||||
ServerKickout = 0x0003, //服务端踢出
|
||||
|
||||
ServerMessage = 0x0004, //服务端发送消息
|
||||
ClientMessage = 0x0005, //客户端发送消息
|
||||
|
||||
ScTest = 0x1001, //测试用
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1c8d60cbbc2c7547827955a371fda00
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,349 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
using UnityEngine;
|
||||
|
||||
public class SocketInfo
|
||||
{
|
||||
public string IP;
|
||||
public Thread ReceiveThread;
|
||||
public long HeadTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Socket服务端
|
||||
/// </summary>
|
||||
public class SocketServer
|
||||
{
|
||||
/// <summary>
|
||||
/// 主线程
|
||||
/// </summary>
|
||||
private readonly SynchronizationContext _mainThread;
|
||||
|
||||
|
||||
private const int HEAD_TIMEOUT = 5000; // 心跳超时 毫秒
|
||||
private const int HEAD_CHECKTIME = 5000; // 心跳包超时检测 毫秒
|
||||
|
||||
public readonly Dictionary<Socket, SocketInfo> ClientInfoDic = new Dictionary<Socket, SocketInfo>();
|
||||
|
||||
private readonly Socket _server;
|
||||
private System.Timers.Timer _headCheckTimer;
|
||||
private readonly DataBuffer _dataBuffer = new DataBuffer();
|
||||
|
||||
public event Action<Socket> OnConnect; //客户端建立连接回调
|
||||
public event Action<Socket> OnDisconnect; // 客户端断开连接回调
|
||||
public event Action<Socket, SocketDataPack> OnReceive; // 接收报文回调
|
||||
public event Action<Socket, SocketDataPack> OnSend; // 发送报文回调
|
||||
|
||||
// 目前捕获异常将触发OnDisconnect回调 暂不单独处理
|
||||
// public event Action<SocketException> OnError; // 异常捕获回调
|
||||
|
||||
private bool _isValid = true;
|
||||
|
||||
public SocketServer(string ip, int port)
|
||||
{
|
||||
OnConnect += (client) =>
|
||||
{
|
||||
UnityEngine.Debug.LogFormat("UnityEvo:连接成功 >> IP:{0}", client.LocalEndPoint.ToString());
|
||||
};
|
||||
OnDisconnect += (client) =>
|
||||
{
|
||||
if(ClientInfoDic.TryGetValue(client, out var value))
|
||||
Debug.LogFormat("UnityEvo:连接断开 >> IP:{0}", value.IP);
|
||||
};
|
||||
|
||||
_mainThread = SynchronizationContext.Current;
|
||||
|
||||
_server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
IPAddress ipAddress = IPAddress.Parse(ip); //解析IP地址
|
||||
_server.Bind(new IPEndPoint(ipAddress, port)); //绑定IP地址:端口
|
||||
|
||||
_server.Listen(10); //设定最多10个排队连接请求
|
||||
|
||||
// 启动线程监听连接
|
||||
var connectThread = new Thread(ListenClientConnect);
|
||||
connectThread.Start();
|
||||
|
||||
// 心跳包定时检测
|
||||
_headCheckTimer = new System.Timers.Timer(HEAD_CHECKTIME);
|
||||
_headCheckTimer.AutoReset = true;
|
||||
_headCheckTimer.Elapsed += delegate(object sender, ElapsedEventArgs args) { CheckHeadTimeOut(); };
|
||||
_headCheckTimer.Start();
|
||||
Debug.Log($"SocketServer Start: {ip}:{port}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听客户端连接
|
||||
/// </summary>
|
||||
private void ListenClientConnect()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_isValid) break;
|
||||
Socket client = _server.Accept();
|
||||
IPEndPoint ipEndPoint = (IPEndPoint)client.RemoteEndPoint;
|
||||
Thread receiveThread = new Thread(ReceiveEvent);
|
||||
ClientInfoDic.Add(client,
|
||||
new SocketInfo() { IP = ipEndPoint.Address.ToString(), ReceiveThread = receiveThread, HeadTime = GetNowTime() });
|
||||
receiveThread.Start(client);
|
||||
|
||||
PostMainThreadAction<Socket>(OnConnect, client);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前时间戳
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private long GetNowTime()
|
||||
{
|
||||
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
|
||||
return Convert.ToInt64(ts.TotalMilliseconds);
|
||||
}
|
||||
|
||||
public void Send(Socket client, UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null)
|
||||
{
|
||||
SendMessage(client, e, buff, onTrigger);
|
||||
}
|
||||
|
||||
|
||||
private void SendMessage(Socket client, UInt16 e, byte[] buff = null, Action<SocketDataPack> onTrigger = null)
|
||||
{
|
||||
buff = buff ?? new byte[] { };
|
||||
var dataPack = new SocketDataPack(e, buff);
|
||||
var data = dataPack.Buff;
|
||||
try
|
||||
{
|
||||
client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback((asyncSend) =>
|
||||
{
|
||||
Socket c = (Socket)asyncSend.AsyncState;
|
||||
c.EndSend(asyncSend);
|
||||
PostMainThreadAction<SocketDataPack>(onTrigger, dataPack);
|
||||
PostMainThreadAction<Socket, SocketDataPack>(OnSend, client, dataPack);
|
||||
}), client);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
CloseClient(client);
|
||||
// onError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线程内接收数据的函数
|
||||
/// </summary>
|
||||
private void ReceiveEvent(object client)
|
||||
{
|
||||
Socket tsocket = (Socket)client;
|
||||
while (true)
|
||||
{
|
||||
if (!_isValid) return;
|
||||
if (!ClientInfoDic.ContainsKey(tsocket))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
byte[] rbytes = new byte[8 * 1024];
|
||||
int len = tsocket.Receive(rbytes);
|
||||
if (len > 0)
|
||||
{
|
||||
_dataBuffer.AddBuffer(rbytes, len); // 将收到的数据添加到缓存器中
|
||||
if (_dataBuffer.TryUnpack(out var dataPack)) // 尝试解包
|
||||
{
|
||||
if (dataPack.Type == (UInt16)SocketEvent.ClientHead)
|
||||
{
|
||||
// 接收到心跳包
|
||||
ReceiveHead(tsocket);
|
||||
}
|
||||
else if (dataPack.Type == (UInt16)SocketEvent.ClientDisconn)
|
||||
{
|
||||
// 客户端断开连接
|
||||
CloseClient(tsocket);
|
||||
}
|
||||
else if (dataPack.Type == (UInt16)SocketEvent.ClientMessage)
|
||||
{
|
||||
// 收到消息
|
||||
PostMainThreadAction<Socket, SocketDataPack>(OnReceive, tsocket, dataPack);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (tsocket.Poll(-1, SelectMode.SelectRead))
|
||||
{
|
||||
CloseClient(tsocket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
CloseClient(tsocket);
|
||||
// onError(ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收到心跳包
|
||||
/// </summary>
|
||||
private void ReceiveHead(Socket client)
|
||||
{
|
||||
SocketInfo info;
|
||||
if (ClientInfoDic.TryGetValue(client, out info))
|
||||
{
|
||||
long now = GetNowTime();
|
||||
long offset = now - info.HeadTime;
|
||||
UnityEngine.Debug.Log("更新心跳时间戳 >>>" + now + " 间隔>>>" + offset);
|
||||
if (offset > HEAD_TIMEOUT)
|
||||
{
|
||||
// 心跳包收到但超时逻辑
|
||||
}
|
||||
|
||||
info.HeadTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测心跳包超时
|
||||
/// </summary>
|
||||
private void CheckHeadTimeOut()
|
||||
{
|
||||
var tempList = new List<Socket>();
|
||||
foreach (var socket in ClientInfoDic.Keys)
|
||||
{
|
||||
tempList.Add(socket);
|
||||
}
|
||||
|
||||
foreach (var socket in tempList)
|
||||
{
|
||||
var info = ClientInfoDic[socket];
|
||||
long now = GetNowTime();
|
||||
long offset = now - info.HeadTime;
|
||||
if (offset > HEAD_TIMEOUT)
|
||||
{
|
||||
// 心跳包超时
|
||||
KickOut(socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void KickOut(Socket client)
|
||||
{
|
||||
// 踢出连接
|
||||
Send(client, (UInt16)SocketEvent.ServerKickout, null, (dataPack) => { CloseClient(client); });
|
||||
}
|
||||
|
||||
public void KickOutAll()
|
||||
{
|
||||
var tempList = new List<Socket>();
|
||||
foreach (var socket in ClientInfoDic.Keys)
|
||||
{
|
||||
tempList.Add(socket);
|
||||
}
|
||||
|
||||
foreach (var socket in tempList)
|
||||
{
|
||||
KickOut(socket);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理客户端连接
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
private void CloseClient(Socket client)
|
||||
{
|
||||
PostMainThreadAction<Socket>((socket) =>
|
||||
{
|
||||
if (OnDisconnect != null) OnDisconnect(socket);
|
||||
ClientInfoDic.Remove(socket);
|
||||
socket.Close();
|
||||
}, client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (!_isValid) return;
|
||||
_isValid = false;
|
||||
// if (_connectThread != null) _connectThread.Abort();
|
||||
var tempList = new List<Socket>();
|
||||
foreach (var socket in ClientInfoDic.Keys)
|
||||
{
|
||||
tempList.Add(socket);
|
||||
}
|
||||
|
||||
foreach (var socket in tempList)
|
||||
{
|
||||
CloseClient(socket);
|
||||
}
|
||||
|
||||
if (_headCheckTimer != null)
|
||||
{
|
||||
_headCheckTimer.Stop();
|
||||
_headCheckTimer = null;
|
||||
}
|
||||
|
||||
_server.Close();
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// 错误回调
|
||||
// /// </summary>
|
||||
// /// <param name="e"></param>
|
||||
// private void onError(SocketException ex)
|
||||
// {
|
||||
// PostMainThreadAction<SocketException>(OnError, ex);
|
||||
// }
|
||||
|
||||
|
||||
// <summary>
|
||||
/// 通知主线程回调
|
||||
/// </summary>
|
||||
private void PostMainThreadAction(Action action)
|
||||
{
|
||||
_mainThread.Post(new SendOrPostCallback((o) =>
|
||||
{
|
||||
Action e = (Action)o.GetType().GetProperty("action")?.GetValue(o);
|
||||
if (e != null) e();
|
||||
}), new { action = action });
|
||||
}
|
||||
|
||||
private void PostMainThreadAction<T>(Action<T> action, T arg1)
|
||||
{
|
||||
_mainThread.Post(new SendOrPostCallback((o) =>
|
||||
{
|
||||
Action<T> e = (Action<T>)o.GetType().GetProperty("action")?.GetValue(o);
|
||||
T t1 = (T)o.GetType().GetProperty("arg1")?.GetValue(o);
|
||||
if (e != null) e(t1);
|
||||
}), new { action = action, arg1 = arg1 });
|
||||
}
|
||||
|
||||
public void PostMainThreadAction<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2)
|
||||
{
|
||||
_mainThread.Post(new SendOrPostCallback((o) =>
|
||||
{
|
||||
Action<T1, T2> e = (Action<T1, T2>)o.GetType().GetProperty("action")?.GetValue(o);
|
||||
T1 t1 = (T1)o.GetType().GetProperty("arg1")?.GetValue(o);
|
||||
T2 t2 = (T2)o.GetType().GetProperty("arg2")?.GetValue(o);
|
||||
if (e != null) e(t1, t2);
|
||||
}), new { action = action, arg1 = arg1, arg2 = arg2 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2c6448e77af258458f5959d7e15c022
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class SocketClientMain : MonoBehaviour
|
||||
{
|
||||
private string ip = "127.0.0.1";
|
||||
private int port = 8080;
|
||||
private SocketClient _client;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_client = new SocketClient("192.168.31.67", 6854);
|
||||
|
||||
|
||||
|
||||
_client.Connect(() =>
|
||||
{
|
||||
UnityEngine.Debug.Log("连接成功");
|
||||
|
||||
// _client.DisConnect();
|
||||
}, () => { UnityEngine.Debug.Log("连接失败"); });
|
||||
}
|
||||
|
||||
public void StartConnect(string ip = "", int port = 0)
|
||||
{
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.A))
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes("我是测试数据");
|
||||
_client.Send((System.UInt16)SocketEvent.ScTest, bytes);
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(KeyCode.D))
|
||||
{
|
||||
_client.DisConnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 注意由于Unity编译器环境下,游戏开启/关闭只影响主线程的开关,游戏关闭回调时需要通过Close函数来关闭服务端/客户端的线程。
|
||||
if (_client != null)
|
||||
{
|
||||
_client.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 052b8595da3036e4a94eabc9a5a2b95b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class SocketServerMain : MonoBehaviour
|
||||
{
|
||||
SocketServer _server;
|
||||
private void Awake()
|
||||
{
|
||||
_server = new SocketServer("192.168.31.67", 6854);
|
||||
|
||||
_server.OnReceive += (client, data) =>
|
||||
{
|
||||
UnityEngine.Debug.LogFormat("[{0}]接收到数据>>>{1} {2}", client.LocalEndPoint.ToString(), (SocketEvent)data.Type, data.Buff.Length);
|
||||
|
||||
switch ((SocketEvent)data.Type)
|
||||
{
|
||||
case SocketEvent.ScTest:
|
||||
UnityEngine.Debug.LogFormat("接收到测试数据 >>> {0}", System.Text.Encoding.UTF8.GetString(data.Data));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.A))
|
||||
{
|
||||
// 踢出连接
|
||||
foreach (var item in _server.ClientInfoDic.Keys)
|
||||
{
|
||||
_server.KickOutAll();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 注意由于Unity编译器环境下,游戏开启/关闭只影响主线程的开关,游戏关闭回调时需要通过Close函数来关闭服务端/客户端的线程。
|
||||
if (_server != null)
|
||||
{
|
||||
_server.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b72333f2bc06a7428fb3c0eadeed0d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user