ImClient.cs
using FreeRedis;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
///
/// im 核心类实现的配置所需
///
public clast ImClientOptions
{
///
/// CSRedis 对象,用于存储数据和发送消息
///
public RedisClient Redis { get; set; }
///
/// 负载的服务端
///
public string[] Servers { get; set; }
///
/// websocket请求的路径,默认值:/ws
///
public string PathMatch { get; set; } = "/ws";
}
public clast ImSendEventArgs : EventArgs
{
///
/// 发送者的客户端id
///
public Guid SenderClientId { get; }
///
/// 接收者的客户端id
///
public List ReceiveClientId { get; } = new List();
///
/// imServer 服务器节点
///
public string Server { get; }
///
/// 消息
///
public object Message { get; }
///
/// 是否回执
///
public bool Receipt { get; }
internal ImSendEventArgs(string server, Guid senderClientId, object message, bool receipt = false)
{
this.Server = server;
this.SenderClientId = senderClientId;
this.Message = message;
this.Receipt = receipt;
}
}
///
/// im 核心类实现
///
public clast ImClient
{
protected RedisClient _redis;
protected string[] _servers;
protected string _redisPrefix;
protected string _pathMatch;
///
/// 推送消息的事件,可审查推向哪个Server节点
///
public EventHandler OnSend;
///
/// 初始化 imclient
///
///
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";
}
///
/// 负载分区规则:取clientId后四位字符,转成10进制数字0-65535,求模
///
/// 客户端id
///
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];
}
///
/// ImServer 连接前的负载、授权,返回 ws 目标地址,使用该地址连接 websocket 服务端
///
/// 客户端id
/// 客户端相关信息,比如ip
/// websocket 地址:ws://yyyx/ws?token=yyy
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}";
}
///
/// 向指定的多个客户端id发送消息
///
/// 发送者的客户端id
/// 接收者的客户端id
/// 消息
/// 是否回执
public void SendMessage(Guid senderClientId, IEnumerable receiveClientId, object message, bool receipt = false)
{
receiveClientId = receiveClientId.Distinct().ToArray();
Dictionary redata = new Dictionary();
foreach (var uid in receiveClientId)
{
string server = SelectServer(uid);
if (redata.ContainsKey(server) == false) redata.Add(server, new ImSendEventArgs(server, senderClientId, message, receipt));
redata[server].ReceiveClientId.Add(uid);
}
var messageJson = JsonConvert.SerializeObject(message);
foreach (var sendArgs in redata.Values)
{
OnSend?.Invoke(this, sendArgs);
_redis.Publish($"{_redisPrefix}Server{sendArgs.Server}",
JsonConvert.SerializeObject((senderClientId, sendArgs.ReceiveClientId, messageJson, sendArgs.Receipt)));
}
}
///
/// 获取所在线客户端id
///
///
public IEnumerable GetClientListByOnline()
{
return _redis.HKeys($"{_redisPrefix}Online").Select(a => Guid.TryParse(a, out var tryguid) ? tryguid : Guid.Empty).Where(a => a != Guid.Empty);
}
///
/// 判断客户端是否在线
///
///
///
public bool HasOnline(Guid clientId)
{
return _redis.HGet($"{_redisPrefix}Online", clientId.ToString()) > 0;
}
///
/// 事件订阅
///
/// 上线
/// 下线
public void EventBus(
Action online,
Action offline)
{
var chanOnline = $"evt_{_redisPrefix}Online";
var chanOffline = $"evt_{_redisPrefix}Offline";
_redis.Subscribe(new[] { chanOnline, chanOffline }, (chan, msg) =>
{
if (chan == chanOnline) online(JsonConvert.DeserializeObject(msg as string));
if (chan == chanOffline) offline(JsonConvert.DeserializeObject(msg as string));
});
}
#region 群聊频道,每次上线都必须重新加入
///
/// 加入群聊频道,每次上线都必须重新加入
///
/// 客户端id
/// 群聊频道名
public void JoinChan(Guid clientId, string chan)
{
using (var pipe = _redis.StartPipe())
{
pipe.HSet($"{_redisPrefix}Chan{chan}", clientId.ToString(), 0);
pipe.HSet($"{_redisPrefix}Client{clientId}", chan, 0);
pipe.HIncrBy($"{_redisPrefix}ListChan", chan, 1);
pipe.EndPipe();
}
}
///
/// 离开群聊频道
///
/// 客户端id
/// 群聊频道名
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());
pipe.HDel($"{_redisPrefix}Client{clientId}", chan);
pipe.Eval($"if redis.call('HINCRBY', KEYS[1], '{chan}', '-1')