csharp/2881099/im/ImCore/ImClient.cs

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')