chore
parent
04e8229f89
commit
51fe69d7c0
|
@ -0,0 +1,230 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using LeanCloud.Realtime.Internal.Router;
|
||||
using LeanCloud.Realtime.Internal.WebSocket;
|
||||
using LeanCloud.Common;
|
||||
using LeanCloud.Storage;
|
||||
|
||||
namespace LeanCloud.LiveQuery.Internal {
|
||||
public class LCLiveQueryConnection {
|
||||
/// <summary>
|
||||
/// 发送超时
|
||||
/// </summary>
|
||||
private const int SEND_TIMEOUT = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// 最大重连次数,超过后重置 Router 缓存后再次尝试重连
|
||||
/// </summary>
|
||||
private const int MAX_RECONNECT_TIMES = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 重连间隔
|
||||
/// </summary>
|
||||
private const int RECONNECT_INTERVAL = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// 子协议
|
||||
/// </summary>
|
||||
private const string SUB_PROTOCOL = "lc.json.3";
|
||||
|
||||
/// <summary>
|
||||
/// 通知事件
|
||||
/// </summary>
|
||||
internal Action<Dictionary<string, object>> OnNotification;
|
||||
|
||||
/// <summary>
|
||||
/// 断线事件
|
||||
/// </summary>
|
||||
internal Action OnDisconnect;
|
||||
|
||||
/// <summary>
|
||||
/// 重连成功事件
|
||||
/// </summary>
|
||||
internal Action OnReconnected;
|
||||
|
||||
internal string id;
|
||||
|
||||
/// <summary>
|
||||
/// 请求回调缓存
|
||||
/// </summary>
|
||||
private readonly Dictionary<int, TaskCompletionSource<Dictionary<string, object>>> responses;
|
||||
|
||||
private int requestI = 1;
|
||||
|
||||
private LCRTMRouter router;
|
||||
|
||||
private LCLiveQueryHeartBeat heartBeat;
|
||||
|
||||
private LCWebSocketClient client;
|
||||
|
||||
public LCLiveQueryConnection(string id) {
|
||||
this.id = id;
|
||||
responses = new Dictionary<int, TaskCompletionSource<Dictionary<string, object>>>();
|
||||
heartBeat = new LCLiveQueryHeartBeat(this);
|
||||
router = new LCRTMRouter();
|
||||
client = new LCWebSocketClient {
|
||||
OnMessage = OnClientMessage,
|
||||
OnClose = OnClientDisconnect
|
||||
};
|
||||
}
|
||||
|
||||
public async Task Connect() {
|
||||
try {
|
||||
LCRTMServer rtmServer = await router.GetServer();
|
||||
try {
|
||||
LCLogger.Debug($"Primary Server");
|
||||
await client.Connect(rtmServer.Primary, SUB_PROTOCOL);
|
||||
} catch (Exception e) {
|
||||
LCLogger.Error(e);
|
||||
LCLogger.Debug($"Secondary Server");
|
||||
await client.Connect(rtmServer.Secondary, SUB_PROTOCOL);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置连接
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal async Task Reset() {
|
||||
// 关闭就连接
|
||||
await client.Close();
|
||||
// 重新创建连接组件
|
||||
heartBeat = new LCLiveQueryHeartBeat(this);
|
||||
router = new LCRTMRouter();
|
||||
client = new LCWebSocketClient {
|
||||
OnMessage = OnClientMessage,
|
||||
OnClose = OnClientDisconnect
|
||||
};
|
||||
await Reconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送请求,会在收到应答后返回
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
internal async Task<Dictionary<string, object>> SendRequest(Dictionary<string, object> request) {
|
||||
TaskCompletionSource<Dictionary<string, object>> tcs = new TaskCompletionSource<Dictionary<string, object>>();
|
||||
int requestIndex = requestI++;
|
||||
request["i"] = requestIndex;
|
||||
responses.Add(requestIndex, tcs);
|
||||
try {
|
||||
string json = JsonConvert.SerializeObject(request);
|
||||
await SendText(json);
|
||||
} catch (Exception e) {
|
||||
tcs.TrySetException(e);
|
||||
}
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送文本消息
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
internal async Task SendText(string text) {
|
||||
LCLogger.Debug($"{id} => {text}");
|
||||
Task sendTask = client.Send(text);
|
||||
if (await Task.WhenAny(sendTask, Task.Delay(SEND_TIMEOUT)) == sendTask) {
|
||||
await sendTask;
|
||||
} else {
|
||||
throw new TimeoutException("Send request time out");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭连接
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal async Task Close() {
|
||||
OnNotification = null;
|
||||
OnDisconnect = null;
|
||||
OnReconnected = null;
|
||||
heartBeat.Stop();
|
||||
await client.Close();
|
||||
}
|
||||
|
||||
private void OnClientMessage(byte[] bytes) {
|
||||
_ = heartBeat.Refresh(OnPingTimeout);
|
||||
try {
|
||||
string json = Encoding.UTF8.GetString(bytes);
|
||||
Dictionary<string, object> msg = JsonConvert.DeserializeObject<Dictionary<string, object>>(json,
|
||||
LCJsonConverter.Default);
|
||||
LCLogger.Debug($"{id} <= {json}");
|
||||
if (msg.TryGetValue("i", out object i)) {
|
||||
int requestIndex = Convert.ToInt32(i);
|
||||
if (responses.TryGetValue(requestIndex, out TaskCompletionSource<Dictionary<string, object>> tcs)) {
|
||||
if (msg.TryGetValue("error", out object error)) {
|
||||
// 错误
|
||||
if (error is Dictionary<string, object> dict) {
|
||||
int code = Convert.ToInt32(dict["code"]);
|
||||
string detail = dict["detail"] as string;
|
||||
tcs.SetException(new LCException(code, detail));
|
||||
} else {
|
||||
tcs.SetException(new Exception(error as string));
|
||||
}
|
||||
} else {
|
||||
tcs.SetResult(msg);
|
||||
}
|
||||
responses.Remove(requestIndex);
|
||||
} else {
|
||||
LCLogger.Error($"No request for {requestIndex}");
|
||||
}
|
||||
} else {
|
||||
// 通知
|
||||
OnNotification?.Invoke(msg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LCLogger.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClientDisconnect() {
|
||||
heartBeat.Stop();
|
||||
OnDisconnect?.Invoke();
|
||||
// 重连
|
||||
_ = Reconnect();
|
||||
}
|
||||
|
||||
private async void OnPingTimeout() {
|
||||
await client.Close();
|
||||
OnClientDisconnect();
|
||||
}
|
||||
|
||||
private async Task Reconnect() {
|
||||
while (true) {
|
||||
int reconnectCount = 0;
|
||||
// 重连策略
|
||||
while (reconnectCount < MAX_RECONNECT_TIMES) {
|
||||
try {
|
||||
LCLogger.Debug($"Reconnecting... {reconnectCount}");
|
||||
await Connect();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
reconnectCount++;
|
||||
LCLogger.Error(e);
|
||||
LCLogger.Debug($"Reconnect after {RECONNECT_INTERVAL}ms");
|
||||
await Task.Delay(RECONNECT_INTERVAL);
|
||||
}
|
||||
}
|
||||
if (reconnectCount < MAX_RECONNECT_TIMES) {
|
||||
// 重连成功
|
||||
LCLogger.Debug("Reconnected");
|
||||
client.OnMessage = OnClientMessage;
|
||||
client.OnClose = OnClientDisconnect;
|
||||
OnReconnected?.Invoke();
|
||||
break;
|
||||
} else {
|
||||
// 重置 Router,继续尝试重连
|
||||
router = new LCRTMRouter();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using LeanCloud.Storage;
|
||||
using LeanCloud.Storage.Internal.Object;
|
||||
using LeanCloud.LiveQuery.Internal;
|
||||
|
||||
namespace LeanCloud.LiveQuery {
|
||||
/// <summary>
|
||||
/// LiveQuery
|
||||
/// </summary>
|
||||
public class LCLiveQuery {
|
||||
/// <summary>
|
||||
/// 新对象创建事件
|
||||
/// </summary>
|
||||
public Action<LCObject> OnCreate;
|
||||
/// <summary>
|
||||
/// 对象更新事件
|
||||
/// </summary>
|
||||
public Action<LCObject, ReadOnlyCollection<string>> OnUpdate;
|
||||
/// <summary>
|
||||
/// 对象被删除
|
||||
/// </summary>
|
||||
public Action<string> OnDelete;
|
||||
/// <summary>
|
||||
/// 有新的满足条件的对象产生
|
||||
/// </summary>
|
||||
public Action<LCObject, ReadOnlyCollection<string>> OnEnter;
|
||||
/// <summary>
|
||||
/// 不再满足条件
|
||||
/// </summary>
|
||||
public Action<LCObject, ReadOnlyCollection<string>> OnLeave;
|
||||
/// <summary>
|
||||
/// 当一个用户登录成功
|
||||
/// </summary>
|
||||
public Action<LCUser> OnLogin;
|
||||
|
||||
public string Id {
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public LCQuery Query {
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
private static LCLiveQueryConnection connection;
|
||||
|
||||
private static Dictionary<string, WeakReference<LCLiveQuery>> liveQueries = new Dictionary<string, WeakReference<LCLiveQuery>>();
|
||||
|
||||
internal LCLiveQuery() {
|
||||
|
||||
}
|
||||
|
||||
private static readonly string DeviceId = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 订阅
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task Subscribe() {
|
||||
// TODO 判断当前连接情况
|
||||
if (connection == null) {
|
||||
connection = new LCLiveQueryConnection(DeviceId) {
|
||||
OnReconnected = OnReconnected,
|
||||
OnNotification = OnNotification
|
||||
};
|
||||
await connection.Connect();
|
||||
await Login();
|
||||
}
|
||||
Dictionary<string, object> queryData = new Dictionary<string, object> {
|
||||
{ "className", Query.ClassName },
|
||||
{ "where", Query.Condition.Encode() }
|
||||
};
|
||||
Dictionary<string, object> data = new Dictionary<string, object> {
|
||||
{ "query", queryData },
|
||||
{ "id", DeviceId },
|
||||
{ "clientTimestamp", DateTimeOffset.Now.ToUnixTimeMilliseconds() }
|
||||
};
|
||||
LCUser user = await LCUser.GetCurrent();
|
||||
if (user != null && !string.IsNullOrEmpty(user.SessionToken)) {
|
||||
data.Add("sessionToken", user.SessionToken);
|
||||
}
|
||||
string path = "LiveQuery/subscribe";
|
||||
Dictionary<string, object> result = await LCApplication.HttpClient.Post<Dictionary<string, object>>(path,
|
||||
data: data);
|
||||
if (result.TryGetValue("query_id", out object id)) {
|
||||
Id = id as string;
|
||||
WeakReference<LCLiveQuery> weakRef = new WeakReference<LCLiveQuery>(this);
|
||||
liveQueries[Id] = weakRef;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消订阅
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task Unsubscribe() {
|
||||
Dictionary<string, object> data = new Dictionary<string, object> {
|
||||
{ "id", DeviceId },
|
||||
{ "query_id", Id }
|
||||
};
|
||||
string path = "LiveQuery/unsubscribe";
|
||||
await LCApplication.HttpClient.Post<Dictionary<string, object>>(path,
|
||||
data: data);
|
||||
// 移除
|
||||
liveQueries.Remove(Id);
|
||||
}
|
||||
|
||||
private static async Task Login() {
|
||||
Dictionary<string, object> data = new Dictionary<string, object> {
|
||||
{ "cmd", "login" },
|
||||
{ "appId", LCApplication.AppId },
|
||||
{ "installationId", DeviceId },
|
||||
{ "clientTs", DateTimeOffset.Now.ToUnixTimeMilliseconds() },
|
||||
{ "service", 1 }
|
||||
};
|
||||
await connection.SendRequest(data);
|
||||
}
|
||||
|
||||
private static async void OnReconnected() {
|
||||
await Login();
|
||||
Dictionary<string, WeakReference<LCLiveQuery>> oldLiveQueries = liveQueries;
|
||||
liveQueries = new Dictionary<string, WeakReference<LCLiveQuery>>();
|
||||
foreach (WeakReference<LCLiveQuery> weakRef in oldLiveQueries.Values) {
|
||||
if (weakRef.TryGetTarget(out LCLiveQuery liveQuery)) {
|
||||
await liveQuery.Subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnNotification(Dictionary<string, object> notification) {
|
||||
if (!notification.TryGetValue("cmd", out object cmd) ||
|
||||
!"data".Equals(cmd)) {
|
||||
return;
|
||||
}
|
||||
if (!notification.TryGetValue("msg", out object msg) ||
|
||||
!(msg is IEnumerable<object> list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (object item in list) {
|
||||
if (item is Dictionary<string, object> dict) {
|
||||
if (!dict.TryGetValue("op", out object op)) {
|
||||
continue;
|
||||
}
|
||||
switch (op as string) {
|
||||
case "create":
|
||||
OnCreateNotification(dict);
|
||||
break;
|
||||
case "update":
|
||||
OnUpdateNotification(dict);
|
||||
break;
|
||||
case "enter":
|
||||
OnEnterNotification(dict);
|
||||
break;
|
||||
case "leave":
|
||||
OnLeaveNotification(dict);
|
||||
break;
|
||||
case "delete":
|
||||
OnDeleteNotification(dict);
|
||||
break;
|
||||
case "login":
|
||||
OnLoginNotification(dict);
|
||||
break;
|
||||
default:
|
||||
LCLogger.Debug($"Not support: {op}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCreateNotification(Dictionary<string, object> data) {
|
||||
if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) &&
|
||||
TryGetObject(data, out LCObject obj)) {
|
||||
liveQuery.OnCreate?.Invoke(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnUpdateNotification(Dictionary<string, object> data) {
|
||||
if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) &&
|
||||
TryGetObject(data, out LCObject obj) &&
|
||||
TryGetUpdatedKeys(data, out ReadOnlyCollection<string> keys)) {
|
||||
liveQuery.OnUpdate?.Invoke(obj, keys);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnEnterNotification(Dictionary<string, object> data) {
|
||||
if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) &&
|
||||
TryGetObject(data, out LCObject obj) &&
|
||||
TryGetUpdatedKeys(data, out ReadOnlyCollection<string> keys)) {
|
||||
liveQuery.OnEnter?.Invoke(obj, keys);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnLeaveNotification(Dictionary<string, object> data) {
|
||||
if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) &&
|
||||
TryGetObject(data, out LCObject obj) &&
|
||||
TryGetUpdatedKeys(data, out ReadOnlyCollection<string> keys)) {
|
||||
liveQuery.OnLeave?.Invoke(obj, keys);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDeleteNotification(Dictionary<string, object> data) {
|
||||
if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) &&
|
||||
TryGetObject(data, out LCObject obj)) {
|
||||
liveQuery.OnDelete?.Invoke(obj.ObjectId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnLoginNotification(Dictionary<string, object> data) {
|
||||
if (TryGetLiveQuery(data, out LCLiveQuery liveQuery) &&
|
||||
data.TryGetValue("object", out object obj) &&
|
||||
obj is Dictionary<string, object> dict) {
|
||||
LCObjectData objectData = LCObjectData.Decode(dict);
|
||||
LCUser user = new LCUser(objectData);
|
||||
liveQuery.OnLogin?.Invoke(user);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetLiveQuery(Dictionary<string, object> data, out LCLiveQuery liveQuery) {
|
||||
if (!data.TryGetValue("query_id", out object i) ||
|
||||
!(i is string id)) {
|
||||
liveQuery = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!liveQueries.TryGetValue(id, out WeakReference<LCLiveQuery> weakRef) ||
|
||||
!weakRef.TryGetTarget(out LCLiveQuery lq)) {
|
||||
liveQuery = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
liveQuery = lq;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetObject(Dictionary<string, object> data, out LCObject obj) {
|
||||
if (!data.TryGetValue("object", out object o) ||
|
||||
!(o is Dictionary<string, object> dict)) {
|
||||
obj = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
LCObjectData objectData = LCObjectData.Decode(dict);
|
||||
obj = LCObject.Create(dict["className"] as string);
|
||||
obj.Merge(objectData);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetUpdatedKeys(Dictionary<string, object> data, out ReadOnlyCollection<string> keys) {
|
||||
if (!data.TryGetValue("updatedKeys", out object uks) ||
|
||||
!(uks is List<object> list)) {
|
||||
keys = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
keys = list.Cast<string>().ToList()
|
||||
.AsReadOnly();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue