fix:1、更换项目,使用winter来创建
This commit is contained in:
@@ -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();
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c97138411a25ced4f9dfa8842cb4f0da
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "endel.nativewebsocket"
|
||||
}
|
||||
+7
@@ -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:
|
||||
Reference in New Issue
Block a user