惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Last Watchdog
The Last Watchdog
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
G
GRAHAM CLULEY
S
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
Recorded Future
Recorded Future
I
Intezer
云风的 BLOG
云风的 BLOG
博客园 - Franky
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
The Hacker News
The Hacker News
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
The Register - Security
The Register - Security
B
Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
The Cloudflare Blog
Webroot Blog
Webroot Blog
W
WeLiveSecurity
H
Heimdal Security Blog
博客园 - 三生石上(FineUI控件)
V
Vulnerabilities – Threatpost
G
Google Developers Blog
O
OpenAI News
V
V2EX
罗磊的独立博客
博客园_首页
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
TaoSecurity Blog
TaoSecurity Blog
Cloudbric
Cloudbric
H
Hacker News: Front Page
博客园 - 叶小钗
T
Tor Project blog
AI
AI

博客园 - victor.x.qu

MCP 企业托管授权(Enterprise-Managed Authorization) 从状态机到运行时:聊聊 .NET 11 的 Runtime Async 和老 Async/Await 到底差在哪 如何用c# 做 mcp/ChatGPT app 春节期间浅试了 ai 视频生成 举个栗子:做个AI Gateway demo 2025个人总结 实现 json path 来评估函数式解析器的损耗 VKProxy 集成 OpenTelemetry c# ACME client (补充) c# ACME client 留个VKProxy性能测试记录 VKProxy新增一些功能 http流量镜像 VKProxy新增CORS设置和http响应缓存 VKProxy新增速率限制功能 VKProxy已提供命令行工具,镜像和简单的ui 已经在为VKProxy写UI配置站点和文档了 如何使用 websocket 完成 socks5 网络穿透 如何基于 Kestrel 实现 socks5 代理 记录一下 简单udp和sni 代理 done 终于写完轮子一部分:tcp代理 了,记录一下
莫想到有一天得重新写个 etcd client
victor.x.qu · 2025-05-10 · via 博客园 - victor.x.qu

莫想到有一天得重新写个 etcd client

其实8年前搞过一个,

不过经过8年时间,etcd 多了很多功能 ,原来的多半不行了

虽然暂时我也没啥需求,但是怕kv和watch有变化

而且其实通过 grpc api 访问 etcd 没啥技术难度,搞client 也没啥意思的 (只要有.proto文件,可以直接生成grpc client代码, 参见微软文档

现成的也很多

所以本来不想写, 直接从 nuget 上找了个下载量最高的 https://www.nuget.org/packages/dotnet-etcd

最开始使用也ok,能读能写

但是当我使用watch时, 居然没有任何变更触发

当然也有其他人发现这个bug 对应issue: https://github.com/shubhamranjan/dotnet-etcd/issues/238(不过我遇见问题时还没人提issue,我看了源码,找到问题原因之后,这位同志已经写了issue了,)

如下是 issue 内容

using dotnet_etcd;
using Etcdserverpb;

EtcdClient etcdClient = new EtcdClient("http://localhost:2379");
etcdClient.Watch(
    "test",
    (WatchResponse response) =>
    {
        if (!response.Events.Any())
            return;
        var value = response.Events[0].Kv.Value.ToStringUtf8();
        Console.WriteLine(value);
    }
);

If you run this sample with 8.0.0, the watch callback never triggers if you modify the key test. If you run it with 6.0.1 it triggers correctly.

为什么 Watch 没用呢?

不是etcd版本问题, 不是网络访问有限制,不是你们第一直觉想到的问题

而是 作者没实现

https://github.com/shubhamranjan/dotnet-etcd/commit/4ba94d0a174fec42d190bffda733ded3a24055e0 这次代码提交中

watch 的 核心处理代码被删了, 但是package 依然水灵灵提交到了nuget上, 所以 当然不可能监听变更啦 囧 (issue 上我也加了描述,以免其他人遇见迷茫)

然后查看其原来实现, 还发现有个问题: 根本没有处理异常重连

更别提 etcd mvcc 避免变更丢失时要特别注意的 revsion

然后作者记录显示,已经近一个月没有更新

所以也不可能指望作者修正问题了

算了,还是自己来吧,

没想到 8年之后还得再次生成 etcd 的 grpc client

根据 最新的 etcd .proto文件 生成了最新的 grpc client

然后为 DependencyInjection 以及使用 简单封装了一下, 源码放在了 https://github.com/fs7744/etcdcsharp

如下是简单的使用文档

ETCD V3 Client

base code all Generate by grpc tools.

Quick start

Install package

  • Package Manager
Install-Package etcd.v3 -Version 0.0.2
Install-Package etcd.v3.Configuration -Version 0.0.2
  • .NET CLI
dotnet add package etcd.v3 --version 0.0.2
dotnet add package etcd.v3.Configuration --version 0.0.2

new client

new client with DI

 ServiceCollection services = new();
 services.UseEtcdClient();
 services.AddEtcdClient("test", new EtcdClientOptions() { Address = ["http://xxx:2379"] });
 var p = services.BuildServiceProvider();

 var client = p.GetRequiredKeyedService<IEtcdClient>("test");

 // you also can create client by factory
 var factory = p.GetRequiredService<IEtcdClientFactory>();
var client2 = factory.CreateClient(new EtcdClientOptions() { Address = ["http://xxx:2379"] });


// get all config
foreach (var i in await client.GetRangeValueUtf8Async("/ReverseProxy/"))
{
   Console.WriteLine($"{i.Key} : {i.Value}");
}

// OR get client in ctor
public class Testt
{
    private readonly IEtcdClient client;

    public Testt([FromKeyedServices("test")] IEtcdClient client)
    {
        this.client = client;
    }
}


new client without DI

 var factory = EtcdClientFactory.Create();
var client = factory.CreateClient(new EtcdClientOptions() { Address = ["http://xxx:2379"] });

// get all config
foreach (var i in await client.GetRangeValueUtf8Async("/ReverseProxy/"))
{
   Console.WriteLine($"{i.Key} : {i.Value}");
}

use with Configuration

var b = new ConfigurationBuilder();
b.UseEtcd(new Etcd.Configuration.EtcdConfigurationOptions()
{
    Prefix = "/ReverseProxy/",
    RemovePrefix = true,
    EtcdClientOptions = new EtcdClientOptions() { Address = ["http://xxx:2379"] }
});
var c = b.Build();

// test watch change
Test(c);

private static void Test(IConfigurationRoot c)
{
    foreach (var i in c.GetChildren())
    {
        Console.WriteLine($"{i.Key} : {i.Value}");
    }
    c.GetReloadToken().RegisterChangeCallback(i =>
    {
        Test(i as IConfigurationRoot);
    }, c);
}

Address

Address just parse by GrpcChannel.ForAddress, so support

KV

get one by key

string v = await client.GetValueUtf8Async("/ReverseProxy/");
//or 
string v = (await client.GetAsync("/ReverseProxy/")).Kvs?.First().Value.ToStrUtf8();
//or
string v = (await client.RangeAsync(new RangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/") })).Kvs?.First().Value.ToStrUtf8();
get all IDictionary<string, string>
foreach (var i in await client.GetRangeValueUtf8Async("/ReverseProxy/"))
{
    Console.WriteLine($"{i.Key} : {i.Value}");
}
//or
foreach (var i in (await client.GetRangeAsync("/ReverseProxy/")).Kvs)
{
    Console.WriteLine($"{i.Key.ToStrUtf8()} : {i.Value.ToStrUtf8()}");
}
//or
foreach (var i in (await client.RangeAsync(new RangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/"), RangeEnd = ByteString.CopyFromUtf8("/ReverseProxy/".GetRangeEnd()) })).Kvs)
{
    Console.WriteLine($"{i.Key.ToStrUtf8()} : {i.Value.ToStrUtf8()}");
}

Put

await client.PutAsync("/ReverseProxy/test", "1");
//or
await client.PutAsync(new PutRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/test"), Value = ByteString.CopyFromUtf8("1") });

Delete one

await client.DeleteAsync("/ReverseProxy/test");
//or
await client.DeleteRangeAsync(new DeleteRangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/test") });

Delete all

await client.DeleteRangeAsync("/ReverseProxy/test");
//or
await client.DeleteRangeAsync(new DeleteRangeRequest() { Key = ByteString.CopyFromUtf8("/ReverseProxy/test"), RangeEnd = ByteString.CopyFromUtf8("/ReverseProxy/test".GetRangeEnd())) });

Watch

 await client.WatchRangeBackendAsync("/ReverseProxy/", i =>
 {
     if (i.Events.Count > 0)
     {
         foreach (var item in i.Events)
         {
             Console.WriteLine($"{item.Type} {item.Kv.Key.ToStrUtf8()}");
         }
     }
     return Task.CompletedTask;
 }, startRevision: 6, reWatchWhenException: true);

 // or
await Task.Factory.StartNew(async () =>
{
    long startRevision = 6;
    while (true)
    {
        try
        {
            using var watcher = await client.WatchRangeAsync("/ReverseProxy/", startRevision: startRevision);
            await watcher.ForAllAsync(i =>
            {
                startRevision = i.FindRevision(startRevision);
                foreach (var item in i.Events)
                {
                    Console.WriteLine($"{item.Type} {item.Kv.Key.ToStrUtf8()}");
                }
                return Task.CompletedTask;
            });
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception: {ex.Message}");
        }
    }
});

all grpc client

if IEtcdClient Missing some grpc method , you can just use grpc client to do

public partial interface IEtcdClient
{
    public AuthClient AuthClient { get; }
    public Cluster.ClusterClient ClusterClient { get; }
    public ElectionClient ElectionClient { get; }
    public KV.KVClient KVClient { get; }
    public LeaseClient LeaseClient { get; }
    public LockClient LockClient { get; }
    public MaintenanceClient MaintenanceClient { get; }
    public Watch.WatchClient WatchClient { get; }
}

api doc

Main api doc please see

https://fs7744.github.io/etcdcsharp/api/Etcd.html
https://fs7744.github.io/etcdcsharp/api/Microsoft.Extensions.Configuration.EtcdConfigurationExtensions.html

All api doc ( include code generate by grpc tool ) please see

https://fs7744.github.io/etcdcsharp/api/index.html