fix:1、更换项目,使用winter来创建

This commit is contained in:
2026-04-22 11:13:16 +08:00
parent 173cfb2dc9
commit 83ff9f71ad
7375 changed files with 209752 additions and 157557 deletions
+67
View File
@@ -0,0 +1,67 @@
# 🌐 Net 网络模块
适用于 Unity 的网络模块,包含网络检测、接口请求、Token 鉴权、服务器时间同步等功能。模块职责清晰、易扩展,使用前需引入通用模块。
------
## 📦 模块概览
| 模块 | 功能描述 |
| ---------------- | --------------------------------------------------- |
| `NetChecker` | 📶 检测网络是否可用,支持 Ping / HTTP 检查 |
| `NetCore<T>` | 📡 网络请求基类,支持加密和 Token 自动处理(需继承) |
| `TokenManager` | 🔐 Token 缓存 / 自动刷新 / 登录回退处理 |
| `ServerClock` | 🕒 同步服务器时间,避免设备本地误差 |
| `WebSocket 模块` | 依赖 nativewebsocket 插件 |
------
## ✅ 使用示例
### 1. 网络检测
```c#
StartCoroutine(NetChecker.Instance.WaitUntilNetworkConnected(10, 1f, success => {
if (success) Debug.Log("✅ 网络可用");
}));
```
------
### 2. 实现你的请求类(必须继承 NetCore)
```c#
public class NetKit : NetCore<NetKit> {
// 详情参考此类
}
```
------
### 3. 发起接口请求(在子类中)
```c#
NetKit.Instance.Post<TrackData>("/event/incrN", trackData,
response => {
callback?.Invoke(response.IsSuccess, response.Data);
});
```
------
### 4. Token 获取(自动缓存)
```c#
yield return TokenManager.Instance.GetToken(token => {
Debug.Log("当前 Token" + token);
});
```
------
### 5. 服务器时间同步
```c#
ServerClock.Init(serverUnixTime);
var now = ServerClock.GetCurrentServerTime();
```
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c97138411a25ced4f9dfa8842cb4f0da
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3196117d57be50e409688a93df57f045
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0915769f0cd1ba541832197a5a79b69b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac3aa62a90e10c34486be40387fbd2b9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,284 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using SGModule.Common.Base;
using SGModule.Common.Helper;
using UnityEngine;
using UnityEngine.Networking;
namespace SGModule.Net {
public enum ConnectionStatus {
Uninitialized, // 未初始化
Connected, // 已连接
Disconnected // 未连接
}
// 网络检测类
public class NetChecker : SingletonMonoBehaviour<NetChecker> {
private const float WebRequestInterval = 5f;
private const float PingInterval = 2;
// 可配置多个检测 URL
[SerializeField] private List<string> checkUrls = new() {
"https://www.baidu.com", // 国内 URL
"https://www.google.com" // 国外 URL
};
// 是否启用 Ping 检测
[SerializeField] private bool usePingCheck;
// 配置可用的 Ping 地址列表
private readonly List<string> _pingAddresses = new() {
"8.8.8.8", // Google DNS(适合全球)
"223.5.5.5", // 阿里云
"1.1.1.1" // Cloudflare DNS(全球可用)
};
private Coroutine _checkInternetCoroutine;
private float _checkInterval = WebRequestInterval;
private bool _isChecking;
private bool _lastConnected;
private string _preferredUrl; // 当前优先检测的 URL
/// <summary>
/// null = 未检测;true = 联网;false = 未联网
/// </summary>
private bool? IsConnected {
get;
set;
}
private void OnEnable() {
StartCheckingInternet();
}
private void OnDisable() {
StopCheckingInternet();
}
/// <summary>
/// 等待网络连通,自动处理初始化和总超时时间
/// </summary>
/// <param name="timeout">最多等待的总时间(秒)</param>
/// <param name="checkInterval">每次重试间隔</param>
/// <param name="callback">检测结果回调</param>
public IEnumerator WaitUntilNetworkConnected(float timeout, float checkInterval, Action<bool> callback) {
var elapsed = 0f;
// 阶段 1: 等待初始化完成
while (!IsConnected.HasValue && elapsed < timeout) {
yield return new WaitForSeconds(checkInterval);
elapsed += checkInterval;
}
if (!IsConnected.HasValue) {
Log.Net.Warning("网络检测初始化超时!");
callback(false);
yield break;
}
// 阶段 2: 等待网络恢复
while (IsConnected == false && elapsed < timeout) {
Log.Net.Warning($"网络异常,等待中... 已耗时: {elapsed:F1}s");
yield return new WaitForSeconds(checkInterval);
elapsed += checkInterval;
}
var success = IsConnected == true;
if (!success) {
Log.Net.Warning("网络连接等待超时!");
}
callback(success);
}
public ConnectionStatus GetConnectionStatus() {
return IsConnected switch {
null => ConnectionStatus.Uninitialized,
true => ConnectionStatus.Connected,
false => ConnectionStatus.Disconnected
};
}
public void Init() {
StartCheckingInternet();
}
private void StartCheckingInternet() {
_checkInterval = usePingCheck ? PingInterval : WebRequestInterval;
_checkInternetCoroutine ??= StartCoroutine(CheckInternetRoutine());
}
private void StopCheckingInternet() {
if (_checkInternetCoroutine != null) {
StopCoroutine(_checkInternetCoroutine);
_checkInternetCoroutine = null;
}
}
private IEnumerator CheckInternetRoutine() {
while (true) {
if (_isChecking) {
yield break; // 避免并发冲突
}
_isChecking = true;
if (QuickNetDeviceCheck()) {
// if (usePingCheck) {
yield return CheckWithPing();
// }
// else {
// yield return CheckWithUnityWebRequest();
// }
}
else {
IsConnected = false;
}
if (IsConnected.HasValue && _lastConnected != IsConnected.Value) {
Log.Net.Info($"网络状态变化: {IsConnected}");
_lastConnected = IsConnected.Value;
}
_isChecking = false;
yield return new WaitForSecondsRealtime(_checkInterval);
}
}
/// <summary>
/// 使用 Ping 检测网络连通性
/// </summary>
private IEnumerator CheckWithPing() {
var pings = _pingAddresses.Select(ip => new Ping(ip)).ToList();
var startTime = Time.realtimeSinceStartup;
var success = false;
// 等待所有 Ping 返回结果或超时
while (Time.realtimeSinceStartup - startTime < _checkInterval) {
foreach (var ping in pings) {
if (ping.isDone && ping.time >= 0) {
success = true;
break;
}
}
if (success) {
break;
}
yield return new WaitForSecondsRealtime(0.1f);
}
// 如果有任意一个 Ping 成功,网络即为可用
IsConnected = success;
}
/// <summary>
/// 使用 UnityWebRequest 检测网络连通性
/// </summary>
private IEnumerator CheckWithUnityWebRequest() {
if (!string.IsNullOrEmpty(_preferredUrl)) {
var request = UnityWebRequest.Get(_preferredUrl);
request.timeout = (int) _checkInterval;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success) {
IsConnected = true;
request.Dispose();
yield break;
}
Log.Net.Warning($"CheckNet 首选URL请求失败: {_preferredUrl}, 错误: {request.error}");
_preferredUrl = null; // 失败则清除
request.Dispose();
}
// fallback:尝试所有 URL
var ops = new List<UnityWebRequestAsyncOperation>();
var requests = new List<UnityWebRequest>();
foreach (var url in checkUrls) {
var req = UnityWebRequest.Get(url);
req.timeout = (int) _checkInterval;
ops.Add(req.SendWebRequest());
requests.Add(req);
}
yield return StartCoroutine(WaitForAnyRequest(ops));
foreach (var req in requests) {
req.Dispose();
}
}
// 等待任意一个请求完成并返回
private IEnumerator WaitForAnyRequest(List<UnityWebRequestAsyncOperation> ops) {
while (ops.Any(op => !op.isDone)) {
if (ops.Any(op => op.isDone && op.webRequest.result == UnityWebRequest.Result.Success)) {
break;
}
yield return null;
}
var successOp = ops.FirstOrDefault(op => op.webRequest.result == UnityWebRequest.Result.Success);
if (successOp == null) {
foreach (var op in ops.Where(op => op.isDone)) {
Log.Net.Warning($"请求失败: {op.webRequest.url}, 错误: {op.webRequest.error}");
}
}
IsConnected = successOp != null;
_preferredUrl = successOp?.webRequest.url;
}
// 添加或删除检查 URL
public void AddCheckUrl(string url) {
if (!checkUrls.Contains(url)) {
checkUrls.Add(url);
}
}
public void RemoveCheckUrl(string url) {
if (checkUrls.Contains(url)) {
checkUrls.Remove(url);
}
}
public void AddPingTarget(string ip) {
if (!_pingAddresses.Contains(ip)) {
_pingAddresses.Add(ip);
}
}
public void RemovePingTarget(string ip) {
if (_pingAddresses.Contains(ip)) {
_pingAddresses.Remove(ip);
}
}
// 切换是否使用 Ping 检测
public void SetUsePingCheck(bool enablePing) {
usePingCheck = enablePing;
#if UNITY_WEBGL
usePingCheck = false;
#endif
_checkInterval = usePingCheck ? PingInterval : WebRequestInterval;
}
private bool QuickNetDeviceCheck() {
if (Application.isEditor) {
return true;
}
return Application.internetReachability != NetworkReachability.NotReachable;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 410b1547c3cc4b6496884bc02a4b8498
timeCreated: 1731574865
@@ -0,0 +1,272 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using SGModule.Common.Base;
using SGModule.Common.Extensions;
using SGModule.Common.Helper;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
namespace SGModule.Net {
// 网络请求核心类
public abstract class NetCore<TC> : SingletonMonoBehaviour<TC> where TC : MonoBehaviour {
private const int Timeout = 15;
private NetCoreModel _config;
private bool _isInit;
private int _requestCounter;
private string _requestHost;
private new void Awake() {
InternalInit();
}
protected abstract string Encrypt(string text, string key);
protected abstract string Decrypt(string text, string key);
protected abstract TokenManager.RefreshTokenDelegate RefreshTokenHandler();
protected abstract TokenManager.ReauthenticateDelegate ReauthenticateHandler();
protected abstract NetCoreModel ProvideConfig();
private void InternalInit() {
NetChecker.Instance.Init();
TokenManager.Instance.InitHandlers(RefreshTokenHandler(), ReauthenticateHandler());
_config = ProvideConfig();
if (_config == null) {
Log.Net.Error("网络配置为空,请检查!!!!");
return;
}
_isInit = true;
_requestHost = _config.RequestHost;
Log.Net.Info($"Host is: {_config.RequestHost}");
if (_config.ConnMode == ConnectionMode.WebSocket) {
WebSocketService.Instance.Init(GetWsUrl());
}
}
private string GetApiUrl() {
return $"https://{_requestHost}/api/";
}
private string GetWsUrl() {
return $"wss://{_requestHost}/ws/";
}
/// <summary>
/// Post请求(异步)
/// </summary>
/// <param name="path">请求路径</param>
/// <param name="requestData">请求参数</param>
/// <param name="onCompleted">请求回调</param>
/// <param name="withToken">是否带Token</param>
/// <param name="headers">额外的请求头</param>
protected IEnumerator PostAsync<T>(string path, object requestData = null, UnityAction<ResponseData<T>> onCompleted = null, bool withToken = true, Dictionary<string, string> headers = null) {
var done = false;
Post<T>(path, requestData, response => {
onCompleted?.Invoke(response);
done = true;
}, withToken, true, headers);
yield return new WaitUntil(() => done);
}
/// <summary>
/// Post请求(泛型)
/// </summary>
/// <param name="path">请求路径</param>
/// <param name="requestData">请求参数</param>
/// <param name="onCompleted">请求回调</param>
/// <param name="withToken">是否带Token</param>
/// <param name="useCoroutine">是否使用协程</param>
/// <param name="headers">额外的请求头</param>
/// <typeparam name="T">返回数据中Data的类型</typeparam>
protected void Post<T>(string path, object requestData = null, UnityAction<ResponseData<T>> onCompleted = null, bool withToken = true, bool useCoroutine = true, Dictionary<string, string> headers = null) {
if (useCoroutine) {
StartCoroutine(PostInternal(path, requestData, onCompleted, withToken, headers));
}
else {
SendWebRequest(path, requestData, withToken, headers);
}
}
/// <summary>
/// 发出请求 所有的请求最终都会从这里发出
/// </summary>
/// <param name="path">请求路径</param>
/// <param name="requestData">请求参数</param>
/// <param name="onCompleted">请求回调</param>
/// <param name="withToken">是否带Token</param>
/// <param name="headers">额外的请求头</param>
/// <typeparam name="T">返回数据中Data的类型</typeparam>
/// <returns></returns>
private IEnumerator PostInternal<T>(string path, object requestData, UnityAction<ResponseData<T>> onCompleted, bool withToken = true, Dictionary<string, string> headers = null) {
var response = new ResponseData<T>();
if (!_isInit) {
Log.Net.Error("NetworkKit模块未完成初始化 请检查!!! ");
response.Code = -1;
response.Msg = "NetworkKit module has not been initialized yet";
onCompleted?.Invoke(response);
yield break;
}
// 1. 网络检查
var isNetworkOk = false;
yield return NetChecker.Instance.WaitUntilNetworkConnected(15, 1f, result => isNetworkOk = result);
if (!isNetworkOk) {
response.Code = -1;
response.Msg = "Network status abnormal.";
onCompleted?.Invoke(response);
yield break;
}
// 2. Token 处理(根据 withToken 参数)
if (withToken) {
string token = null;
yield return TokenManager.Instance.GetToken(result => token = result);
if (string.IsNullOrEmpty(token)) {
response.Code = -1;
response.Msg = "Token acquisition failed.";
onCompleted?.Invoke(response);
yield break;
}
// 合并原有 header
headers ??= new Dictionary<string, string>();
headers["x-token"] = token;
}
yield return SendRequest(path, requestData, onCompleted, headers);
}
private IEnumerator SendRequest<T>(
string path,
object requestData,
UnityAction<ResponseData<T>> onCompleted = null,
Dictionary<string, string> headers = null) {
var requestId = GetNextRequestId();
var requestJson = SerializeHelper.ToJsonIndented(requestData ?? new object());
var unityWebRequest = CreatePostRequest(path, requestJson, headers);
Log.Net.Info($"[Req:{requestId}] | Path: {path} | Body: {requestJson}");
yield return unityWebRequest.SendWebRequest(); //发送请求
var response = new ResponseData<T>();
if (unityWebRequest.result is not UnityWebRequest.Result.Success) {
Log.Net.Error($"[Resp:{requestId}] | Path: {path} | Error: {unityWebRequest.error}");
response.Code = -1;
response.Msg = unityWebRequest.error;
}
else {
var receiveContent = unityWebRequest.downloadHandler.text;
if (!receiveContent.IsNullOrWhiteSpace()) {
receiveContent = Decrypt(UnquoteString(receiveContent), _requestHost);
}
var raw = SerializeHelper.ToObject<ResponseData<object>>(receiveContent);
if (raw?.Code == 0) {
Log.Net.Info($"[Resp:{requestId}] | Path: {path} | Result: {receiveContent}");
// 处理字符串类型的情况
if (typeof(T) == typeof(string)) {
response.Data = (T) (object) (raw.Data?.ToString() ?? "");
}
else {
response.Data = SerializeHelper.ToObject<T>(JsonConvert.SerializeObject(raw.Data));
}
}
else {
Log.Net.Warning($"[Resp:{requestId}] | Path: {path} | Result: {receiveContent}");
response.Code = raw?.Code ?? -2;
response.Msg = raw?.Msg ?? "未知错误";
}
}
onCompleted?.Invoke(response);
unityWebRequest.Dispose();
}
private UnityWebRequest CreatePostRequest(string path, string requestJson, Dictionary<string, string> headers = null) {
path = Encrypt(path, _requestHost);
requestJson = Encrypt(requestJson, _requestHost);
var fullUrl = GetApiUrl() + path;
var request = new UnityWebRequest(fullUrl, UnityWebRequest.kHttpVerbPOST) {
uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(requestJson)),
downloadHandler = new DownloadHandlerBuffer(),
timeout = Timeout
};
SetRequestHeaders(request, headers);
return request;
}
private void SetRequestHeaders(UnityWebRequest request, Dictionary<string, string> headers) {
request.SetRequestHeader("Content-Type", "application/json;charset=utf-8");
if (headers == null) {
return;
}
foreach (var kvp in headers.Where(kvp => kvp.Value != null)) {
request.SetRequestHeader(kvp.Key, kvp.Value);
}
}
private int GetNextRequestId() {
return Interlocked.Increment(ref _requestCounter);
}
// 去除双引号(如果存在)
private static string UnquoteString(string input) {
if (!string.IsNullOrEmpty(input) && input.Length >= 2 &&
input[0] == '"' && input[input.Length - 1] == '"') {
return input.Substring(1, input.Length - 2);
}
return input;
}
private void SendWebRequest(string url, object requestData = null, bool withToken = false, Dictionary<string, string> header = null) {
if (withToken) {
// 合并原有 header
header ??= new Dictionary<string, string>();
header["x-token"] = TokenManager.Instance.CachedToken;
}
var requestId = GetNextRequestId();
var requestJson = SerializeHelper.ToJsonIndented(requestData ?? new object());
var unityWebRequest = CreatePostRequest(url, requestJson, header);
Log.Net.Info($"[Req:{requestId}] | Path: {url} | Body: {requestJson}");
unityWebRequest.SendWebRequest(); //发送请求
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4495c7a990814f9e977dc6853d262a5f
timeCreated: 1731305623
@@ -0,0 +1,20 @@
using System;
namespace SGModule.Net {
// 服务器时钟管理类
public static class ServerClock {
private static long _timeDifference; // 本地与服务器的时间偏差
public static void Init(long serverTime) {
_timeDifference = serverTime - Now();
}
public static long GetCurrentServerTime() {
return Now() + _timeDifference;
}
private static long Now() {
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 59a4eda105634775a948f64a9f6631d7
timeCreated: 1750225342
@@ -0,0 +1,204 @@
using System.Collections;
using System.Collections.Generic;
using SGModule.Common.Base;
using SGModule.Common.Extensions;
using SGModule.Common.Helper;
using UnityEngine;
using UnityEngine.Events;
namespace SGModule.Net {
// Token管理类
public class TokenManager : SingletonMonoBehaviour<TokenManager> {
public delegate IEnumerator ReauthenticateDelegate(UnityAction<string, long> onCompleted);
public delegate IEnumerator RefreshTokenDelegate(UnityAction<string, long> onCompleted);
private const string JarvisTokenKey = "JarvisToken";
private const string JarvisTokenExpiresAtKey = "JarvisTokenExpiresAt";
private const float MinRefreshInterval = 10f; // 刷新间隔秒数限制
private const int MaxTokenRecordCount = 10; // 最多记录最近token数
private readonly List<string> _tokenRecord = new();
private long _cachedExpiresAt;
private string _cachedToken;
private bool _isReauthenticating;
private bool _isRefreshing;
private float _lastRefreshTime;
private ReauthenticateDelegate _reauthenticateHandler;
private RefreshTokenDelegate _refreshTokenHandler;
public string CachedToken {
get {
if (string.IsNullOrEmpty(_cachedToken)) {
_cachedToken = PlayerPrefs.GetString(JarvisTokenKey, null);
}
return _cachedToken;
}
set {
_cachedToken = value;
if (!string.IsNullOrEmpty(value)) {
PlayerPrefs.SetString(JarvisTokenKey, value);
}
else {
PlayerPrefs.DeleteKey(JarvisTokenKey);
}
}
}
public long CachedExpiresAt {
get {
if (_cachedExpiresAt == 0) {
var expiresStr = PlayerPrefs.GetString(JarvisTokenExpiresAtKey, "0");
if (!long.TryParse(expiresStr, out _cachedExpiresAt)) {
_cachedExpiresAt = 0;
}
}
return _cachedExpiresAt;
}
set {
_cachedExpiresAt = value;
PlayerPrefs.SetString(JarvisTokenExpiresAtKey, value.ToString());
}
}
private long RemainingTokenSeconds => CachedExpiresAt - ServerClock.GetCurrentServerTime();
public void InitHandlers(RefreshTokenDelegate refreshTokenHandler, ReauthenticateDelegate reauthenticateHandler) {
_refreshTokenHandler = refreshTokenHandler;
_reauthenticateHandler = reauthenticateHandler;
}
/// <summary>
/// 获取有效Token,自动处理刷新和重新登录
/// </summary>
public IEnumerator GetToken(UnityAction<string> callback) {
// 若正在重新登录,则等待其完成,避免刷新或登录并发
yield return WaitForReauthenticationComplete();
if (_isReauthenticating) {
callback?.Invoke(null);
yield break;
}
if (IsTokenValid()) {
callback?.Invoke(CachedToken); // 优先返回现有有效 Token
// 如果快过期,则后台触发刷新,不等待
if (IsTokenExpiringSoon() && ShouldStartRefresh()) {
_lastRefreshTime = Time.realtimeSinceStartup;
StartCoroutine(RefreshTokenRoutine()); // 不阻塞当前流程
}
}
else {
// token 已过期,必须重新登录
yield return ReauthenticateRoutine(success => {
callback?.Invoke(success ? CachedToken : null);
});
}
}
private IEnumerator WaitForReauthenticationComplete() {
var waitCount = 0;
while (_isReauthenticating && waitCount < 6) {
yield return new WaitForSeconds(0.5f);
waitCount++;
}
}
private IEnumerator RefreshTokenRoutine() {
if (_isRefreshing || _refreshTokenHandler == null) {
if (_refreshTokenHandler == null) {
Log.Net.Error("未设置 RefreshTokenHandler 委托,请先调用 InitHandlers 进行注入!");
}
yield break;
}
_isRefreshing = true;
var retryCount = 0;
var refreshSuccess = false;
while (!refreshSuccess && retryCount < 3) {
retryCount++;
Log.Net.Info($"Token快过期了, 尝试刷新Token, 次数: {retryCount}");
yield return _refreshTokenHandler((token, expiresAt) => {
refreshSuccess = !token.IsNullOrWhiteSpace();
if (refreshSuccess) {
UpdateTokenCache(token, expiresAt);
}
});
if (!refreshSuccess) {
yield return new WaitForSeconds(1f);
}
}
if (!refreshSuccess) {
Log.Net.Error("Token刷新失败");
}
_isRefreshing = false;
}
private IEnumerator ReauthenticateRoutine(UnityAction<bool> onCompleted) {
if (_isReauthenticating || _reauthenticateHandler == null) {
if (_reauthenticateHandler == null) {
Log.Net.Error("未设置 ReauthenticateHandler 委托,请先调用 InitHandlers 进行注入!");
}
onCompleted?.Invoke(false);
yield break;
}
_isReauthenticating = true;
Log.Net.Error("Token过期,开始重新登录流程");
yield return _reauthenticateHandler((token, expiresAt) => {
var success = !token.IsNullOrWhiteSpace();
if (success) {
UpdateTokenCache(token, expiresAt);
}
onCompleted?.Invoke(success);
});
_isReauthenticating = false;
}
private bool IsTokenValid() {
return !string.IsNullOrEmpty(CachedToken) && RemainingTokenSeconds > 0;
}
// 小于1小时(3600秒)有效期,认为快过期需要刷新
private bool IsTokenExpiringSoon() {
return RemainingTokenSeconds < 3600;
}
public void UpdateTokenCache(string token, long expiresAt) {
CachedToken = token;
CachedExpiresAt = expiresAt;
_tokenRecord.Add(token);
if (_tokenRecord.Count > MaxTokenRecordCount) {
_tokenRecord.RemoveAt(0);
}
}
private bool ShouldStartRefresh() {
return !_isRefreshing && Time.realtimeSinceStartup - _lastRefreshTime > MinRefreshInterval;
}
/// <summary>
/// 获取Token记录(测试功能用)
/// </summary>
public List<string> GetTokenRecord() {
return _tokenRecord;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9f7c53b24f0542499942172b93150c4b
timeCreated: 1750153672
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 92442bedfec46f64e99ebfad2afc096c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,6 @@
namespace SGModule.Net {
public class NetCoreModel {
public ConnectionMode ConnMode;
public string RequestHost;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f1af59db74324485a1ad287fae41f4a7
timeCreated: 1750301716
@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace SGModule.Net {
public class ResponseData<T> {
[JsonProperty("code")] public int Code;
[JsonProperty("data")] public T Data;
[JsonProperty("msg")] public string Msg;
public bool IsSuccess => Code == 0;
public void Deconstruct(out bool isSuccess, out T data) {
isSuccess = IsSuccess;
data = Data;
}
}
public class RequestTokenData {
[JsonProperty("expires_at")] public long ExpiresAt;
[JsonProperty("token")] public string Token;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 367023343f32423a945b22cd9e10954d
timeCreated: 1731310905
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1927ccee3f8f484eb4cb2c12246b1021
timeCreated: 1736219769
@@ -0,0 +1,36 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class ResponseData {
[JsonProperty("cmd")]
public int Cmd {
get;
set;
}
[JsonProperty("body")]
public Body Body {
get;
set;
}
}
public class Body {
[JsonProperty("code")]
public int Code {
get;
set;
}
[JsonProperty("data")]
public JToken Data {
get;
set;
} // 使用 JToken 来处理不同的 data 结构
[JsonProperty("msg")]
public string Msg {
get;
set;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d2969c12b6014b5c935b92b050e7f19a
timeCreated: 1736158044
@@ -0,0 +1,4 @@
internal class WebSocketData {
public int cmd;
public string token;
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c7cbb19f802743be9768ee60d9eaf110
timeCreated: 1736221524
@@ -0,0 +1,6 @@
namespace SGModule.Net {
public enum WebSocketEvent {
BindToken = 4097,
SendHeartbeat = 4098
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6ab1b29ab34f456b8398d41adcb955c3
timeCreated: 1736221044
@@ -0,0 +1,221 @@
using System;
using System.Text;
using System.Threading.Tasks;
using NativeWebSocket;
using Newtonsoft.Json;
using SGModule.Common.Base;
using SGModule.Common.Helper;
using UnityEngine;
namespace SGModule.Net {
public class WebSocketService : SingletonMonoBehaviour<WebSocketService> {
private const float OffLineDecisionTime = 5f;
private bool _bindToken;
private float _offlineDuration;
private string _url;
private WebSocket _webSocket;
private void Update() {
#if !UNITY_WEBGL || UNITY_EDITOR
if (_webSocket != null) {
_webSocket.DispatchMessageQueue();
}
#endif
if (IsInitComplete) {
// 网络状态检测
if (NetChecker.Instance.GetConnectionStatus() == ConnectionStatus.Connected) {
_offlineDuration = 0;
if (_webSocket == null || _webSocket.State == WebSocketState.Closed) {
Log.Net.Warning("网络恢复 重连");
TryReconnect();
}
}
else {
_offlineDuration += Time.deltaTime;
if (_offlineDuration > OffLineDecisionTime && _webSocket != null && _webSocket.State == WebSocketState.Open) {
Log.Net.Warning("断网 主动结束连接");
_ = Disconnect();
}
}
}
}
private async void OnApplicationQuit() {
await Disconnect();
}
private void InitGmTool() {
}
/// <summary>
/// 初始化
/// </summary>
/// <param name="url"></param>
public async void Init(string url) {
InitGmTool();
NetChecker.Instance.SetUsePingCheck(true);
Log.Net.Info($"Initializing WebSocket service. Url: {url}");
_url = url;
OnOpenAction += OnOpen;
OnMessageAction += OnMessage;
OnErrorAction += OnError;
OnCloseAction += OnClose;
await Connect();
}
private void Send(object data) {
if (_webSocket == null) {
Log.Net.Warning("webSocket is null");
return;
}
if (_webSocket.State != WebSocketState.Open) {
Log.Net.Warning("webSocket.State != Open");
return;
}
var jsonString = JsonConvert.SerializeObject(data);
var byteArray = Encoding.UTF8.GetBytes(jsonString);
_webSocket.Send(byteArray);
}
/// <summary>
/// 断开连接
/// </summary>
public async Task Disconnect() {
if (_webSocket != null) {
var webSocket = _webSocket;
_webSocket = null;
await webSocket.Close();
}
}
#region
// 事件回调
public event Action OnOpenAction;
public event Action<string> OnMessageAction;
public event Action<WebSocketCloseCode> OnCloseAction;
public event Action<string> OnErrorAction;
/// <summary>
/// 初始化并尝试连接
/// </summary>
private async Task Connect() {
if (_webSocket != null && (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting)) {
Log.Net.Warning("WebSocket is already connected or connecting.");
return;
}
_webSocket = new WebSocket(_url);
// 绑定事件
_webSocket.OnOpen += () => {
Log.Net.Info("WebSocket connected.");
OnOpenAction?.Invoke();
};
_webSocket.OnMessage += bytes => {
var message = Encoding.UTF8.GetString(bytes);
Log.Net.Info($"Message received: {message}");
OnMessageAction?.Invoke(message);
};
_webSocket.OnClose += code => {
Log.Net.Info($"WebSocket disconnected with code: {code}");
OnCloseAction?.Invoke(code);
};
_webSocket.OnError += error => {
Log.Net.Error($"WebSocket error: {error}");
OnErrorAction?.Invoke(error);
};
// 开始连接
try {
await _webSocket.Connect();
}
catch (Exception ex) {
Log.Net.Error($"WebSocket connection failed: {ex.Message}");
TryReconnect();
}
}
private void OnOpen() {
IsInitComplete = true;
// 重连成功后绑定 Token
if (_bindToken) {
BindToken(TokenManager.Instance.CachedToken);
}
}
private void OnClose(WebSocketCloseCode closeCode) {
}
private void OnMessage(string message) {
var responseData = WebSocketTools.DeserializeResponse(message);
WebSocketTools.ProcessResponseData(responseData);
}
private void OnError(string errorMsg) {
}
/// <summary>
/// 尝试重连
/// </summary>
private async void TryReconnect() {
var maxRetries = 5; // 最大重连次数
var attempt = 0;
while (attempt < maxRetries) {
attempt++;
Log.Net.Info($"Attempting to reconnect... (Attempt {attempt}/{maxRetries})");
try {
await Connect();
//以下或许不会执行 这个
Log.Net.Info("Reconnected successfully.");
return; // 重连成功
}
catch (Exception ex) {
Log.Net.Error($"Reconnection attempt {attempt} failed: {ex.Message}");
await Task.Delay(2000); // 等待一段时间再尝试
}
}
Log.Net.Error("Max reconnection attempts reached. Connection failed.");
}
#endregion
#region
/// <summary>
/// 绑定Token
/// </summary>
public void BindToken(string token) {
Send(new WebSocketData {
cmd = (int) WebSocketEvent.BindToken,
token = token
});
_bindToken = true;
}
/// <summary>
/// 发送心跳
/// </summary>
public void SendHeartbeat() {
Send(new WebSocketData {
cmd = (int) WebSocketEvent.SendHeartbeat
});
}
#endregion
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cdcb270d0b1e4b3fbc8983b5d1088660
timeCreated: 1736219848
@@ -0,0 +1,70 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SGModule.Common.Helper;
public class WebSocketTools {
// 反序列化函数,返回 ResponseData 对象
public static ResponseData DeserializeResponse(string jsonMessage) {
try {
return JsonConvert.DeserializeObject<ResponseData>(jsonMessage);
}
catch (JsonException ex) {
Log.Net.Error($"Failed to deserialize response: {ex.Message}");
return null;
}
}
// 处理数据函数
public static void ProcessResponseData(ResponseData responseData) {
// 处理请求成功的情况
if (responseData.Body.Code == 0) {
Log.Net.Info("Request succeeded");
HandleData(responseData.Body.Data); // 不传入类型时,先默认不解析
}
else {
Log.Net.Error($"Request failed: {responseData.Body.Msg}");
}
}
// 泛型版本的 HandleData,允许外部传入一个类型
public static void HandleData(JToken data) {
// 检查 data 是否为空对象
if (data.Type == JTokenType.Object && !data.HasValues) {
Log.Net.Info("Data is empty");
// 可根据需要进一步处理空数据的情况
}
else {
// 如果 data 包含有效数据,打印或进一步处理
Log.Net.Info("Received data: " + data);
ProcessValidData(data);
}
}
// 泛型版本的 HandleData,传入类型进行解析
public static void HandleData<T>(JToken data) {
if (data != null) {
try {
var parsedData = data.ToObject<T>(); // 将 data 解析为指定类型
Log.Net.Info("Parsed data: " + JsonConvert.SerializeObject(parsedData));
ProcessValidData(parsedData); // 进一步处理解析后的数据
}
catch (JsonException ex) {
Log.Net.Error($"Failed to parse data to type {typeof(T)}: {ex.Message}");
}
}
else {
Log.Net.Warning("Data is null, cannot parse.");
}
}
// 处理解析后的 data 类型
public static void ProcessValidData<T>(T parsedData) {
if (parsedData != null) {
// 在这里可以对解析后的 data 进行具体处理
Log.Net.Info("Successfully processed data: " + parsedData);
}
else {
Log.Net.Warning("Parsed data is null, cannot process.");
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 69c922dca7314799ade451fd4f2fb6fd
timeCreated: 1736159321
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c7f0b7a09c76e4c419c7aee3dd9bfefd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6183796adc7be374c86975de4519da5c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,848 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.WebSockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AOT;
using System.Runtime.InteropServices;
using UnityEngine;
using System.Collections;
public class MainThreadUtil : MonoBehaviour
{
public static MainThreadUtil Instance { get; private set; }
public static SynchronizationContext synchronizationContext { get; private set; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void Setup()
{
Instance = new GameObject("MainThreadUtil")
.AddComponent<MainThreadUtil>();
synchronizationContext = SynchronizationContext.Current;
}
public static void Run(IEnumerator waitForUpdate)
{
synchronizationContext.Post(_ => Instance.StartCoroutine(
waitForUpdate), null);
}
void Awake()
{
gameObject.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad(gameObject);
}
}
public class WaitForUpdate : CustomYieldInstruction
{
public override bool keepWaiting
{
get { return false; }
}
public MainThreadAwaiter GetAwaiter()
{
var awaiter = new MainThreadAwaiter();
MainThreadUtil.Run(CoroutineWrapper(this, awaiter));
return awaiter;
}
public class MainThreadAwaiter : INotifyCompletion
{
Action continuation;
public bool IsCompleted { get; set; }
public void GetResult() { }
public void Complete()
{
IsCompleted = true;
continuation?.Invoke();
}
void INotifyCompletion.OnCompleted(Action continuation)
{
this.continuation = continuation;
}
}
public static IEnumerator CoroutineWrapper(IEnumerator theWorker, MainThreadAwaiter awaiter)
{
yield return theWorker;
awaiter.Complete();
}
}
namespace NativeWebSocket
{
public delegate void WebSocketOpenEventHandler();
public delegate void WebSocketMessageEventHandler(byte[] data);
public delegate void WebSocketErrorEventHandler(string errorMsg);
public delegate void WebSocketCloseEventHandler(WebSocketCloseCode closeCode);
public enum WebSocketCloseCode
{
/* Do NOT use NotSet - it's only purpose is to indicate that the close code cannot be parsed. */
NotSet = 0,
Normal = 1000,
Away = 1001,
ProtocolError = 1002,
UnsupportedData = 1003,
Undefined = 1004,
NoStatus = 1005,
Abnormal = 1006,
InvalidData = 1007,
PolicyViolation = 1008,
TooBig = 1009,
MandatoryExtension = 1010,
ServerError = 1011,
TlsHandshakeFailure = 1015
}
public enum WebSocketState
{
Connecting,
Open,
Closing,
Closed
}
public interface IWebSocket
{
event WebSocketOpenEventHandler OnOpen;
event WebSocketMessageEventHandler OnMessage;
event WebSocketErrorEventHandler OnError;
event WebSocketCloseEventHandler OnClose;
WebSocketState State { get; }
}
public static class WebSocketHelpers
{
public static WebSocketCloseCode ParseCloseCodeEnum(int closeCode)
{
if (WebSocketCloseCode.IsDefined(typeof(WebSocketCloseCode), closeCode))
{
return (WebSocketCloseCode)closeCode;
}
else
{
return WebSocketCloseCode.Undefined;
}
}
public static WebSocketException GetErrorMessageFromCode(int errorCode, Exception inner)
{
switch (errorCode)
{
case -1:
return new WebSocketUnexpectedException("WebSocket instance not found.", inner);
case -2:
return new WebSocketInvalidStateException("WebSocket is already connected or in connecting state.", inner);
case -3:
return new WebSocketInvalidStateException("WebSocket is not connected.", inner);
case -4:
return new WebSocketInvalidStateException("WebSocket is already closing.", inner);
case -5:
return new WebSocketInvalidStateException("WebSocket is already closed.", inner);
case -6:
return new WebSocketInvalidStateException("WebSocket is not in open state.", inner);
case -7:
return new WebSocketInvalidArgumentException("Cannot close WebSocket. An invalid code was specified or reason is too long.", inner);
default:
return new WebSocketUnexpectedException("Unknown error.", inner);
}
}
}
public class WebSocketException : Exception
{
public WebSocketException() { }
public WebSocketException(string message) : base(message) { }
public WebSocketException(string message, Exception inner) : base(message, inner) { }
}
public class WebSocketUnexpectedException : WebSocketException
{
public WebSocketUnexpectedException() { }
public WebSocketUnexpectedException(string message) : base(message) { }
public WebSocketUnexpectedException(string message, Exception inner) : base(message, inner) { }
}
public class WebSocketInvalidArgumentException : WebSocketException
{
public WebSocketInvalidArgumentException() { }
public WebSocketInvalidArgumentException(string message) : base(message) { }
public WebSocketInvalidArgumentException(string message, Exception inner) : base(message, inner) { }
}
public class WebSocketInvalidStateException : WebSocketException
{
public WebSocketInvalidStateException() { }
public WebSocketInvalidStateException(string message) : base(message) { }
public WebSocketInvalidStateException(string message, Exception inner) : base(message, inner) { }
}
public class WaitForBackgroundThread
{
public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
{
return Task.Run(() => { }).ConfigureAwait(false).GetAwaiter();
}
}
#if UNITY_WEBGL && !UNITY_EDITOR
/// <summary>
/// WebSocket class bound to JSLIB.
/// </summary>
public class WebSocket : IWebSocket {
/* WebSocket JSLIB functions */
[DllImport ("__Internal")]
public static extern int WebSocketConnect (int instanceId);
[DllImport ("__Internal")]
public static extern int WebSocketClose (int instanceId, int code, string reason);
[DllImport ("__Internal")]
public static extern int WebSocketSend (int instanceId, byte[] dataPtr, int dataLength);
[DllImport ("__Internal")]
public static extern int WebSocketSendText (int instanceId, string message);
[DllImport ("__Internal")]
public static extern int WebSocketGetState (int instanceId);
protected int instanceId;
public event WebSocketOpenEventHandler OnOpen;
public event WebSocketMessageEventHandler OnMessage;
public event WebSocketErrorEventHandler OnError;
public event WebSocketCloseEventHandler OnClose;
public WebSocket (string url, Dictionary<string, string> headers = null) {
if (!WebSocketFactory.isInitialized) {
WebSocketFactory.Initialize ();
}
int instanceId = WebSocketFactory.WebSocketAllocate (url);
WebSocketFactory.instances.Add (instanceId, this);
this.instanceId = instanceId;
}
public WebSocket (string url, string subprotocol, Dictionary<string, string> headers = null) {
if (!WebSocketFactory.isInitialized) {
WebSocketFactory.Initialize ();
}
int instanceId = WebSocketFactory.WebSocketAllocate (url);
WebSocketFactory.instances.Add (instanceId, this);
WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol);
this.instanceId = instanceId;
}
public WebSocket (string url, List<string> subprotocols, Dictionary<string, string> headers = null) {
if (!WebSocketFactory.isInitialized) {
WebSocketFactory.Initialize ();
}
int instanceId = WebSocketFactory.WebSocketAllocate (url);
WebSocketFactory.instances.Add (instanceId, this);
foreach (string subprotocol in subprotocols) {
WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol);
}
this.instanceId = instanceId;
}
~WebSocket () {
WebSocketFactory.HandleInstanceDestroy (this.instanceId);
}
public int GetInstanceId () {
return this.instanceId;
}
public Task Connect () {
int ret = WebSocketConnect (this.instanceId);
if (ret < 0)
throw WebSocketHelpers.GetErrorMessageFromCode (ret, null);
return Task.CompletedTask;
}
public void CancelConnection () {
if (State == WebSocketState.Open)
Close (WebSocketCloseCode.Abnormal);
}
public Task Close (WebSocketCloseCode code = WebSocketCloseCode.Normal, string reason = null) {
int ret = WebSocketClose (this.instanceId, (int) code, reason);
if (ret < 0)
throw WebSocketHelpers.GetErrorMessageFromCode (ret, null);
return Task.CompletedTask;
}
public Task Send (byte[] data) {
int ret = WebSocketSend (this.instanceId, data, data.Length);
if (ret < 0)
throw WebSocketHelpers.GetErrorMessageFromCode (ret, null);
return Task.CompletedTask;
}
public Task SendText (string message) {
int ret = WebSocketSendText (this.instanceId, message);
if (ret < 0)
throw WebSocketHelpers.GetErrorMessageFromCode (ret, null);
return Task.CompletedTask;
}
public WebSocketState State {
get {
int state = WebSocketGetState (this.instanceId);
if (state < 0)
throw WebSocketHelpers.GetErrorMessageFromCode (state, null);
switch (state) {
case 0:
return WebSocketState.Connecting;
case 1:
return WebSocketState.Open;
case 2:
return WebSocketState.Closing;
case 3:
return WebSocketState.Closed;
default:
return WebSocketState.Closed;
}
}
}
public void DelegateOnOpenEvent () {
this.OnOpen?.Invoke ();
}
public void DelegateOnMessageEvent (byte[] data) {
this.OnMessage?.Invoke (data);
}
public void DelegateOnErrorEvent (string errorMsg) {
this.OnError?.Invoke (errorMsg);
}
public void DelegateOnCloseEvent (int closeCode) {
this.OnClose?.Invoke (WebSocketHelpers.ParseCloseCodeEnum (closeCode));
}
}
#else
public class WebSocket : IWebSocket
{
public event WebSocketOpenEventHandler OnOpen;
public event WebSocketMessageEventHandler OnMessage;
public event WebSocketErrorEventHandler OnError;
public event WebSocketCloseEventHandler OnClose;
private Uri uri;
private Dictionary<string, string> headers;
private List<string> subprotocols;
private ClientWebSocket m_Socket = new ClientWebSocket();
private CancellationTokenSource m_TokenSource;
private CancellationToken m_CancellationToken;
private readonly object OutgoingMessageLock = new object();
private readonly object IncomingMessageLock = new object();
private bool isSending = false;
private List<ArraySegment<byte>> sendBytesQueue = new List<ArraySegment<byte>>();
private List<ArraySegment<byte>> sendTextQueue = new List<ArraySegment<byte>>();
public WebSocket(string url, Dictionary<string, string> headers = null)
{
uri = new Uri(url);
if (headers == null)
{
this.headers = new Dictionary<string, string>();
}
else
{
this.headers = headers;
}
subprotocols = new List<string>();
string protocol = uri.Scheme;
if (!protocol.Equals("ws") && !protocol.Equals("wss"))
throw new ArgumentException("Unsupported protocol: " + protocol);
}
public WebSocket(string url, string subprotocol, Dictionary<string, string> headers = null)
{
uri = new Uri(url);
if (headers == null)
{
this.headers = new Dictionary<string, string>();
}
else
{
this.headers = headers;
}
subprotocols = new List<string> {subprotocol};
string protocol = uri.Scheme;
if (!protocol.Equals("ws") && !protocol.Equals("wss"))
throw new ArgumentException("Unsupported protocol: " + protocol);
}
public WebSocket(string url, List<string> subprotocols, Dictionary<string, string> headers = null)
{
uri = new Uri(url);
if (headers == null)
{
this.headers = new Dictionary<string, string>();
}
else
{
this.headers = headers;
}
this.subprotocols = subprotocols;
string protocol = uri.Scheme;
if (!protocol.Equals("ws") && !protocol.Equals("wss"))
throw new ArgumentException("Unsupported protocol: " + protocol);
}
public void CancelConnection()
{
m_TokenSource?.Cancel();
}
public async Task Connect()
{
try
{
m_TokenSource = new CancellationTokenSource();
m_CancellationToken = m_TokenSource.Token;
m_Socket = new ClientWebSocket();
foreach (var header in headers)
{
m_Socket.Options.SetRequestHeader(header.Key, header.Value);
}
foreach (string subprotocol in subprotocols) {
m_Socket.Options.AddSubProtocol(subprotocol);
}
await m_Socket.ConnectAsync(uri, m_CancellationToken);
OnOpen?.Invoke();
await Receive();
}
catch (Exception ex)
{
OnError?.Invoke(ex.Message);
OnClose?.Invoke(WebSocketCloseCode.Abnormal);
}
finally
{
if (m_Socket != null)
{
m_TokenSource.Cancel();
m_Socket.Dispose();
}
}
}
public WebSocketState State
{
get
{
switch (m_Socket.State)
{
case System.Net.WebSockets.WebSocketState.Connecting:
return WebSocketState.Connecting;
case System.Net.WebSockets.WebSocketState.Open:
return WebSocketState.Open;
case System.Net.WebSockets.WebSocketState.CloseSent:
case System.Net.WebSockets.WebSocketState.CloseReceived:
return WebSocketState.Closing;
case System.Net.WebSockets.WebSocketState.Closed:
return WebSocketState.Closed;
default:
return WebSocketState.Closed;
}
}
}
public Task Send(byte[] bytes)
{
// return m_Socket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None);
return SendMessage(sendBytesQueue, WebSocketMessageType.Binary, new ArraySegment<byte>(bytes));
}
public Task SendText(string message)
{
var encoded = Encoding.UTF8.GetBytes(message);
// m_Socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
return SendMessage(sendTextQueue, WebSocketMessageType.Text, new ArraySegment<byte>(encoded, 0, encoded.Length));
}
private async Task SendMessage(List<ArraySegment<byte>> queue, WebSocketMessageType messageType, ArraySegment<byte> buffer)
{
// Return control to the calling method immediately.
// await Task.Yield ();
// Make sure we have data.
if (buffer.Count == 0)
{
return;
}
// The state of the connection is contained in the context Items dictionary.
bool sending;
lock (OutgoingMessageLock)
{
sending = isSending;
// If not, we are now.
if (!isSending)
{
isSending = true;
}
}
if (!sending)
{
// Lock with a timeout, just in case.
if (!Monitor.TryEnter(m_Socket, 1000))
{
// If we couldn't obtain exclusive access to the socket in one second, something is wrong.
await m_Socket.CloseAsync(WebSocketCloseStatus.InternalServerError, string.Empty, m_CancellationToken);
return;
}
try
{
// Send the message synchronously.
var t = m_Socket.SendAsync(buffer, messageType, true, m_CancellationToken);
t.Wait(m_CancellationToken);
}
finally
{
Monitor.Exit(m_Socket);
}
// Note that we've finished sending.
lock (OutgoingMessageLock)
{
isSending = false;
}
// Handle any queued messages.
await HandleQueue(queue, messageType);
}
else
{
// Add the message to the queue.
lock (OutgoingMessageLock)
{
queue.Add(buffer);
}
}
}
private async Task HandleQueue(List<ArraySegment<byte>> queue, WebSocketMessageType messageType)
{
var buffer = new ArraySegment<byte>();
lock (OutgoingMessageLock)
{
// Check for an item in the queue.
if (queue.Count > 0)
{
// Pull it off the top.
buffer = queue[0];
queue.RemoveAt(0);
}
}
// Send that message.
if (buffer.Count > 0)
{
await SendMessage(queue, messageType, buffer);
}
}
private List<byte[]> m_MessageList = new List<byte[]>();
// simple dispatcher for queued messages.
public void DispatchMessageQueue()
{
if (m_MessageList.Count == 0)
{
return;
}
List<byte[]> messageListCopy;
lock (IncomingMessageLock)
{
messageListCopy = new List<byte[]>(m_MessageList);
m_MessageList.Clear();
}
var len = messageListCopy.Count;
for (int i = 0; i < len; i++)
{
OnMessage?.Invoke(messageListCopy[i]);
}
}
public async Task Receive()
{
WebSocketCloseCode closeCode = WebSocketCloseCode.Abnormal;
await new WaitForBackgroundThread();
ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[8192]);
try
{
while (m_Socket.State == System.Net.WebSockets.WebSocketState.Open)
{
WebSocketReceiveResult result = null;
using (var ms = new MemoryStream())
{
do
{
result = await m_Socket.ReceiveAsync(buffer, m_CancellationToken);
ms.Write(buffer.Array, buffer.Offset, result.Count);
}
while (!result.EndOfMessage);
ms.Seek(0, SeekOrigin.Begin);
if (result.MessageType == WebSocketMessageType.Text)
{
lock (IncomingMessageLock)
{
m_MessageList.Add(ms.ToArray());
}
//using (var reader = new StreamReader(ms, Encoding.UTF8))
//{
// string message = reader.ReadToEnd();
// OnMessage?.Invoke(this, new MessageEventArgs(message));
//}
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
lock (IncomingMessageLock)
{
m_MessageList.Add(ms.ToArray());
}
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await Close();
closeCode = WebSocketHelpers.ParseCloseCodeEnum((int)result.CloseStatus);
break;
}
}
}
}
catch (Exception)
{
m_TokenSource.Cancel();
}
finally
{
await new WaitForUpdate();
OnClose?.Invoke(closeCode);
}
}
public async Task Close()
{
if (State == WebSocketState.Open)
{
await m_Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, m_CancellationToken);
}
}
}
#endif
///
/// Factory
///
/// <summary>
/// Class providing static access methods to work with JSLIB WebSocket or WebSocketSharp interface
/// </summary>
public static class WebSocketFactory
{
#if UNITY_WEBGL && !UNITY_EDITOR
/* Map of websocket instances */
public static Dictionary<Int32, WebSocket> instances = new Dictionary<Int32, WebSocket> ();
/* Delegates */
public delegate void OnOpenCallback (int instanceId);
public delegate void OnMessageCallback (int instanceId, System.IntPtr msgPtr, int msgSize);
public delegate void OnErrorCallback (int instanceId, System.IntPtr errorPtr);
public delegate void OnCloseCallback (int instanceId, int closeCode);
/* WebSocket JSLIB callback setters and other functions */
[DllImport ("__Internal")]
public static extern int WebSocketAllocate (string url);
[DllImport ("__Internal")]
public static extern int WebSocketAddSubProtocol (int instanceId, string subprotocol);
[DllImport ("__Internal")]
public static extern void WebSocketFree (int instanceId);
[DllImport ("__Internal")]
public static extern void WebSocketSetOnOpen (OnOpenCallback callback);
[DllImport ("__Internal")]
public static extern void WebSocketSetOnMessage (OnMessageCallback callback);
[DllImport ("__Internal")]
public static extern void WebSocketSetOnError (OnErrorCallback callback);
[DllImport ("__Internal")]
public static extern void WebSocketSetOnClose (OnCloseCallback callback);
/* If callbacks was initialized and set */
public static bool isInitialized = false;
/*
* Initialize WebSocket callbacks to JSLIB
*/
public static void Initialize () {
WebSocketSetOnOpen (DelegateOnOpenEvent);
WebSocketSetOnMessage (DelegateOnMessageEvent);
WebSocketSetOnError (DelegateOnErrorEvent);
WebSocketSetOnClose (DelegateOnCloseEvent);
isInitialized = true;
}
/// <summary>
/// Called when instance is destroyed (by destructor)
/// Method removes instance from map and free it in JSLIB implementation
/// </summary>
/// <param name="instanceId">Instance identifier.</param>
public static void HandleInstanceDestroy (int instanceId) {
instances.Remove (instanceId);
WebSocketFree (instanceId);
}
[MonoPInvokeCallback (typeof (OnOpenCallback))]
public static void DelegateOnOpenEvent (int instanceId) {
WebSocket instanceRef;
if (instances.TryGetValue (instanceId, out instanceRef)) {
instanceRef.DelegateOnOpenEvent ();
}
}
[MonoPInvokeCallback (typeof (OnMessageCallback))]
public static void DelegateOnMessageEvent (int instanceId, System.IntPtr msgPtr, int msgSize) {
WebSocket instanceRef;
if (instances.TryGetValue (instanceId, out instanceRef)) {
byte[] msg = new byte[msgSize];
Marshal.Copy (msgPtr, msg, 0, msgSize);
instanceRef.DelegateOnMessageEvent (msg);
}
}
[MonoPInvokeCallback (typeof (OnErrorCallback))]
public static void DelegateOnErrorEvent (int instanceId, System.IntPtr errorPtr) {
WebSocket instanceRef;
if (instances.TryGetValue (instanceId, out instanceRef)) {
string errorMsg = Marshal.PtrToStringAuto (errorPtr);
instanceRef.DelegateOnErrorEvent (errorMsg);
}
}
[MonoPInvokeCallback (typeof (OnCloseCallback))]
public static void DelegateOnCloseEvent (int instanceId, int closeCode) {
WebSocket instanceRef;
if (instances.TryGetValue (instanceId, out instanceRef)) {
instanceRef.DelegateOnCloseEvent (closeCode);
}
}
#endif
/// <summary>
/// Create WebSocket client instance
/// </summary>
/// <returns>The WebSocket instance.</returns>
/// <param name="url">WebSocket valid URL.</param>
public static WebSocket CreateInstance(string url)
{
return new WebSocket(url);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b775e14fc5a58124e8dbe558d81b5824
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,333 @@
var LibraryWebSocket = {
$webSocketState: {
/*
* Map of instances
*
* Instance structure:
* {
* url: string,
* ws: WebSocket
* }
*/
instances: {},
/* Last instance ID */
lastId: 0,
/* Event listeners */
onOpen: null,
onMesssage: null,
onError: null,
onClose: null,
/* Debug mode */
debug: false
},
/**
* Set onOpen callback
*
* @param callback Reference to C# static function
*/
WebSocketSetOnOpen: function(callback) {
webSocketState.onOpen = callback;
},
/**
* Set onMessage callback
*
* @param callback Reference to C# static function
*/
WebSocketSetOnMessage: function(callback) {
webSocketState.onMessage = callback;
},
/**
* Set onError callback
*
* @param callback Reference to C# static function
*/
WebSocketSetOnError: function(callback) {
webSocketState.onError = callback;
},
/**
* Set onClose callback
*
* @param callback Reference to C# static function
*/
WebSocketSetOnClose: function(callback) {
webSocketState.onClose = callback;
},
/**
* Allocate new WebSocket instance struct
*
* @param url Server URL
*/
WebSocketAllocate: function(url) {
var urlStr = UTF8ToString(url);
var id = webSocketState.lastId++;
webSocketState.instances[id] = {
subprotocols: [],
url: urlStr,
ws: null
};
return id;
},
/**
* Add subprotocol to instance
*
* @param instanceId Instance ID
* @param subprotocol Subprotocol name to add to instance
*/
WebSocketAddSubProtocol: function(instanceId, subprotocol) {
var subprotocolStr = UTF8ToString(subprotocol);
webSocketState.instances[instanceId].subprotocols.push(subprotocolStr);
},
/**
* Remove reference to WebSocket instance
*
* If socket is not closed function will close it but onClose event will not be emitted because
* this function should be invoked by C# WebSocket destructor.
*
* @param instanceId Instance ID
*/
WebSocketFree: function(instanceId) {
var instance = webSocketState.instances[instanceId];
if (!instance) return 0;
// Close if not closed
if (instance.ws && instance.ws.readyState < 2)
instance.ws.close();
// Remove reference
delete webSocketState.instances[instanceId];
return 0;
},
/**
* Connect WebSocket to the server
*
* @param instanceId Instance ID
*/
WebSocketConnect: function(instanceId) {
var instance = webSocketState.instances[instanceId];
if (!instance) return -1;
if (instance.ws !== null)
return -2;
instance.ws = new WebSocket(instance.url, instance.subprotocols);
instance.ws.binaryType = 'arraybuffer';
instance.ws.onopen = function() {
if (webSocketState.debug)
console.log("[JSLIB WebSocket] Connected.");
if (webSocketState.onOpen)
Module.dynCall_vi(webSocketState.onOpen, instanceId);
};
instance.ws.onmessage = function(ev) {
if (webSocketState.debug)
console.log("[JSLIB WebSocket] Received message:", ev.data);
if (webSocketState.onMessage === null)
return;
if (ev.data instanceof ArrayBuffer) {
var dataBuffer = new Uint8Array(ev.data);
var buffer = _malloc(dataBuffer.length);
HEAPU8.set(dataBuffer, buffer);
try {
Module.dynCall_viii(webSocketState.onMessage, instanceId, buffer, dataBuffer.length);
} finally {
_free(buffer);
}
} else {
var dataBuffer = (new TextEncoder()).encode(ev.data);
var buffer = _malloc(dataBuffer.length);
HEAPU8.set(dataBuffer, buffer);
try {
Module.dynCall_viii(webSocketState.onMessage, instanceId, buffer, dataBuffer.length);
} finally {
_free(buffer);
}
}
};
instance.ws.onerror = function(ev) {
if (webSocketState.debug)
console.log("[JSLIB WebSocket] Error occured.");
if (webSocketState.onError) {
var msg = "WebSocket error.";
var length = lengthBytesUTF8(msg) + 1;
var buffer = _malloc(length);
stringToUTF8(msg, buffer, length);
try {
Module.dynCall_vii(webSocketState.onError, instanceId, buffer);
} finally {
_free(buffer);
}
}
};
instance.ws.onclose = function(ev) {
if (webSocketState.debug)
console.log("[JSLIB WebSocket] Closed.");
if (webSocketState.onClose)
Module.dynCall_vii(webSocketState.onClose, instanceId, ev.code);
delete instance.ws;
};
return 0;
},
/**
* Close WebSocket connection
*
* @param instanceId Instance ID
* @param code Close status code
* @param reasonPtr Pointer to reason string
*/
WebSocketClose: function(instanceId, code, reasonPtr) {
var instance = webSocketState.instances[instanceId];
if (!instance) return -1;
if (!instance.ws)
return -3;
if (instance.ws.readyState === 2)
return -4;
if (instance.ws.readyState === 3)
return -5;
var reason = ( reasonPtr ? UTF8ToString(reasonPtr) : undefined );
try {
instance.ws.close(code, reason);
} catch(err) {
return -7;
}
return 0;
},
/**
* Send message over WebSocket
*
* @param instanceId Instance ID
* @param bufferPtr Pointer to the message buffer
* @param length Length of the message in the buffer
*/
WebSocketSend: function(instanceId, bufferPtr, length) {
var instance = webSocketState.instances[instanceId];
if (!instance) return -1;
if (!instance.ws)
return -3;
if (instance.ws.readyState !== 1)
return -6;
instance.ws.send(HEAPU8.buffer.slice(bufferPtr, bufferPtr + length));
return 0;
},
/**
* Send text message over WebSocket
*
* @param instanceId Instance ID
* @param bufferPtr Pointer to the message buffer
* @param length Length of the message in the buffer
*/
WebSocketSendText: function(instanceId, message) {
var instance = webSocketState.instances[instanceId];
if (!instance) return -1;
if (!instance.ws)
return -3;
if (instance.ws.readyState !== 1)
return -6;
instance.ws.send(UTF8ToString(message));
return 0;
},
/**
* Return WebSocket readyState
*
* @param instanceId Instance ID
*/
WebSocketGetState: function(instanceId) {
var instance = webSocketState.instances[instanceId];
if (!instance) return -1;
if (instance.ws)
return instance.ws.readyState;
else
return 3;
}
};
autoAddDeps(LibraryWebSocket, '$webSocketState');
mergeInto(LibraryManager.library, LibraryWebSocket);
@@ -0,0 +1,32 @@
fileFormatVersion: 2
guid: d4e1195d1a5508b419c5fc7120cba0ed
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
WebGL: WebGL
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
{
"name": "endel.nativewebsocket"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f3cf245a6f30d9740a037cf5cd3dfb43
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c66c722e6468770478c807f30bd55575
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
{
"name": "com.endel.nativewebsocket",
"version": "1.1.4",
"description": "WebSocket client for Unity - with no external dependencies (WebGL, Native, Android, iOS, UWP).",
"license": "Apache 2.0",
"repository": {
"type": "git",
"url": "https://github.com/endel/NativeWebSocket.git"
},
"author": {
"name": "Endel Dreyer",
"email": "endel.dreyer@gmail.com",
"url": "https://github.com/endel/NativeWebSocket"
},
"keywords": [
"websocket",
"websockets",
"native websocket",
"native websockets"
],
"displayName": "Native WebSockets",
"unity": "2019.1",
"dependencies": {},
"samples": [
{
"displayName": "Example",
"description": "WebSocket Example",
"path": "Samples~/WebSocketExample"
}
],
"_fingerprint": "c5101c07762251edc82caad9e8a39d51b1229c31"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: de5c97ce7ae5a4a4b9da45566ae46222
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: