diff --git a/FreeIM/FreeIM.xml b/FreeIM/FreeIM.xml
index 7f60420..9ab6c33 100644
--- a/FreeIM/FreeIM.xml
+++ b/FreeIM/FreeIM.xml
@@ -4,30 +4,36 @@
FreeIM
-
+
im 核心类实现
+ 客户端ID类型,约束为数值类型(如 long、int、short 等)
-
+
推送消息的事件,可审查推向哪个Server节点
-
+
初始化 imclient
-
+
负载分区规则:clientId求模
客户端id
-
+
+
+ 将泛型 clientId 转换为 long
+
+
+
ImServer 连接前的负载、授权,返回 ws 目标地址,使用该地址连接 websocket 服务端
@@ -35,7 +41,7 @@
客户端相关信息,比如ip
websocket 地址:ws://xxxx/ws?token=xxx
-
+
向指定的多个客户端id发送消息
@@ -44,94 +50,99 @@
消息
是否回执
-
+
获取所在线客户端id
-
+
+
+ 将 long 转换为泛型 clientId 类型
+
+
+
判断客户端是否在线
-
+
判断客户端是否在线
-
+
强制下线
-
+
事件订阅
上线
下线
-
+
加入群聊频道,每次上线都必须重新加入
客户端id
群聊频道名
-
+
离开群聊频道
客户端id
群聊频道名
-
+
离开群聊频道
群聊频道名
客户端id
-
+
获取群聊频道所有客户端id(测试)
群聊频道名
-
+
清理群聊频道的离线客户端(测试)
群聊频道名
-
+
获取所有群聊频道和在线人数
频道名和在线人数
-
+
获取用户参与的所有群聊频道
客户端id
-
+
获取群聊频道的在线人数
群聊频道名
在线人数
-
+
发送群聊消息,在线的用户将收到消息
@@ -139,12 +150,23 @@
群聊频道名
消息
-
+
发送广播消息
消息
+
+
+ 兼容旧版本的 ImClient 类型别名(使用 long 作为 clientId)
+
+
+
+
+ 使用默认配置初始化 ImClient
+
+
+
im 核心类实现的配置所需
@@ -170,34 +192,45 @@
Json 序列化设置
-
+
+
+ ImSendEventArgs 泛型版本
+
+ 客户端ID类型
+
+
发送者的客户端id
-
+
接收者的客户端id
-
+
imServer 服务器节点
-
+
消息
-
+
是否回执
+
+
+ 兼容旧版本的 ImSendEventArgs(使用 long 作为 clientId)
+
+
- im 核心类 ImClient 实现的静态代理类
+ im 核心类 ImClient 实现的静态代理类(兼容版本,使用 long 作为 clientId)
@@ -324,5 +357,140 @@
消息
+
+
+ ImHelper 泛型版本(预览版)
+
+ 客户端ID类型,支持 long、int、short、byte 等数值类型
+
+
+
+ 初始化 ImHelper
+
+
+
+
+
+ ImServer 连接前的负载、授权,返回 ws 目标地址,使用该地址连接 websocket 服务端
+
+ 客户端id
+ 客户端相关信息,比如ip
+ websocket 地址:ws://xxxx/ws?token=xxx
+
+
+
+ 向指定的多个客户端id发送消息
+
+ 发送者的客户端id
+ 接收者的客户端id
+ 消息
+ 是否回执
+
+
+
+ 获取所在线客户端id
+
+
+
+
+
+ 判断客户端是否在线
+
+
+
+
+
+
+ 判断客户端是否在线(多个)
+
+
+
+
+
+
+ 强制下线
+
+
+
+
+
+ 事件订阅
+
+ 上线
+ 下线
+
+
+
+ 加入群聊频道,每次上线都必须重新加入
+
+ 客户端id
+ 群聊频道名
+
+
+
+ 离开群聊频道
+
+ 客户端id
+ 群聊频道名
+
+
+
+ 离开群聊频道
+
+ 群聊频道名
+ 客户端id
+
+
+
+ 获取群聊频道所有客户端id(测试)
+
+ 群聊频道名
+
+
+
+
+ 清理群聊频道的离线客户端(测试)
+
+ 群聊频道名
+
+
+
+ 获取用户参与的所有群聊频道
+
+ 客户端id
+
+
+
+
+ 发送群聊消息,所有在线的用户将收到消息
+
+ 发送者的客户端id
+ 群聊频道名
+ 消息
+
+
+
+ 发送广播消息
+
+ 消息
+
+
+
+ 启用 ImServer 服务端
+
+
+
+
+
+
+
+ im 核心类实现的配置所需
+
+
+
+
+ 设置服务名称,它应该是 servers 内的一个
+
+
diff --git a/FreeIM/ImClient.cs b/FreeIM/ImClient.cs
index 844299e..a8bb0ab 100644
--- a/FreeIM/ImClient.cs
+++ b/FreeIM/ImClient.cs
@@ -1,4 +1,4 @@
-using FreeRedis;
+using FreeRedis;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -7,7 +7,8 @@
///
/// im 核心类实现
///
-public class ImClient
+/// 客户端ID类型,约束为数值类型(如 long、int、short 等)
+public class ImClient where TClientId : struct, IComparable
{
protected RedisClient _redis;
protected string[] _servers;
@@ -18,7 +19,7 @@ public class ImClient
///
/// 推送消息的事件,可审查推向哪个Server节点
///
- public EventHandler OnSend;
+ public EventHandler> OnSend;
///
/// 初始化 imclient
@@ -40,24 +41,41 @@ public ImClient(ImClientOptions options)
///
/// 客户端id
///
- protected string SelectServer(long clientId)
+ protected string SelectServer(TClientId clientId)
{
- var servers_idx = clientId % _servers.Length;
+ // 转换为 long 进行求模运算
+ var clientIdLong = ConvertToInt64(clientId);
+ var servers_idx = clientIdLong % _servers.Length;
if (servers_idx >= _servers.Length) servers_idx = 0;
return _servers[servers_idx];
}
+ ///
+ /// 将泛型 clientId 转换为 long
+ ///
+ private long ConvertToInt64(TClientId clientId)
+ {
+ if (clientId is long l) return l;
+ if (clientId is int i) return i;
+ if (clientId is short s) return s;
+ if (clientId is byte b) return b;
+ if (clientId is ulong ul) return (long)ul;
+ if (clientId is uint ui) return (long)ui;
+ if (clientId is ushort us) return (long)us;
+ throw new InvalidOperationException($"不支持的 clientId 类型: {typeof(TClientId)}");
+ }
+
///
/// ImServer 连接前的负载、授权,返回 ws 目标地址,使用该地址连接 websocket 服务端
///
/// 客户端id
/// 客户端相关信息,比如ip
/// websocket 地址:ws://xxxx/ws?token=xxx
- public string PrevConnectServer(long clientId, string clientMetaData)
+ public string PrevConnectServer(TClientId clientId, string clientMetaData)
{
var server = SelectServer(clientId);
var token = $"{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}{Guid.NewGuid()}".Replace("-", "");
- _redis.Set($"{_redisPrefix}Token{token}", JsonConvert.SerializeObject((clientId, clientMetaData), _jsonSerializerSettings), 10);
+ _redis.Set($"{_redisPrefix}Token{token}", JsonConvert.SerializeObject((ConvertToInt64(clientId), clientMetaData), _jsonSerializerSettings), 10);
return $"ws://{server}{_pathMatch}?token={token}";
}
@@ -68,15 +86,15 @@ public string PrevConnectServer(long clientId, string clientMetaData)
/// 接收者的客户端id
/// 消息
/// 是否回执
- public void SendMessage(long senderClientId, IEnumerable receiveClientId, object message, bool receipt = false)
+ public void SendMessage(TClientId senderClientId, IEnumerable receiveClientId, object message, bool receipt = false)
{
- receiveClientId = receiveClientId.Distinct().ToArray();
- Dictionary redata = new Dictionary();
+ var receiverArray = receiveClientId.Distinct().ToArray();
+ Dictionary> redata = new Dictionary>();
- foreach (var uid in receiveClientId)
+ foreach (var uid in receiverArray)
{
string server = SelectServer(uid);
- if (redata.ContainsKey(server) == false) redata.Add(server, new ImSendEventArgs(server, senderClientId, message, receipt));
+ if (redata.ContainsKey(server) == false) redata.Add(server, new ImSendEventArgs(server, senderClientId, message, receipt));
redata[server].ReceiveClientId.Add(uid);
}
var messageJson = JsonConvert.SerializeObject(message, _jsonSerializerSettings);
@@ -85,8 +103,9 @@ public void SendMessage(long senderClientId, IEnumerable receiveClientId,
foreach (var sendArgs in redata.Values)
{
OnSend?.Invoke(this, sendArgs);
+ var receiverLongs = sendArgs.ReceiveClientId.Select(a => ConvertToInt64(a)).ToArray();
pipe.Publish($"{_redisPrefix}Server{sendArgs.Server}",
- JsonConvert.SerializeObject((senderClientId, sendArgs.ReceiveClientId, messageJson, sendArgs.Receipt), _jsonSerializerSettings));
+ JsonConvert.SerializeObject((ConvertToInt64(senderClientId), receiverLongs, messageJson, sendArgs.Receipt), _jsonSerializerSettings));
}
pipe.EndPipe();
}
@@ -96,9 +115,24 @@ public void SendMessage(long senderClientId, IEnumerable receiveClientId,
/// 获取所在线客户端id
///
///
- public IEnumerable GetClientListByOnline()
+ public IEnumerable GetClientListByOnline()
{
- return _redis.HKeys($"{_redisPrefix}Online").Select(a => long.TryParse(a, out var tryval) ? tryval : 0).Where(a => a != 0);
+ return _redis.HKeys($"{_redisPrefix}Online")
+ .Select(a => long.TryParse(a, out var tryval) ? tryval : 0)
+ .Where(a => a != 0)
+ .Select(a => ConvertToClientIdType(a));
+ }
+
+ ///
+ /// 将 long 转换为泛型 clientId 类型
+ ///
+ private TClientId ConvertToClientIdType(long value)
+ {
+ if (typeof(TClientId) == typeof(long)) return (TClientId)(object)value;
+ if (typeof(TClientId) == typeof(int)) return (TClientId)(object)(int)value;
+ if (typeof(TClientId) == typeof(short)) return (TClientId)(object)(short)value;
+ if (typeof(TClientId) == typeof(byte)) return (TClientId)(object)(byte)value;
+ throw new InvalidOperationException($"不支持的 clientId 类型: {typeof(TClientId)}");
}
///
@@ -106,16 +140,17 @@ public IEnumerable GetClientListByOnline()
///
///
///
- public bool HasOnline(long clientId)
+ public bool HasOnline(TClientId clientId)
{
return _redis.HGet($"{_redisPrefix}Online", clientId.ToString()) > 0;
}
+
///
/// 判断客户端是否在线
///
///
///
- public bool[] HasOnline(IEnumerable clientIds)
+ public bool[] HasOnline(IEnumerable clientIds)
{
if (clientIds?.Any() != true) return new bool[0];
return _redis.HMGet($"{_redisPrefix}Online", clientIds.Select(a => a.ToString()).ToArray()).Select(a => a > 0).ToArray();
@@ -125,10 +160,10 @@ public bool[] HasOnline(IEnumerable clientIds)
/// 强制下线
///
///
- public void ForceOffline(long clientId)
+ public void ForceOffline(TClientId clientId)
{
string server = SelectServer(clientId);
- _redis.Publish($"{_redisPrefix}Server{server}", $"__FreeIM__(ForceOffline){clientId}");
+ _redis.Publish($"{_redisPrefix}Server{server}", $"__FreeIM__(ForceOffline){ConvertToInt64(clientId)}");
}
///
@@ -137,15 +172,23 @@ public void ForceOffline(long clientId)
/// 上线
/// 下线
public void EventBus(
- Action<(long clientId, string clientMetaData)> online,
- Action<(long clientId, string clientMetaData)> offline)
+ Action<(TClientId clientId, string clientMetaData)> online,
+ Action<(TClientId clientId, string clientMetaData)> offline)
{
var chanOnline = $"evt_{_redisPrefix}Online";
var chanOffline = $"evt_{_redisPrefix}Offline";
_redis.Subscribe(new[] { chanOnline, chanOffline }, (chan, msg) =>
{
- if (chan == chanOnline) online(JsonConvert.DeserializeObject<(long clientId, string clientMetaData)>(msg as string));
- if (chan == chanOffline) offline(JsonConvert.DeserializeObject<(long clientId, string clientMetaData)>(msg as string));
+ if (chan == chanOnline)
+ {
+ var data = JsonConvert.DeserializeObject<(long clientId, string clientMetaData)>(msg as string);
+ online((ConvertToClientIdType(data.clientId), data.clientMetaData));
+ }
+ if (chan == chanOffline)
+ {
+ var data = JsonConvert.DeserializeObject<(long clientId, string clientMetaData)>(msg as string);
+ offline((ConvertToClientIdType(data.clientId), data.clientMetaData));
+ }
});
}
@@ -156,51 +199,49 @@ public void EventBus(
///
/// 客户端id
/// 群聊频道名
- public void JoinChan(long clientId, params string[] chans)
+ public void JoinChan(TClientId clientId, params string[] chans)
{
if (chans?.Any() != true) return;
+ var clientIdStr = ConvertToInt64(clientId).ToString();
using (var pipe = _redis.StartPipe())
{
foreach (var chan in chans)
{
if (string.IsNullOrEmpty(chan)) continue;
pipe.Eval($"if redis.call('HSETNX',KEYS[1],ARGV[1],0)==1 then redis.call('HSET',KEYS[2],ARGV[2],0) redis.call('HINCRBY',KEYS[3],ARGV[2],1) end return 1",
- new[] { $"{_redisPrefix}Chan{chan}", $"{_redisPrefix}Client{clientId}", $"{_redisPrefix}ListChan" }, new object[] { clientId, chan });
- //pipe.HSet($"{_redisPrefix}Chan{chan}", clientId.ToString(), 0);
- //pipe.HSet($"{_redisPrefix}Client{clientId}", chan, 0);
- //pipe.HIncrBy($"{_redisPrefix}ListChan", chan, 1);
+ new[] { $"{_redisPrefix}Chan{chan}", $"{_redisPrefix}Client{clientIdStr}", $"{_redisPrefix}ListChan" }, new object[] { clientIdStr, chan });
}
pipe.EndPipe();
}
}
+
///
/// 离开群聊频道
///
/// 客户端id
/// 群聊频道名
- public void LeaveChan(long clientId, params string[] chans)
+ public void LeaveChan(TClientId clientId, params string[] chans)
{
if (chans?.Any() != true) return;
+ var clientIdStr = ConvertToInt64(clientId).ToString();
using (var pipe = _redis.StartPipe())
{
foreach (var chan in chans)
{
if (string.IsNullOrEmpty(chan)) continue;
pipe.Eval($"if redis.call('HDEL',KEYS[1],ARGV[1])==1 then redis.call('HDEL',KEYS[2],ARGV[2]) if redis.call('HINCRBY',KEYS[3],ARGV[2],-1)<=0 then redis.call('HDEL',KEYS[3],ARGV[2]) end end return 1",
- new[] { $"{_redisPrefix}Chan{chan}", $"{_redisPrefix}Client{clientId}", $"{_redisPrefix}ListChan" }, new object[] { clientId, chan });
- //pipe.HDel($"{_redisPrefix}Chan{chan}", clientId.ToString());
- //pipe.HDel($"{_redisPrefix}Client{clientId}", chan);
- //pipe.Eval($"if redis.call('HINCRBY', KEYS[1], '{chan}', '-1') <= 0 then redis.call('HDEL', KEYS[1], '{chan}') end return 1", new[] { $"{_redisPrefix}ListChan" });
+ new[] { $"{_redisPrefix}Chan{chan}", $"{_redisPrefix}Client{clientIdStr}", $"{_redisPrefix}ListChan" }, new object[] { clientIdStr, chan });
}
pipe.EndPipe();
}
}
+
///
/// 离开群聊频道
///
/// 群聊频道名
/// 客户端id
- public void LeaveChan(string chan, params long[] clientIds)
+ public void LeaveChan(string chan, params TClientId[] clientIds)
{
if (string.IsNullOrEmpty(chan)) return;
if (clientIds?.Any() != true) return;
@@ -209,25 +250,29 @@ public void LeaveChan(string chan, params long[] clientIds)
foreach (var clientId in clientIds)
{
if (string.IsNullOrEmpty(chan)) continue;
+ var clientIdStr = ConvertToInt64(clientId).ToString();
pipe.Eval($"if redis.call('HDEL',KEYS[1],ARGV[1])==1 then redis.call('HDEL',KEYS[2],ARGV[2]) if redis.call('HINCRBY',KEYS[3],ARGV[2],-1)<=0 then redis.call('HDEL',KEYS[3],ARGV[2]) end end return 1",
- new[] { $"{_redisPrefix}Chan{chan}", $"{_redisPrefix}Client{clientId}", $"{_redisPrefix}ListChan" }, new object[] { clientId, chan });
- //pipe.HDel($"{_redisPrefix}Chan{chan}", clientId.ToString());
- //pipe.HDel($"{_redisPrefix}Client{clientId}", chan);
- //pipe.Eval($"if redis.call('HINCRBY', KEYS[1], '{chan}', '-1') <= 0 then redis.call('HDEL', KEYS[1], '{chan}') end return 1", new[] { $"{_redisPrefix}ListChan" });
+ new[] { $"{_redisPrefix}Chan{chan}", $"{_redisPrefix}Client{clientIdStr}", $"{_redisPrefix}ListChan" }, new object[] { clientIdStr, chan });
}
pipe.EndPipe();
}
}
+
///
/// 获取群聊频道所有客户端id(测试)
///
/// 群聊频道名
///
- public long[] GetChanClientList(string chan)
+ public TClientId[] GetChanClientList(string chan)
{
- if (string.IsNullOrEmpty(chan)) return new long[0];
- return _redis.HKeys($"{_redisPrefix}Chan{chan}").Select(a => long.TryParse(a, out var tryval) ? tryval : 0).Where(a => a != 0).ToArray();
+ if (string.IsNullOrEmpty(chan)) return new TClientId[0];
+ return _redis.HKeys($"{_redisPrefix}Chan{chan}")
+ .Select(a => long.TryParse(a, out var tryval) ? tryval : 0)
+ .Where(a => a != 0)
+ .Select(a => ConvertToClientIdType(a))
+ .ToArray();
}
+
///
/// 清理群聊频道的离线客户端(测试)
///
@@ -272,15 +317,17 @@ public void ClearChanClient(string chan)
var ret = _redis.HGetAll($"{_redisPrefix}ListChan");
return ret.Select(a => (a.Key, a.Value));
}
+
///
/// 获取用户参与的所有群聊频道
///
/// 客户端id
///
- public string[] GetChanListByClientId(long clientId)
+ public string[] GetChanListByClientId(TClientId clientId)
{
- return _redis.HKeys($"{_redisPrefix}Client{clientId}");
+ return _redis.HKeys($"{_redisPrefix}Client{ConvertToInt64(clientId)}");
}
+
///
/// 获取群聊频道的在线人数
///
@@ -298,24 +345,37 @@ public long GetChanOnline(string chan)
/// 发送者的客户端id
/// 群聊频道名
/// 消息
- public void SendChanMessage(long senderClientId, string chan, object message)
+ public void SendChanMessage(TClientId senderClientId, string chan, object message)
{
- var sendArgs = _servers.Select(server => new ImSendEventArgs(server, senderClientId, message, false) { Chan = chan }).ToArray();
+ var sendArgs = _servers.Select(server => new ImSendEventArgs(server, senderClientId, message, false) { Chan = chan }).ToArray();
var messageJson = JsonConvert.SerializeObject(message, _jsonSerializerSettings);
using (var pipe = _redis.StartPipe())
{
foreach (var arg in sendArgs)
{
OnSend?.Invoke(this, arg);
- pipe.Publish($"{_redisPrefix}Server{arg.Server}", $"__FreeIM__(ChanMessage){JsonConvert.SerializeObject((senderClientId, chan, messageJson), _jsonSerializerSettings)}");
+ pipe.Publish($"{_redisPrefix}Server{arg.Server}", $"__FreeIM__(ChanMessage){JsonConvert.SerializeObject((ConvertToInt64(arg.SenderClientId), chan, messageJson), _jsonSerializerSettings)}");
}
pipe.EndPipe();
}
}
+
///
/// 发送广播消息
///
/// 消息
- public void SendBroadcastMessage(object message) => SendChanMessage(0, null, message);
+ public void SendBroadcastMessage(object message) => SendChanMessage(default, null, message);
#endregion
}
+
+///
+/// 兼容旧版本的 ImClient 类型别名(使用 long 作为 clientId)
+///
+public class ImClient : ImClient
+{
+ ///
+ /// 使用默认配置初始化 ImClient
+ ///
+ ///
+ public ImClient(ImClientOptions options) : base(options) { }
+}
diff --git a/FreeIM/ImClientOptions.cs b/FreeIM/ImClientOptions.cs
index 0979b84..8b0f68b 100644
--- a/FreeIM/ImClientOptions.cs
+++ b/FreeIM/ImClientOptions.cs
@@ -1,4 +1,4 @@
-using FreeRedis;
+using FreeRedis;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -26,16 +26,20 @@ public class ImClientOptions
public JsonSerializerSettings JsonSerializerSettings { get;set;}
}
-public class ImSendEventArgs : EventArgs
+///
+/// ImSendEventArgs 泛型版本
+///
+/// 客户端ID类型
+public class ImSendEventArgs : EventArgs
{
///
/// 发送者的客户端id
///
- public long SenderClientId { get; }
+ public TClientId SenderClientId { get; }
///
/// 接收者的客户端id
///
- public List ReceiveClientId { get; } = new List();
+ public List ReceiveClientId { get; } = new List();
public string Chan { get; internal set; }
///
/// imServer 服务器节点
@@ -50,11 +54,20 @@ public class ImSendEventArgs : EventArgs
///
public bool Receipt { get; }
- internal ImSendEventArgs(string server, long senderClientId, object message, bool receipt = false)
+ internal ImSendEventArgs(string server, TClientId senderClientId, object message, bool receipt = false)
{
this.Server = server;
this.SenderClientId = senderClientId;
this.Message = message;
this.Receipt = receipt;
}
-}
\ No newline at end of file
+}
+
+///
+/// 兼容旧版本的 ImSendEventArgs(使用 long 作为 clientId)
+///
+public class ImSendEventArgs : ImSendEventArgs
+{
+ internal ImSendEventArgs(string server, long senderClientId, object message, bool receipt = false)
+ : base(server, senderClientId, message, receipt) { }
+}
diff --git a/FreeIM/ImHelper.cs b/FreeIM/ImHelper.cs
index 36c40a5..630ae2d 100644
--- a/FreeIM/ImHelper.cs
+++ b/FreeIM/ImHelper.cs
@@ -1,8 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
///
-/// im 核心类 ImClient 实现的静态代理类
+/// im 核心类 ImClient 实现的静态代理类(兼容版本,使用 long 作为 clientId)
///
public static class ImHelper
{
@@ -48,12 +48,14 @@ public static void SendMessage(long senderClientId, IEnumerable receiveCli
///
///
public static bool HasOnline(long clientId) => Instance.HasOnline(clientId);
+
///
/// 判断客户端是否在线(多个)
///
///
///
public static bool[] HasOnline(IEnumerable clientIds) => Instance.HasOnline(clientIds);
+
///
/// 强制下线
///
@@ -77,24 +79,28 @@ public static void EventBus(
/// 客户端id
/// 群聊频道名
public static void JoinChan(long clientId, params string[] chans) => Instance.JoinChan(clientId, chans);
+
///
/// 离开群聊频道
///
/// 客户端id
/// 群聊频道名
public static void LeaveChan(long clientId, params string[] chans) => Instance.LeaveChan(clientId, chans);
+
///
/// 离开群聊频道
///
/// 群聊频道名
/// 客户端id
public static void LeaveChan(string chan, params long[] clientIds) => Instance.LeaveChan(chan, clientIds);
+
///
/// 获取群聊频道所有客户端id(测试)
///
/// 群聊频道名
///
public static long[] GetChanClientList(string chan) => Instance.GetChanClientList(chan);
+
///
/// 清理群聊频道的离线客户端(测试)
///
@@ -106,12 +112,14 @@ public static void EventBus(
///
/// 频道名和在线人数
public static IEnumerable<(string chan, long online)> GetChanList() => Instance.GetChanList();
+
///
/// 获取用户参与的所有群聊频道
///
/// 客户端id
///
public static string[] GetChanListByClientId(long clientId) => Instance.GetChanListByClientId(clientId);
+
///
/// 获取群聊频道的在线人数
///
@@ -125,7 +133,139 @@ public static void EventBus(
/// 发送者的客户端id
/// 群聊频道名
/// 消息
- public static void SendChanMessage(long senderClientId, string chan, object message) => Instance.SendChanMessage(senderClientId, chan, message);
+ public static void SendChanMessage(long senderClientId, string chan, object message) => Instance.SendChanMessage(senderClientId, chan, message);
+
+ ///
+ /// 发送广播消息
+ ///
+ /// 消息
+ public static void SendBroadcastMessage(object message) => Instance.SendBroadcastMessage(message);
+ #endregion
+}
+
+///
+/// ImHelper 泛型版本(预览版)
+///
+/// 客户端ID类型,支持 long、int、short、byte 等数值类型
+public static class ImHelper
+ where TClientId : struct, IComparable
+{
+ static ImClient _instance;
+ public static ImClient Instance => _instance ?? throw new Exception("使用前请初始化 ImHelper.Initialization(...);");
+
+ ///
+ /// 初始化 ImHelper
+ ///
+ ///
+ public static void Initialization(ImClientOptions options)
+ {
+ _instance = new ImClient(options);
+ }
+
+ ///
+ /// ImServer 连接前的负载、授权,返回 ws 目标地址,使用该地址连接 websocket 服务端
+ ///
+ /// 客户端id
+ /// 客户端相关信息,比如ip
+ /// websocket 地址:ws://xxxx/ws?token=xxx
+ public static string PrevConnectServer(TClientId clientId, string clientMetaData) => Instance.PrevConnectServer(clientId, clientMetaData);
+
+ ///
+ /// 向指定的多个客户端id发送消息
+ ///
+ /// 发送者的客户端id
+ /// 接收者的客户端id
+ /// 消息
+ /// 是否回执
+ public static void SendMessage(TClientId senderClientId, IEnumerable receiveClientId, object message, bool receipt = false) =>
+ Instance.SendMessage(senderClientId, receiveClientId, message, receipt);
+
+ ///
+ /// 获取所在线客户端id
+ ///
+ ///
+ public static IEnumerable GetClientListByOnline() => Instance.GetClientListByOnline();
+
+ ///
+ /// 判断客户端是否在线
+ ///
+ ///
+ ///
+ public static bool HasOnline(TClientId clientId) => Instance.HasOnline(clientId);
+
+ ///
+ /// 判断客户端是否在线(多个)
+ ///
+ ///
+ ///
+ public static bool[] HasOnline(IEnumerable clientIds) => Instance.HasOnline(clientIds);
+
+ ///
+ /// 强制下线
+ ///
+ ///
+ public static void ForceOffline(TClientId clientId) => Instance.ForceOffline(clientId);
+
+ ///
+ /// 事件订阅
+ ///
+ /// 上线
+ /// 下线
+ public static void EventBus(
+ Action<(TClientId clientId, string clientMetaData)> online,
+ Action<(TClientId clientId, string clientMetaData)> offline) => Instance.EventBus(online, offline);
+
+ #region 群聊频道,每次上线都必须重新加入
+
+ ///
+ /// 加入群聊频道,每次上线都必须重新加入
+ ///
+ /// 客户端id
+ /// 群聊频道名
+ public static void JoinChan(TClientId clientId, params string[] chans) => Instance.JoinChan(clientId, chans);
+
+ ///
+ /// 离开群聊频道
+ ///
+ /// 客户端id
+ /// 群聊频道名
+ public static void LeaveChan(TClientId clientId, params string[] chans) => Instance.LeaveChan(clientId, chans);
+
+ ///
+ /// 离开群聊频道
+ ///
+ /// 群聊频道名
+ /// 客户端id
+ public static void LeaveChan(string chan, params TClientId[] clientIds) => Instance.LeaveChan(chan, clientIds);
+
+ ///
+ /// 获取群聊频道所有客户端id(测试)
+ ///
+ /// 群聊频道名
+ ///
+ public static TClientId[] GetChanClientList(string chan) => Instance.GetChanClientList(chan);
+
+ ///
+ /// 清理群聊频道的离线客户端(测试)
+ ///
+ /// 群聊频道名
+ public static void ClearChanClient(string chan) => Instance.ClearChanClient(chan);
+
+ ///
+ /// 获取用户参与的所有群聊频道
+ ///
+ /// 客户端id
+ ///
+ public static string[] GetChanListByClientId(TClientId clientId) => Instance.GetChanListByClientId(clientId);
+
+ ///
+ /// 发送群聊消息,所有在线的用户将收到消息
+ ///
+ /// 发送者的客户端id
+ /// 群聊频道名
+ /// 消息
+ public static void SendChanMessage(TClientId senderClientId, string chan, object message) => Instance.SendChanMessage(senderClientId, chan, message);
+
///
/// 发送广播消息
///
diff --git a/readme.md b/readme.md
index 97d7610..4ddead8 100644
--- a/readme.md
+++ b/readme.md
@@ -1,9 +1,8 @@
-# FreeIM v2.0.0
+# FreeIM v3.0.0
- - 调整:已将 ClientId Guid 改为 long;
- - 修复:JoinChan/LeaveChan 数量统计问题;
- - 增加:SendBroadcastMessage 广播消息;
- - 优化:SendChanMessage 性能;
+ - **新增**:clientId 支持泛型,可自定义类型(如 long、int、short、byte 等);
+ - **兼容**:保留非泛型 ImClient/ImHelper 类,确保向后兼容;
+ - **优化**:内部实现泛型化,提升类型安全;
FreeIM 使用 websocket 协议实现简易、高性能(单机支持5万+连接)、集群即时通讯组件,支持点对点通讯、群聊通讯、上线下线事件消息等众多实用性功能。 `ImCore` 已正式改名为 `FreeIM`。
@@ -15,42 +14,71 @@ FreeIM 使用 websocket 协议实现简易、高性能(单机支持5万+连接
> dotnet add package FreeIM
-## ImServer 服务端
+## 快速开始
+
+### 使用默认配置(long 作为 clientId)
```csharp
public void Configure(IApplicationBuilder app)
{
+ // WebApi 业务端
+ ImHelper.Initialization(new ImClientOptions
+ {
+ Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
+ Servers = new[] { "127.0.0.1:6001" }
+ });
+
+ ImHelper.EventBus(
+ t => Console.WriteLine(t.clientId + "上线了"),
+ t => Console.WriteLine(t.clientId + "下线了"));
+}
+```
+
+```csharp
+public void Configure(IApplicationBuilder app)
+{
+ // ImServer 服务端
app.UseFreeImServer(new ImServerOptions
{
Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
- Servers = new[] { "127.0.0.1:6001" }, //集群配置
+ Servers = new[] { "127.0.0.1:6001" },
Server = "127.0.0.1:6001"
});
}
//dotnet run --urls=http://127.0.0.1:6001
```
+
> 一套永远不需要迭代更新的 `ImServer` 服务端,支持 .NET8.0 AOT 发布(C++运行时)。
-## WebApi 业务端
+### 使用泛型版本(自定义 clientId 类型)
```csharp
-public void Configure(IApplicationBuilder app)
+// 使用 int 作为 clientId
+ImHelper.Initialization(new ImClientOptions
{
- //...
+ Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
+ Servers = new[] { "127.0.0.1:6001" }
+});
- ImHelper.Initialization(new ImClientOptions
- {
- Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
- Servers = new[] { "127.0.0.1:6001" }
- });
+ImHelper.EventBus(
+ t => Console.WriteLine(t.clientId + "上线了"),
+ t => Console.WriteLine(t.clientId + "下线了"));
+```
- ImHelper.EventBus(
- t => Console.WriteLine(t.clientId + "上线了"),
- t => Console.WriteLine(t.clientId + "下线了"));
-}
+```csharp
+// 使用 long 作为 clientId(显式指定)
+ImHelper.Initialization(new ImClientOptions
+{
+ Redis = new FreeRedis.RedisClient("127.0.0.1:6379,poolsize=5"),
+ Servers = new[] { "127.0.0.1:6001" }
+});
```
-| ImHelper方法 | 参数 | 描述 |
+## API 参考
+
+### ImHelper(兼容版本,使用 long)
+
+| 方法 | 参数 | 描述 |
| - | - | - |
| PrevConnectServer | (clientId, string) | 在终端准备连接 websocket 前调用 |
| SendMessage | (发送者, 接收者, 消息内容, 是否回执) | 发送消息 |
@@ -59,7 +87,9 @@ public void Configure(IApplicationBuilder app)
| ForceOffline | clientId | 强制下线 |
| EventBus | (上线委托, 离线委托) | socket上线与下线事件 |
-| 频道 | 参数 | 描述 |
+### 频道相关
+
+| 方法 | 参数 | 描述 |
| - | - | - |
| JoinChan | (clientId, 频道名) | 加入 |
| LeaveChan | (clientId, 频道名) | 离开 |
@@ -68,10 +98,26 @@ public void Configure(IApplicationBuilder app)
| GetChanListByClientId | (clientId) | 获取用户参与的所有频道 |
| GetChanOnline | (频道名) | 获取频道的在线人数 |
| SendChanMessage | (clientId, 频道名, 消息内容) | 发送消息,所有在线的用户将收到消息 |
-| SendBroadcastMessage | (clientId, 频道名, 消息内容) | 发送广播消息 |
+| SendBroadcastMessage | (message) | 发送广播消息 |
+
+### ImHelper(泛型版本)
+
+| 方法 | 参数 | 描述 |
+| - | - | - |
+| PrevConnectServer | (clientId, string) | 在终端准备连接 websocket 前调用 |
+| SendMessage | (发送者, 接收者, 消息内容, 是否回执) | 发送消息 |
+| GetClientListByOnline | - | 返回所有在线clientId |
+| HasOnline | clientId | 判断客户端是否在线 |
+| ForceOffline | clientId | 强制下线 |
+| EventBus | (上线委托, 离线委托) | socket上线与下线事件 |
+
+支持的 TClientId 类型:`long`、`int`、`short`、`byte`、`ulong`、`uint`、`ushort`
+
+## clientId 说明
- clientId 应该与用户id相同,或者关联;
- 频道适用临时的群聊需求,如聊天室、讨论区;
+- 泛型版本允许你使用不同的数值类型作为 clientId,提升类型安全;
> ImHelper 支持 .NetFramework 4.5+、.NetStandard 2.0
@@ -168,7 +214,7 @@ FreeIM 强依赖 redis-server 组件功能:
## 集群分区
-单个 `ImServer` 实例支持多少个客户端连接,3万?如果在线用户有10万人,怎么办???
+单个 `ImServer` 实例支持多少个客户端连接,3万?如果在线用户有10万人,怎么办??
部署 4 个 `ImServer`:
diff --git a/tests/FreeIM.Tests/FreeIM.Tests.csproj b/tests/FreeIM.Tests/FreeIM.Tests.csproj
new file mode 100644
index 0000000..cd2680a
--- /dev/null
+++ b/tests/FreeIM.Tests/FreeIM.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/FreeIM.Tests/ImClientTests.cs b/tests/FreeIM.Tests/ImClientTests.cs
new file mode 100644
index 0000000..5afd255
--- /dev/null
+++ b/tests/FreeIM.Tests/ImClientTests.cs
@@ -0,0 +1,106 @@
+using FreeRedis;
+using Newtonsoft.Json;
+
+namespace FreeIM.Tests;
+
+///
+/// ImClient 泛型类单元测试(不依赖 Redis 的测试)
+///
+public class ImClientTests
+{
+ ///
+ /// 测试 ImClient 构造函数
+ ///
+ [Fact]
+ public void Constructor_Long_ClientId_ShouldSuccess()
+ {
+ var options = new ImClientOptions
+ {
+ Redis = new RedisClient("127.0.0.1:6379"),
+ Servers = new[] { "127.0.0.1:6001" }
+ };
+
+ var client = new ImClient(options);
+ Assert.NotNull(client);
+ }
+
+ ///
+ /// 测试 ImClient 构造函数
+ ///
+ [Fact]
+ public void Constructor_Int_ClientId_ShouldSuccess()
+ {
+ var options = new ImClientOptions
+ {
+ Redis = new RedisClient("127.0.0.1:6379"),
+ Servers = new[] { "127.0.0.1:6001" }
+ };
+
+ var client = new ImClient(options);
+ Assert.NotNull(client);
+ }
+
+ ///
+ /// 测试 ImClient 构造函数
+ ///
+ [Fact]
+ public void Constructor_Short_ClientId_ShouldSuccess()
+ {
+ var options = new ImClientOptions
+ {
+ Redis = new RedisClient("127.0.0.1:6379"),
+ Servers = new[] { "127.0.0.1:6001" }
+ };
+
+ var client = new ImClient(options);
+ Assert.NotNull(client);
+ }
+
+ ///
+ /// 测试 ImClient 构造函数
+ ///
+ [Fact]
+ public void Constructor_Byte_ClientId_ShouldSuccess()
+ {
+ var options = new ImClientOptions
+ {
+ Redis = new RedisClient("127.0.0.1:6379"),
+ Servers = new[] { "127.0.0.1:6001" }
+ };
+
+ var client = new ImClient(options);
+ Assert.NotNull(client);
+ }
+
+ ///
+ /// 测试构造函数参数验证 - 缺少 Redis
+ ///
+ [Fact]
+ public void Constructor_ShouldThrowWithoutRedis()
+ {
+ var options = new ImClientOptions
+ {
+ Redis = null!,
+ Servers = new[] { "127.0.0.1:6001" }
+ };
+
+ var exception = Assert.Throws(() => new ImClient(options));
+ Assert.Equal("ImClientOptions.Redis 参数不能为空", exception.Message);
+ }
+
+ ///
+ /// 测试构造函数参数验证 - 缺少 Servers
+ ///
+ [Fact]
+ public void Constructor_ShouldThrowWithoutServers()
+ {
+ var options = new ImClientOptions
+ {
+ Redis = new RedisClient("127.0.0.1:6379"),
+ Servers = Array.Empty()
+ };
+
+ var exception = Assert.Throws(() => new ImClient(options));
+ Assert.Equal("ImClientOptions.Servers 参数不能为空", exception.Message);
+ }
+}
diff --git a/tests/FreeIM.Tests/ImHelperGenericTests.cs b/tests/FreeIM.Tests/ImHelperGenericTests.cs
new file mode 100644
index 0000000..e9dc402
--- /dev/null
+++ b/tests/FreeIM.Tests/ImHelperGenericTests.cs
@@ -0,0 +1,167 @@
+using FreeRedis;
+
+namespace FreeIM.Tests;
+
+///
+/// ImHelper 泛型版本单元测试(不依赖 Redis 的测试)
+///
+public class ImHelperGenericTests
+{
+ [Fact]
+ public void Initialization_Long_ShouldSetInstance()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ Assert.NotNull(ImHelper.Instance);
+ }
+
+ [Fact]
+ public void Initialization_Int_ShouldSetInstance()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ Assert.NotNull(ImHelper.Instance);
+ }
+
+ [Fact]
+ public void PrevConnectServer_Long_ShouldReturnWebSocketUrl()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" }, PathMatch = "/ws" };
+ ImHelper.Initialization(options);
+ var url = ImHelper.PrevConnectServer(12345L, "192.168.1.1");
+ Assert.Contains("ws://", url);
+ Assert.Contains("token=", url);
+ }
+
+ [Fact]
+ public void PrevConnectServer_Int_ShouldReturnWebSocketUrl()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" }, PathMatch = "/ws" };
+ ImHelper.Initialization(options);
+ var url = ImHelper.PrevConnectServer(12345, "192.168.1.1");
+ Assert.Contains("ws://", url);
+ Assert.Contains("token=", url);
+ }
+
+ [Fact]
+ public void SendMessage_Long_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendMessage(100L, new long[] { 1, 2, 3 }, new { message = "test" });
+ }
+
+ [Fact]
+ public void SendMessage_Int_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendMessage(100, new int[] { 1, 2, 3 }, new { message = "test" });
+ }
+
+ [Fact]
+ public void ForceOffline_Long_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.ForceOffline(999999L);
+ }
+
+ [Fact]
+ public void ForceOffline_Int_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.ForceOffline(999999);
+ }
+
+ [Fact]
+ public void JoinChan_Long_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.JoinChan(12345L, "channel1");
+ }
+
+ [Fact]
+ public void JoinChan_Int_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.JoinChan(12345, "channel1");
+ }
+
+ [Fact]
+ public void LeaveChan_Long_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.LeaveChan(12345L, "channel1");
+ }
+
+ [Fact]
+ public void LeaveChan_Int_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.LeaveChan(12345, "channel1");
+ }
+
+ [Fact]
+ public void LeaveChan_Params_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.LeaveChan("channel1", 1, 2, 3);
+ }
+
+ [Fact]
+ public void GetChanClientList_Int_ShouldReturnIntArray()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ var result = ImHelper.GetChanClientList("channel1");
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void ClearChanClient_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.ClearChanClient("channel1");
+ }
+
+ [Fact]
+ public void GetChanListByClientId_Int_ShouldReturnArray()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ var result = ImHelper.GetChanListByClientId(12345);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void SendChanMessage_Int_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendChanMessage(12345, "channel1", new { message = "test" });
+ }
+
+ [Fact]
+ public void SendBroadcastMessage_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendBroadcastMessage(new { message = "broadcast" });
+ }
+
+ [Fact]
+ public void Instance_WithoutInitialization_ShouldThrow()
+ {
+ typeof(ImHelper).GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static)?.SetValue(null, null);
+ var exception = Assert.Throws(() => ImHelper.Instance);
+ Assert.Equal("使用前请初始化 ImHelper.Initialization(...);", exception.Message);
+ }
+}
diff --git a/tests/FreeIM.Tests/ImHelperTests.cs b/tests/FreeIM.Tests/ImHelperTests.cs
new file mode 100644
index 0000000..c78ccc0
--- /dev/null
+++ b/tests/FreeIM.Tests/ImHelperTests.cs
@@ -0,0 +1,117 @@
+using FreeRedis;
+
+namespace FreeIM.Tests;
+
+///
+/// ImHelper 非泛型版本单元测试(向后兼容性测试)
+///
+public class ImHelperTests
+{
+ [Fact]
+ public void Initialization_ShouldSetInstance()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ Assert.NotNull(ImHelper.Instance);
+ }
+
+ [Fact]
+ public void PrevConnectServer_ShouldReturnWebSocketUrl()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" }, PathMatch = "/ws" };
+ ImHelper.Initialization(options);
+ var url = ImHelper.PrevConnectServer(12345L, "192.168.1.1");
+ Assert.Contains("ws://", url);
+ Assert.Contains("token=", url);
+ }
+
+ [Fact]
+ public void SendMessage_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendMessage(100L, new long[] { 1, 2, 3 }, new { message = "test" });
+ }
+
+ [Fact]
+ public void ForceOffline_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.ForceOffline(999999L);
+ }
+
+ [Fact]
+ public void JoinChan_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.JoinChan(12345L, "channel1");
+ }
+
+ [Fact]
+ public void LeaveChan_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.LeaveChan(12345L, "channel1");
+ }
+
+ [Fact]
+ public void LeaveChan_Params_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.LeaveChan("channel1", 1L, 2L, 3L);
+ }
+
+ [Fact]
+ public void GetChanClientList_ShouldReturnArray()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ var result = ImHelper.GetChanClientList("channel1");
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void ClearChanClient_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.ClearChanClient("channel1");
+ }
+
+ [Fact]
+ public void GetChanListByClientId_ShouldReturnArray()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ var result = ImHelper.GetChanListByClientId(12345L);
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void SendChanMessage_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendChanMessage(12345L, "channel1", new { message = "test" });
+ }
+
+ [Fact]
+ public void SendBroadcastMessage_ShouldNotThrow()
+ {
+ var options = new ImClientOptions { Redis = new RedisClient("127.0.0.1:6379"), Servers = new[] { "127.0.0.1:6001" } };
+ ImHelper.Initialization(options);
+ ImHelper.SendBroadcastMessage(new { message = "broadcast" });
+ }
+
+ [Fact]
+ public void Instance_WithoutInitialization_ShouldThrow()
+ {
+ typeof(ImHelper).GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static)?.SetValue(null, null);
+ var exception = Assert.Throws(() => ImHelper.Instance);
+ Assert.Equal("使用前请初始化 ImHelper.Initialization(...);", exception.Message);
+ }
+}
diff --git a/tests/FreeIM.Tests/TypeConversionTests.cs b/tests/FreeIM.Tests/TypeConversionTests.cs
new file mode 100644
index 0000000..b372c4a
--- /dev/null
+++ b/tests/FreeIM.Tests/TypeConversionTests.cs
@@ -0,0 +1,179 @@
+using FreeRedis;
+
+namespace FreeIM.Tests;
+
+///
+/// 类型转换逻辑单元测试
+///
+public class TypeConversionTests
+{
+ private readonly RedisClient _redis = new RedisClient("127.0.0.1:6379");
+
+ [Fact]
+ public void ConvertToInt64_Long_ShouldReturnSameValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [12345L]);
+ Assert.Equal(12345L, result);
+ }
+
+ [Fact]
+ public void ConvertToInt64_Int_ShouldReturnCorrectValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [12345]);
+ Assert.Equal(12345L, result);
+ }
+
+ [Fact]
+ public void ConvertToInt64_Short_ShouldReturnCorrectValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [(short)12345]);
+ Assert.Equal(12345L, result);
+ }
+
+ [Fact]
+ public void ConvertToInt64_Byte_ShouldReturnCorrectValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [(byte)255]);
+ Assert.Equal(255L, result);
+ }
+
+ [Fact]
+ public void ConvertToClientIdType_Long_ShouldReturnSameValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [12345L]);
+ Assert.Equal(12345L, result);
+ }
+
+ [Fact]
+ public void ConvertToClientIdType_Int_ShouldReturnCorrectValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [12345L]);
+ Assert.Equal(12345, result);
+ }
+
+ [Fact]
+ public void ConvertToClientIdType_Short_ShouldReturnCorrectValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [12345L]);
+ Assert.Equal((short)12345, result);
+ }
+
+ [Fact]
+ public void ConvertToClientIdType_Byte_ShouldReturnCorrectValue()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var method = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var result = method.Invoke(client, [255L]);
+ Assert.Equal((byte)255, result);
+ }
+
+ [Fact]
+ public void GetChanClientList_Long_ShouldReturnLongArray()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var result = client.GetChanClientList("channel1");
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void GetChanClientList_Int_ShouldReturnIntArray()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var result = client.GetChanClientList("channel1");
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void SendMessage_Long_ReceiverShouldBeLong()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ client.SendMessage(100L, new long[] { 1L, 2L, 3L }, new { message = "test" });
+ }
+
+ [Fact]
+ public void SendMessage_Int_ReceiverShouldBeInt()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ client.SendMessage(100, new int[] { 1, 2, 3 }, new { message = "test" });
+ }
+
+ [Fact]
+ public void LeaveChan_Params_Long_ShouldAcceptLongArray()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ client.LeaveChan("channel1", 1L, 2L, 3L);
+ }
+
+ [Fact]
+ public void LeaveChan_Params_Int_ShouldAcceptIntArray()
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ client.LeaveChan("channel1", 1, 2, 3);
+ }
+
+ [Theory]
+ [InlineData(1L)]
+ [InlineData(100L)]
+ public void ConvertToInt64_Long_RoundTrip_ShouldPreserveValue(long input)
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var convertMethod = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var backMethod = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var longValue = convertMethod.Invoke(client, [input]);
+ var result = backMethod.Invoke(client, [longValue]);
+ Assert.Equal(input, result);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(100)]
+ public void ConvertToInt64_Int_RoundTrip_ShouldPreserveValue(int input)
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var convertMethod = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var backMethod = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var longValue = convertMethod.Invoke(client, [input]);
+ var result = backMethod.Invoke(client, [longValue]);
+ Assert.Equal(input, result);
+ }
+
+ [Theory]
+ [InlineData((short)1)]
+ [InlineData((short)100)]
+ public void ConvertToInt64_Short_RoundTrip_ShouldPreserveValue(short input)
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var convertMethod = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var backMethod = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var longValue = convertMethod.Invoke(client, [input]);
+ var result = backMethod.Invoke(client, [longValue]);
+ Assert.Equal(input, result);
+ }
+
+ [Theory]
+ [InlineData((byte)1)]
+ [InlineData((byte)100)]
+ public void ConvertToInt64_Byte_RoundTrip_ShouldPreserveValue(byte input)
+ {
+ var client = new ImClient(new ImClientOptions { Redis = _redis, Servers = new[] { "127.0.0.1:6001" } });
+ var convertMethod = typeof(ImClient).GetMethod("ConvertToInt64", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var backMethod = typeof(ImClient).GetMethod("ConvertToClientIdType", BindingFlags.NonPublic | BindingFlags.Instance)!;
+ var longValue = convertMethod.Invoke(client, [input]);
+ var result = backMethod.Invoke(client, [longValue]);
+ Assert.Equal(input, result);
+ }
+}
diff --git a/tests/FreeIM.Tests/Usings.cs b/tests/FreeIM.Tests/Usings.cs
new file mode 100644
index 0000000..2da4c7d
--- /dev/null
+++ b/tests/FreeIM.Tests/Usings.cs
@@ -0,0 +1,7 @@
+global using Xunit;
+global using FreeRedis;
+global using FreeIM;
+global using System;
+global using System.Collections.Generic;
+global using System.Linq;
+global using System.Reflection;