一、引言

  研究Kafka有一段时间了,略有心得,基于此自己就写了一个Kafka的消费者的类和Kafka消息生产者的类,进行了单元测试和生产环境的测试,还是挺可靠的。

二、源码

  话不多说,直接上代码,代码不是很难,注释很全,希望大家多多发表意见,继续提升。

     /// <summary>
/// Kafka消息消费者接口
/// </summary>
public interface IKafkaConsumer
{
/// <summary>
/// 指定的组别的消费者开始消费指定主题的消息
/// </summary>
/// <param name="broker">Kafka消息服务器的地址</param>
/// <param name="topic">Kafka消息所属的主题</param>
/// <param name="groupID">Kafka消费者所属的组别</param>
/// <param name="action">可以对已经消费的消息进行相关处理</param>
void Consume(string broker, string topic, string groupID, Action<ConsumerResult> action = null);
}

  以上类型是接口定义,这个类定义的抽象类,可以重复使用相关的代码定义其中,但是目前这两个方法没有使用。

     /// <summary>
/// Kafka抽象基类,提供公共接口实现
/// </summary>
public abstract class KafkaBase
{
/// <summary>
/// 获取Kafka服务器地址
/// </summary>
/// <param name="brokerNameKey">配置文件中Broker服务器地址的key的名称</param>
/// <returns>返回获取到的Kafka服务器的地址明细</returns>
public string GetKafkaBroker(string brokerNameKey = "Broker")
{
string kafkaBroker = string.Empty; if (!ConfigurationManager.AppSettings.AllKeys.Contains(brokerNameKey))
{
kafkaBroker = "http://localhost:9092";
}
else
{
kafkaBroker = ConfigurationManager.AppSettings[brokerNameKey];
}
return kafkaBroker;
} /// <summary>
/// 在配置文件中获取系统中已经生成的主题名称
/// </summary>
/// <param name="topicNameKey">配置文件中主题的key名称</param>
/// <returns>返回获取到的主题的具体值</returns>
public string GetTopicName(string topicNameKey = "Topic")
{
string topicName = string.Empty; if (!ConfigurationManager.AppSettings.AllKeys.Contains(topicNameKey))
{
throw new Exception("Key \"" + topicNameKey + "\" not found in Config file -> configuration/AppSettings");
}
else
{
topicName = ConfigurationManager.AppSettings[topicNameKey];
}
return topicName;
}
}

  还有一个用于数据传递的工具类,代码如下:

     /// <summary>
/// Kafka消息消费者设置对象,提供Kafka消费消息的参数对象(Consumer.Consum)
/// </summary>
public sealed class ConsumerSetting
{
/// <summary>
/// Kafka消息服务器的地址
/// </summary>
public string Broker { get; set; } /// <summary>
/// Kafka消息所属的主题
/// </summary>
public string Topic { get; set; } /// <summary>
/// Kafka消息消费者分组主键
/// </summary>
public string GroupID { get; set; } /// <summary>
/// 消费消息后可以执行的方法
/// </summary>
public Action<ConsumerResult> Action { get; set; }
}

  我们可以对消息进行消费,该类型用于对消息进行整理,代码如下:

     /// <summary>
/// 已经消费的消息的详情信息
/// </summary>
public sealed class ConsumerResult
{
/// <summary>
/// Kafka消息服务器的地址
/// </summary>
public string Broker { get; set; } /// <summary>
/// Kafka消息所属的主题
/// </summary>
public string Topic { get; set; } /// <summary>
/// Kafka消息消费者分组主键
/// </summary>
public string GroupID { get; set; } /// <summary>
/// 我们需要处理的消息具体的内容
/// </summary>
public string Message { get; set; } /// <summary>
/// Kafka数据读取的当前位置
/// </summary>
public long Offset { get; set; } /// <summary>
/// 消息所在的物理分区
/// </summary>
public int Partition { get; set; }
}

  最后阶段了,该是消费者的代码了,代码如下:

     /// <summary>
/// Kafka消息消费者
/// </summary>
public sealed class KafkaConsumer : KafkaBase, IKafkaConsumer
{
#region 私有字段 private bool isCancelled; #endregion #region 构造函数 /// <summary>
/// 构造函数,初始化IsCancelled属性
/// </summary>
public KafkaConsumer()
{
isCancelled = false;
} #endregion #region 属性 /// <summary>
/// 是否应该取消继续消费Kafka的消息,默认值是false,继续消费消息
/// </summary>
public bool IsCancelled
{
get { return isCancelled; }
set { isCancelled = value; }
} #endregion #region 同步版本 /// <summary>
/// 指定的组别的消费者开始消费指定主题的消息
/// </summary>
/// <param name="broker">Kafka消息服务器的地址</param>
/// <param name="topic">Kafka消息所属的主题</param>
/// <param name="groupID">Kafka消费者所属的组别</param>
/// <param name="action">可以对已经消费的消息进行相关处理</param>
public void Consume(string broker, string topic, string groupID, Action<ConsumerResult> action = null)
{
if (string.IsNullOrEmpty(broker) || string.IsNullOrWhiteSpace(broker) || broker.Length <= )
{
throw new ArgumentNullException("Kafka消息服务器的地址不能为空!");
} if (string.IsNullOrEmpty(topic) || string.IsNullOrWhiteSpace(topic) || topic.Length <= )
{
throw new ArgumentNullException("消息所属的主题不能为空!");
} if (string.IsNullOrEmpty(groupID) || string.IsNullOrWhiteSpace(groupID) || groupID.Length <= )
{
throw new ArgumentNullException("用户分组ID不能为空!");
} var config = new Dictionary<string, object>
{
{ "bootstrap.servers", broker },
{ "group.id", groupID },
{ "enable.auto.commit", true }, // this is the default
{ "auto.commit.interval.ms", },
{ "statistics.interval.ms", },
{ "session.timeout.ms", },
{ "auto.offset.reset", "smallest" }
}; using (var consumer = new Consumer<Ignore, string>(config, null, new StringDeserializer(Encoding.UTF8)))
{
// Note: All event handlers are called on the main thread.
//consumer.OnMessage += (_, message) => Console.WriteLine("Topic:" + message.Topic + " Partition:" + message.Partition + " Offset:" + message.Offset + " " + message.Value);
//consumer.OnMessage += (_, message) => Console.WriteLine("Offset:【" + message.Offset + "】Message:【" + message.Value + "】");
if (action != null)
{
consumer.OnMessage += (_, message) => {
ConsumerResult messageResult = new ConsumerResult();
messageResult.Broker = broker;
messageResult.Topic = message.Topic;
messageResult.Partition = message.Partition;
messageResult.Offset = message.Offset.Value;
messageResult.Message = message.Value; //执行外界自定义的方法
action(messageResult);
};
} consumer.OnPartitionEOF += (_, end) => Console.WriteLine("Reached end of topic " + end.Topic + " partition " + end.Partition + ", next message will be at offset " + end.Offset); consumer.OnError += (_, error) => Console.WriteLine("Error:" + error); //引发反序列化错误或消费消息出现错误!= NoError。
consumer.OnConsumeError += (_, message) => Console.WriteLine("Error consuming from topic/partition/offset " + message.Topic + "/" + message.Partition + "/" + message.Offset + ": " + message.Error); consumer.OnOffsetsCommitted += (_, commit) => Console.WriteLine(commit.Error ? "Failed to commit offsets:" + commit.Error : "Successfully committed offsets:" + commit.Offsets); // 当消费者被分配一组新的分区时引发。
consumer.OnPartitionsAssigned += (_, partitions) =>
{
Console.WriteLine("Assigned Partitions:" + partitions + ", Member ID:" + consumer.MemberId);
//如果您未向OnPartitionsAssigned事件添加处理程序,则会自动执行以下.Assign调用。 如果你为它添加了事件处理程序,你必须明确地调用.Assign以便消费者开始消费消息。
consumer.Assign(partitions);
}; // Raised when the consumer's current assignment set has been revoked.
//当消费者的当前任务集已被撤销时引发。
consumer.OnPartitionsRevoked += (_, partitions) =>
{
Console.WriteLine("Revoked Partitions:" + partitions);
// If you don't add a handler to the OnPartitionsRevoked event,the below .Unassign call happens automatically. If you do, you must call .Unassign explicitly in order for the consumer to stop consuming messages from it's previously assigned partitions.
//如果您未向OnPartitionsRevoked事件添加处理程序,则下面的.Unassign调用会自动发生。 如果你为它增加了事件处理程序,你必须明确地调用.Usessign以便消费者停止从它先前分配的分区中消费消息。
consumer.Unassign();
}; //consumer.OnStatistics += (_, json) => Console.WriteLine("Statistics: " + json); consumer.Subscribe(topic); //Console.WriteLine("Subscribed to:" + consumer.Subscription); while (!IsCancelled)
{
consumer.Poll(TimeSpan.FromMilliseconds());
}
}
} #endregion #region 异步版本 /// <summary>
/// 指定的组别的消费者开始消费指定主题的消息
/// </summary>
/// <param name="broker">Kafka消息服务器的地址</param>
/// <param name="topic">Kafka消息所属的主题</param>
/// <param name="groupID">Kafka消费者所属的组别</param>
/// <param name="action">可以对已经消费的消息进行相关处理</param>
public void ConsumeAsync(string broker, string topic, string groupID, Action<ConsumerResult> action = null)
{
if (string.IsNullOrEmpty(broker) || string.IsNullOrWhiteSpace(broker) || broker.Length <= )
{
throw new ArgumentNullException("Kafka消息服务器的地址不能为空!");
} if (string.IsNullOrEmpty(topic) || string.IsNullOrWhiteSpace(topic) || topic.Length <= )
{
throw new ArgumentNullException("消息所属的主题不能为空!");
} if (string.IsNullOrEmpty(groupID) || string.IsNullOrWhiteSpace(groupID) || groupID.Length <= )
{
throw new ArgumentNullException("用户分组ID不能为空!");
} ThreadPool.QueueUserWorkItem(KafkaAutoCommittedOffsets, new ConsumerSetting() { Broker = broker, Topic = topic, GroupID = groupID, Action=action });
} #endregion #region 两种提交Offsets的版本 /// <summary>
/// Kafka消息队列服务器自动提交offset
/// </summary>
/// <param name="state">消息消费者信息</param>
private void KafkaAutoCommittedOffsets(object state)
{
ConsumerSetting setting = state as ConsumerSetting; var config = new Dictionary<string, object>
{
{ "bootstrap.servers", setting.Broker },
{ "group.id", setting.GroupID },
{ "enable.auto.commit", true }, // this is the default
{ "auto.commit.interval.ms", },
{ "statistics.interval.ms", },
{ "session.timeout.ms", },
{ "auto.offset.reset", "smallest" }
}; using (var consumer = new Consumer<Ignore, string>(config, null, new StringDeserializer(Encoding.UTF8)))
{
if (setting.Action != null)
{
consumer.OnMessage += (_, message) =>
{
ConsumerResult messageResult = new ConsumerResult();
messageResult.Broker = setting.Broker;
messageResult.Topic = message.Topic;
messageResult.Partition = message.Partition;
messageResult.Offset = message.Offset.Value;
messageResult.Message = message.Value; //执行外界自定义的方法
setting.Action(messageResult);
};
} //consumer.OnStatistics += (_, json)=> Console.WriteLine("Statistics: {json}"); //可以写日志
//consumer.OnError += (_, error)=> Console.WriteLine("Error:"+error); //可以写日志
//consumer.OnConsumeError += (_, msg) => Console.WriteLine("Error consuming from topic/partition/offset {msg.Topic}/{msg.Partition}/{msg.Offset}: {msg.Error}"); consumer.Subscribe(setting.Topic); while (!IsCancelled)
{
consumer.Poll(TimeSpan.FromMilliseconds());
}
}
} /// <summary>
/// Kafka消息队列服务器手动提交offset
/// </summary>
/// <param name="state">消息消费者信息</param>
private void KafkaManuallyCommittedOffsets(object state)
{
ConsumerSetting setting = state as ConsumerSetting; var config = new Dictionary<string, object>
{
{ "bootstrap.servers", setting.Broker },
{ "group.id", setting.GroupID },
{ "enable.auto.commit", false },//不是自动提交的
{ "auto.commit.interval.ms", },
{ "statistics.interval.ms", },
{ "session.timeout.ms", },
{ "auto.offset.reset", "smallest" }
}; using (var consumer = new Consumer<Ignore, string>(config, null, new StringDeserializer(Encoding.UTF8)))
{
//可以写日志
//consumer.OnError += (_, error) => Console.WriteLine("Error:"+error); //可以写日志
// Raised on deserialization errors or when a consumed message has an error != NoError.
//consumer.OnConsumeError += (_, error)=> Console.WriteLine("Consume error:"+error); consumer.Subscribe(setting.Topic); Message<Ignore, string> message = null; while (!isCancelled)
{
if (!consumer.Consume(out message, TimeSpan.FromMilliseconds()))
{
continue;
} if (setting.Action != null)
{
ConsumerResult messageResult = new ConsumerResult();
messageResult.Broker = setting.Broker;
messageResult.Topic = message.Topic;
messageResult.Partition = message.Partition;
messageResult.Offset = message.Offset.Value;
messageResult.Message = message.Value; //执行外界自定义的方法
setting.Action(messageResult);
} if (message.Offset % == )
{
var committedOffsets = consumer.CommitAsync(message).Result;
//Console.WriteLine("Committed offset:"+committedOffsets);
}
}
}
} #endregion
}

  有了消息的消费者代码,那消息的生产者代码肯定是少不了的。代码如下:

     /// <summary>
/// Kafka消息生产者
/// </summary>
public sealed class KafkaProducer : KafkaBase, IKafkaProducer
{
/// <summary>
/// 生产消息并发送消息
/// </summary>
/// <param name="broker">kafka的服务器地址</param>
/// <param name="topic">kafka的消息主题名称</param>
/// <param name="message">需要传送的消息</param>
public bool Produce(string broker, string topic, string message)
{
bool result = false;
if (string.IsNullOrEmpty(broker) || string.IsNullOrWhiteSpace(broker) || broker.Length <= )
{
throw new ArgumentNullException("Kafka消息服务器地址不能为空!");
} if (string.IsNullOrEmpty(topic) || string.IsNullOrWhiteSpace(topic) || topic.Length <= )
{
throw new ArgumentNullException("消息所属的主题不能为空!");
} if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message) || message.Length <= )
{
throw new ArgumentNullException("消息内容不能为空!");
} var config = new Dictionary<string, object> { { "bootstrap.servers", broker } };
using (var producer = new Producer<Null, string>(config, null, new StringSerializer(Encoding.UTF8)))
{
var deliveryReport = producer.ProduceAsync(topic, null, message);
deliveryReport.ContinueWith(task =>
{
if (task.Result.Error.Code == ErrorCode.NoError)
{
result = true;
}
//可以在控制台使用以下语句
//Console.WriteLine("Producer:" + producer.Name + "\r\nTopic:" + topic + "\r\nPartition:" + task.Result.Partition + "\r\nOffset:" + task.Result.Offset + "\r\nMessage:" + task.Result.Value);
}); producer.Flush(TimeSpan.FromSeconds());
}
return result;
}
}

  好了,以上就是全部代码,大家可以整理使用。

三、总结

  继续学习,先使用,慢慢了解内部的细节,我已经迈出了第一步,不忘初心,继续努力。

基于Confluent.Kafka实现的KafkaConsumer消费者类和KafkaProducer消息生产者类型的更多相关文章

  1. 基于Confluent.Kafka实现的Kafka客户端操作类使用详解

    一.引言 有段时间没有写东西了,当然不是没得写,还有MongoDB的系列没有写完呢,那个系列还要继续.今天正好是周末,有点时间,来写新东西吧.最近公司用了Kafka做为消息的中间件,最开始写的那个版本 ...

  2. kafka - Confluent.Kafka

    上个章节我们讲了kafka的环境安装(这里),现在主要来了解下Kafka使用,基于.net实现kafka的消息队列应用,本文用的是Confluent.Kafka,版本0.11.6 1.安装: 在NuG ...

  3. kafka高吞吐量的分布式发布订阅的消息队列系统

    一:kafka介绍kafka(官网地址:http://kafka.apache.org)是一种高吞吐量的分布式发布订阅的消息队列系统,具有高性能和高吞吐率. 1.1 术语介绍BrokerKafka集群 ...

  4. 基于kafka-net实现的可以长链接的消息生产者

    今天有点时间,我就来说两句.最近接触的Kafka相关的东西要多一些,其实以前也接触过,但是在项目使用中的经验不是很多.最近公司的项目里面使用了Kafka消息中间件,由于以前的人员编写的客户端的类不是很 ...

  5. Kafka 0.9 新消费者API

    kafka诞生之初,它自带一个基于scala的生产者和消费者客户端.但是慢慢的我们认识到这些API有很多限制.比如,消费者有一个“高级”API支持分组和异常控制,但是不支持很多更复杂的应用场景:它也有 ...

  6. kafka消费组、消费者

    consumer group consumer instance 一个消费组可能有一个或者多个消费者.同一个消费组可以订阅一个或者多个主题.主题的某一个分区只能被消费组的某一个消费者消费.那么分区和消 ...

  7. DataPipeline联合Confluent Kafka Meetup上海站

    Confluent作为国际数据“流”处理技术领先者,提供实时数据处理解决方案,在市场上拥有大量企业客户,帮助企业轻松访问各类数据.DataPipeline作为国内首家原生支持Kafka解决方案的“iP ...

  8. 【Open Search产品评测】-- 淘点点:基于OpenSearch,轻松实现一整套O2O类搜索解决方案

     [Open Search产品评测]--  淘点点:基于OpenSearch,轻松实现一整套O2O类搜索解决方案   [使用背景] 我们淘点点团队应该可以算是内网首批使用opensearch来搭建应用 ...

  9. wemall app商城源码中基于PHP的通用的树型类代码

    wemall doraemon是Android客户端程序,服务端采用wemall微信商城,不对原商城做任何修改,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可随意定制修改.本文分享其中 ...

随机推荐

  1. 前端Web安全介绍及规避。。。

    本文转载自:https://jelon.top/posts/web-security/ 如果侵权,请及时告知. 一.跨站脚本攻击 (xss) 反射型跨站脚本攻击 攻击者会通过社会工程学手段,发送一个 ...

  2. DBCC维护语句语法

    一.DBCC维护语句:对数据库.索引或文件组进行维护的任务--1.DBCC CLEANTABLE,回收删除的可变长度列和文本列的空间 DBCC CLEANTABLE  ( { 'database_na ...

  3. hdu2669-Romantic-(扩展欧几里得定理)

    Romantic Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  4. Oracle11g服务详细介绍

    Oracle11g服务详细介绍及哪些服务是必须开启的? Oracle ORCL VSS Writer Service Oracle卷映射拷贝写入服务,VSS(Volume Shadow Copy Se ...

  5. centos7升级内核

    载入elrepo源,搜索内核更新资源,并进行更新操作. 具体实验步骤: # 载入公钥rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org ...

  6. VR/AR 科技了解

    Dream学院学习资料: VR/AR科技学习需要先学习NDK技术 AR/VR->图像学->图像处理(OpenCV->Intel公司在1999年发布3.2).图像绘制渲染(OpenGL ...

  7. aspectj ----- 简介

    一.为什么写这个系列的博客   Aspectj一个易用的.功能强大的aop编程语言.其官网地址是:http://www.eclipse.org/aspectj/,目前最新版本为:1.7.0 RC1.但 ...

  8. 第二篇:Jmeter功能概要

    一.jmeter工具组成部分: 1.资源生成器:用于生成测试过程中服务器,负载机的资源代码: 2.用户运行器:通常是一个脚本运行引擎,根据脚本的要求模拟指定用户行为,(lr中的controller) ...

  9. linux 后台运行命令

    command & 关闭终端,程序会终止 nohup command & 关闭终端,程序不会终止

  10. selenium ide界面介绍

    Selenium Ide是firefox浏览器的一个插件,可以进行web的录制和回放,完成简单的自动化测试,同时可以将录制的脚本导出多种语言的脚本. 下面是Selenium Ide的界面: Base  ...