修正命名错误
xiyunfei authored at 2023-04-09 21:10:58
4.31 KiB
NewLife.JT808
#if !NET45
using Confluent.Kafka;
using NewLife.Log;
using NewLife.Serialization;

namespace NewLife.JT808.Protocols.Kafka;

/// <summary>基于 Kafka 的消息消费者适配器</summary>
/// <remarks>
/// 将 Confluent.Kafka 的 IConsumer 包装为 JT808 的 IMsgConsumer 接口。
/// 消费时反序列化为 TMessage,从消息 Key 中提取 mobile。
/// 需要 .NET Standard 2.0+ 或 .NET Framework 4.6.1+ 运行时支持。
/// </remarks>
/// <typeparam name="TMessage">消息类型</typeparam>
public class KafkaConsumer<TMessage> : IMsgConsumer<TMessage>, IDisposable
{
    #region 属性
    /// <summary>Kafka 消费者实例</summary>
    public IConsumer<String, String> Consumer { get; }

    /// <summary>主题名称</summary>
    public String Topic { get; }

    /// <summary>是否已启动</summary>
    public Boolean Active => _active;
    private Boolean _active;
    #endregion

    #region 构造
    /// <summary>实例化 Kafka 消费者适配器</summary>
    /// <param name="bootstrapServers">Kafka 服务器地址(如 localhost:9092)</param>
    /// <param name="topic">主题名称</param>
    /// <param name="group">消费组</param>
    /// <param name="config">额外消费者配置。若为 null 则使用默认配置</param>
    public KafkaConsumer(String bootstrapServers, String topic, String group, ConsumerConfig? config = null)
    {
        Topic = topic;

        var cfg = config ?? new ConsumerConfig();
        cfg.BootstrapServers = bootstrapServers;
        cfg.GroupId = group;
        cfg.AutoOffsetReset = AutoOffsetReset.Earliest;
        cfg.EnableAutoCommit = true;

        Consumer = new ConsumerBuilder<String, String>(cfg).Build();
    }

    /// <summary>实例化 Kafka 消费者适配器</summary>
    /// <param name="consumer">已创建的 IConsumer 实例</param>
    /// <param name="topic">主题名称</param>
    public KafkaConsumer(IConsumer<String, String> consumer, String topic)
    {
        Consumer = consumer;
        Topic = topic;
    }
    #endregion

    #region 方法
    /// <summary>订阅消息</summary>
    /// <param name="onMessage">消息处理回调:参数为 mobile 和消息体</param>
    /// <param name="cancellationToken">取消令牌</param>
    public Task SubscribeAsync(Func<String, TMessage, Task> onMessage, CancellationToken cancellationToken = default)
    {
        _active = true;

        Consumer.Subscribe(Topic);

        // 启动后台消费循环
        _ = ConsumeLoopAsync(onMessage, cancellationToken);

        return Task.CompletedTask;
    }

    private async Task ConsumeLoopAsync(Func<String, TMessage, Task> onMessage, CancellationToken cancellationToken)
    {
        while (_active && !cancellationToken.IsCancellationRequested)
        {
            try
            {
                var result = Consumer.Consume(cancellationToken);
                if (result == null) continue;

                var mobile = result.Message.Key ?? String.Empty;
                var value = result.Message.Value;

                var body = default(TMessage);
                if (value != null)
                {
                    if (typeof(TMessage) == typeof(String))
                        body = (TMessage)(Object)value;
                    else
                        body = JsonHelper.ToJsonEntity<TMessage>(value);
                }

                if (body != null)
                    await onMessage(mobile, body);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                Log?.Error("Kafka 消费异常: {0}", ex.Message);
                await Task.Delay(1000, cancellationToken);
            }
        }
    }

    /// <summary>取消订阅</summary>
    public Task UnsubscribeAsync(CancellationToken cancellationToken = default)
    {
        _active = false;
        try
        {
            Consumer.Unsubscribe();
        }
        catch { }
        return Task.CompletedTask;
    }

    /// <summary>释放资源</summary>
    public void Dispose()
    {
        _active = false;
        try
        {
            Consumer.Unsubscribe();
            Consumer.Close();
            Consumer.Dispose();
        }
        catch { }
    }

    /// <summary>日志</summary>
    public ILog Log { get; set; } = Logger.Null;
    #endregion
}
#endif