mirror of
https://github.com/2881099/FreeIM.git
synced 2026-10-23 00:58:52 +08:00
243 lines
8.8 KiB
C#
243 lines
8.8 KiB
C#
using CSRedis;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
|
|
/// <summary>
|
|
/// im 核心类实现的配置所需
|
|
/// </summary>
|
|
public class ImClientOptions
|
|
{
|
|
/// <summary>
|
|
/// CSRedis 对象,用于存储数据和发送消息
|
|
/// </summary>
|
|
public CSRedisClient Redis { get; set; }
|
|
/// <summary>
|
|
/// 负载的服务端
|
|
/// </summary>
|
|
public string[] Servers { get; set; }
|
|
/// <summary>
|
|
/// websocket请求的路径,默认值:/ws
|
|
/// </summary>
|
|
public string PathMatch { get; set; } = "/ws";
|
|
}
|
|
|
|
/// <summary>
|
|
/// im 核心类实现
|
|
/// </summary>
|
|
public class ImClient
|
|
{
|
|
protected CSRedisClient _redis;
|
|
protected string[] _servers;
|
|
protected string _redisPrefix;
|
|
protected string _pathMatch;
|
|
|
|
public EventHandler Error;
|
|
|
|
/// <summary>
|
|
/// 初始化 imclient
|
|
/// </summary>
|
|
/// <param name="options"></param>
|
|
public ImClient(ImClientOptions options)
|
|
{
|
|
if (options.Redis == null) throw new ArgumentException("ImClientOptions.Redis 参数不能为空");
|
|
if (options.Servers.Any() == false) throw new ArgumentException("ImClientOptions.Servers 参数不能为空");
|
|
_redis = options.Redis;
|
|
_servers = options.Servers;
|
|
_redisPrefix = $"wsim{options.PathMatch.Replace('/', '_')}";
|
|
_pathMatch = options.PathMatch ?? "/ws";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 负载分区规则:取clientId后四位字符,转成10进制数字0-65535,求模
|
|
/// </summary>
|
|
/// <param name="clientId">客户端id</param>
|
|
/// <returns></returns>
|
|
protected string SelectServer(Guid clientId)
|
|
{
|
|
var servers_idx = int.Parse(clientId.ToString("N").Substring(28), NumberStyles.HexNumber) % _servers.Length;
|
|
if (servers_idx >= _servers.Length) servers_idx = 0;
|
|
return _servers[servers_idx];
|
|
}
|
|
|
|
/// <summary>
|
|
/// ImServer 连接前的负载、授权,返回 ws 目标地址,使用该地址连接 websocket 服务端
|
|
/// </summary>
|
|
/// <param name="clientId">客户端id</param>
|
|
/// <param name="clientMetaData">客户端相关信息,比如ip</param>
|
|
/// <returns>websocket 地址:ws://xxxx/ws?token=xxx</returns>
|
|
public string PrevConnectServer(Guid 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)), 10);
|
|
return $"ws://{server}{_pathMatch}?token={token}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 向指定的多个客户端id发送消息
|
|
/// </summary>
|
|
/// <param name="senderClientId">发送者的客户端id</param>
|
|
/// <param name="receiveClientId">接收者的客户端id</param>
|
|
/// <param name="message">消息</param>
|
|
/// <param name="receipt">是否回执</param>
|
|
public void SendMessage(Guid senderClientId, Guid[] receiveClientId, object message, bool receipt = false)
|
|
{
|
|
receiveClientId = receiveClientId.Distinct().ToArray();
|
|
Dictionary<string, List<Guid>> redata = new Dictionary<string, List<Guid>>();
|
|
|
|
foreach (var uid in receiveClientId)
|
|
{
|
|
string server = SelectServer(uid);
|
|
if (redata.ContainsKey(server) == false) redata.Add(server, new List<Guid>());
|
|
redata[server].Add(uid);
|
|
}
|
|
var messageJson = JsonConvert.SerializeObject(message);
|
|
foreach (string server in redata.Keys)
|
|
_redis.Publish($"{_redisPrefix}Server{server}",
|
|
JsonConvert.SerializeObject((senderClientId, redata[server], messageJson, receipt)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取所在线客户端id
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public string[] GetClientListByOnline()
|
|
{
|
|
return _redis.HKeys($"{_redisPrefix}Online");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 事件订阅
|
|
/// </summary>
|
|
/// <param name="online">上线</param>
|
|
/// <param name="offline">下线</param>
|
|
public void EventBus(
|
|
Action<(Guid clientId, string clientMetaData)> online,
|
|
Action<(Guid clientId, string clientMetaData)> offline)
|
|
{
|
|
_redis.Subscribe(
|
|
($"evt_{_redisPrefix}Online", msg => online(JsonConvert.DeserializeObject<(Guid clientId, string clientMetaData)>(msg.Body))),
|
|
($"evt_{_redisPrefix}Offline", msg => offline(JsonConvert.DeserializeObject<(Guid clientId, string clientMetaData)>(msg.Body))));
|
|
}
|
|
|
|
#region 群聊频道,每次上线都必须重新加入
|
|
|
|
/// <summary>
|
|
/// 加入群聊频道,每次上线都必须重新加入
|
|
/// </summary>
|
|
/// <param name="clientId">客户端id</param>
|
|
/// <param name="chan">群聊频道名</param>
|
|
public void JoinChan(Guid clientId, string chan)
|
|
{
|
|
_redis.StartPipe(a => a
|
|
.HSet($"{_redisPrefix}Chan{chan}", clientId.ToString(), 0)
|
|
.HSet($"{_redisPrefix}Client{clientId}", chan, 0)
|
|
.HIncrBy($"{_redisPrefix}ListChan", chan, 1));
|
|
}
|
|
/// <summary>
|
|
/// 离开群聊频道
|
|
/// </summary>
|
|
/// <param name="clientId">客户端id</param>
|
|
/// <param name="chans">群聊频道名</param>
|
|
public void LeaveChan(Guid clientId, params string[] chans)
|
|
{
|
|
if (chans?.Any() != true) return;
|
|
using (var pipe = _redis.StartPipe())
|
|
{
|
|
foreach (var chan in chans)
|
|
pipe
|
|
.HDel($"{_redisPrefix}Chan{chan}", clientId.ToString())
|
|
.HDel($"{_redisPrefix}Client{clientId}", chan)
|
|
.Eval($"if redis.call('HINCRBY', KEYS[1], '{chan}', '-1') <= 0 then redis.call('HDEL', KEYS[1], '{chan}') end return 1",
|
|
$"{_redisPrefix}ListChan");
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// 获取群聊频道所有客户端id(测试)
|
|
/// </summary>
|
|
/// <param name="chan">群聊频道名</param>
|
|
/// <returns></returns>
|
|
public Guid[] GetChanClientList(string chan)
|
|
{
|
|
return _redis.HKeys($"{_redisPrefix}Chan{chan}").Select(a => Guid.Parse(a)).ToArray();
|
|
}
|
|
/// <summary>
|
|
/// 清理群聊频道的离线客户端(测试)
|
|
/// </summary>
|
|
/// <param name="chan">群聊频道名</param>
|
|
public void ClearChanClient(string chan)
|
|
{
|
|
var websocketIds = _redis.HKeys($"{_redisPrefix}Chan{chan}");
|
|
var offline = new List<string>();
|
|
var span = websocketIds.AsSpan();
|
|
var start = span.Length;
|
|
while (start > 0)
|
|
{
|
|
start = start - 10;
|
|
var length = 10;
|
|
if (start < 0)
|
|
{
|
|
length = start + 10;
|
|
start = 0;
|
|
}
|
|
var slice = span.Slice(start, length);
|
|
var hvals = _redis.HMGet($"{_redisPrefix}Online", slice.ToArray().Select(b => b.ToString()).ToArray());
|
|
for (var a = length - 1; a >= 0; a--)
|
|
{
|
|
if (string.IsNullOrEmpty(hvals[a]))
|
|
{
|
|
offline.Add(span[start + a]);
|
|
span[start + a] = null;
|
|
}
|
|
}
|
|
}
|
|
//删除离线订阅
|
|
if (offline.Any()) _redis.HDel($"{_redisPrefix}Chan{chan}", offline.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取所有群聊频道和在线人数
|
|
/// </summary>
|
|
/// <returns>频道名和在线人数</returns>
|
|
public IEnumerable<(string chan, long online)> GetChanList()
|
|
{
|
|
var ret = _redis.HGetAll<long>($"{_redisPrefix}ListChan");
|
|
return ret.Select(a => (a.Key, a.Value));
|
|
}
|
|
/// <summary>
|
|
/// 获取用户参与的所有群聊频道
|
|
/// </summary>
|
|
/// <param name="clientId">客户端id</param>
|
|
/// <returns></returns>
|
|
public string[] GetChanListByClientId(Guid clientId)
|
|
{
|
|
return _redis.HKeys($"{_redisPrefix}Client{clientId}");
|
|
}
|
|
/// <summary>
|
|
/// 获取群聊频道的在线人数
|
|
/// </summary>
|
|
/// <param name="chan">群聊频道名</param>
|
|
/// <returns>在线人数</returns>
|
|
public long GetChanOnline(string chan)
|
|
{
|
|
return _redis.HGet<long>($"{_redisPrefix}ListChan", chan);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 发送群聊消息,所有在线的用户将收到消息
|
|
/// </summary>
|
|
/// <param name="senderClientId">发送者的客户端id</param>
|
|
/// <param name="chan">群聊频道名</param>
|
|
/// <param name="message">消息</param>
|
|
public void SendChanMessage(Guid senderClientId, string chan, object message)
|
|
{
|
|
var websocketIds = _redis.HKeys($"{_redisPrefix}Chan{chan}");
|
|
SendMessage(Guid.Empty, websocketIds.Where(a => !string.IsNullOrEmpty(a)).Select(a => Guid.TryParse(a, out var tryuuid) ? tryuuid : Guid.Empty).ToArray(), message);
|
|
}
|
|
|
|
#endregion
|
|
}
|