dapr::Listiner

你贴出的这段代码正是上面我们讨论的 事件监听是如何"常驻运行"的底层机制

让我们一步步剖析它,看看:

🔍 "是谁一直在调用 listener?"

🔄 "goroutine 是在哪个环节启动的?"

🧩 "eventBus / Dapr / listenerRegistry 三者之间是怎样协作的?"


🧩 一、核心方法:SubscribeWithHandler

我们重点看这一段:

go 复制代码
go func() {
    defer s.Close()

    for {
        msg, err := s.Receive()
        if err != nil {
            ...
            return
        }

        go func() {
            if err := msg.respondStatus(handler(msg.TopicEvent)); err != nil {
                ...
            }
        }()
    }
}()

这几行代码做的事情可以总结为👇:

动作 说明
go func() { ... }() 启动一个新的 goroutine,专门负责监听消息流。
for { msg, _ := s.Receive() } 持续阻塞等待(long polling),直到从 pub/sub 收到新消息。
handler(msg.TopicEvent) 收到消息后调用你注册的业务处理函数(例如 processCreated)。
msg.respondStatus(...) 将处理结果(成功/失败)回传给 Dapr runtime。

也就是说:

每个订阅(Subscription)在启动时都会自动启动一个 goroutine,永久监听该 topic。

这就是为什么即使你不主动"轮询"调用,事件监听器也能一直"活着"的原因。


🧠 二、事件监听的运行机制图(从 Dapr 到你的 listener)

scss 复制代码
[Dapr sidecar]
   │  ← 消息发布 (RabbitMQ / Redis Streams / Kafka / etc.)
   ▼
[Dapr gRPC server]
   │
   ▼
client.SubscribeWithHandler(...)        ← 你调用的函数
   │
   ├─ s := c.Subscribe(ctx, opts)       ← 创建底层订阅连接
   │
   └─ go func() {                       ← 启动 goroutine
        for {
            msg, _ := s.Receive()       ← 阻塞等待新事件
            go handler(msg.TopicEvent)  ← 调用业务处理函数
        }
      }()

💡 当 Dapr sidecar 收到新消息,会通过 gRPC 推送给客户端,
Receive() 负责接收并触发你的 handler


⚙️ 三、DaprClient 的生命周期管理

再看:

go 复制代码
func (c *DaprClient) SubscribeWithHandler(...) error {
    stop, err := c.client.SubscribeWithHandler(...)
    ...
    c.stopHandlers = append(c.stopHandlers, stop)
}
  • 每个订阅(topic)都会返回一个 stop 函数;
  • 这个函数内部调用 s.Close()
  • DaprClient 把它们都存到 stopHandlers 里,以便在应用关闭时统一释放。

👉 所以:

  • 启动时:每个订阅会起一个 goroutine;
  • 关闭时:调用 for _, stop := range c.stopHandlers { stop() } 来安全退出。

🔁 四、关联到你上面的 ListenerRegistry 流程

整个流程串起来是这样的:

css 复制代码
main()
 └─ wireApp()
     ├─ eventBus := data.NewEventBus()
     │     └─ 包装 DaprClient.SubscribeWithHandler()
     │           └─ 启动 goroutine 循环 Receive()
     │
     ├─ workflowEventListener := NewWorkflowEventListener(eventBus, ...)
     │     └─ 调用 eventBus.Subscribe(pubsub="pubsub", topic="workflow_event")
     │           └─ 启动 goroutine 等待消息
     │
     └─ listenerRegistry := &ListenerRegistry{WorkflowEventListener: ...}
           (只是保存引用)

结果:

  • listenerRegistry 是一个引用容器;
  • WorkflowEventListener.Subscribe() 通过 Dapr client 启动 goroutine;
  • Dapr runtime 持续通过 gRPC 推消息;
  • 你的 processCreatedprocessStop 等处理函数在 goroutine 中被自动调用。

✅ 五、总结要点

部分 是否常驻 说明
listenerRegistry ❌ 否,仅保存指针引用 不执行任何逻辑
WorkflowEventListener.Subscribe() ✅ 启动 Dapr 订阅 goroutine 实现事件监听
SubscribeWithHandler ✅ 内部 go func() { for { s.Receive() } }() 永久循环接收消息
handler(msg.TopicEvent) ✅ 每条消息触发一次 执行业务逻辑
stopHandlers ✅ 管理所有订阅 在应用退出时关闭 goroutine

💬 一句话总结

监听器之所以能"一直运行",

是因为 SubscribeWithHandler 在内部启动了一个 goroutine,

这个 goroutine 会阻塞等待 s.Receive() 不停从 Dapr pub/sub 拉取消息。

listenerRegistry 只是保存所有这些监听器的引用,

真正"跑"的是 Dapr 客户端的消息循环。


是否希望我帮你画出这个完整的 "事件监听常驻机制时序图" (Dapr → gRPCClient → WorkflowEventListener → handler 调用链)?

这样你能一眼看清整个 goroutine 调用流。

You said:

Dapr runtime 持续通过 gRPC 推消息;

ChatGPT said:

非常对 👍,你抓到了整个机制的关键一句话:

💡 "Dapr runtime 持续通过 gRPC 推消息。"

下面我帮你完整解释这句话背后的 运行原理(包括 Dapr sidecar 的行为、gRPC 通道、消息流动方向) ,并结合你贴的代码,让你真正明白 listener 为什么会"一直被调用"。


🧩 一、整体结构:Dapr 的 pub/sub 机制分成两部分

scss 复制代码
+-----------------------------+
|      Your App (Go)         |
|-----------------------------|
|  Dapr SDK / Client (gRPC)  | ← 你用的 dapr.NewClient()
|-----------------------------|
|  Listener goroutines        |
|   - SubscribeWithHandler()  | ← 启动 for { Receive() }
+-------------▲---------------+
              │ gRPC (推送)
              ▼
+-----------------------------+
|       Dapr Sidecar          |
|-----------------------------|
|  Pub/Sub component plugin   | ← Redis / RabbitMQ / Kafka / etc.
|  Subscription registry      |
+-----------------------------+

Dapr runtime(sidecar)是一个 本地进程 (通常与应用容器同 pod),

它负责:

  1. 从外部消息系统订阅消息;
  2. 把消息 推送(push) 到你的应用;
  3. 等待你的应用返回 ack(成功/失败)。

⚙️ 二、gRPC 推送机制(双向流)

你看到的代码:

css 复制代码
msg, err := s.Receive()

其实是封装在一个 gRPC 双向流(Bidirectional Stream) 之上的。

Dapr runtime 在 gRPC 层面的协议大致如下(伪代码):

scss 复制代码
service AppCallback {
  rpc OnTopicEventStream(stream TopicEventRequest) returns (stream TopicEventResponse);
}

运行时流程:

scss 复制代码
Dapr Sidecar (server)  ←→  Your App (client)
        ↑                       ↓
   [Push message]         [Receive() blocks]
        ↑                       ↓
   [Wait ack]              [RespondStatus()]

所以当 Dapr 从外部 pub/sub 系统(如 Redis)收到消息时:

  1. 它通过 gRPC 流式发送(Send) 一条 TopicEventRequest
  2. 你的应用那边 Receive() 就被唤醒;
  3. Dapr SDK 执行你注册的 handler(msg.TopicEvent)
  4. 你业务处理完后,SDK 调用 respondStatus()
  5. Dapr runtime 收到 ack,决定是否从 pub/sub 确认(acknowledge)该消息。

🧵 三、为什么是"推送(push)",而不是"拉取(poll)"

区别如下👇

模式 谁主动发起 SDK 里表现 性能
拉取 (polling) 应用主动调用 API 去取消息 手动调用 Receive() 延迟高
推送 (stream push) Dapr runtime 主动发消息 Receive() 被唤醒 延迟低、实时性高 ✅

所以即使你在 Go 代码里看到 for { s.Receive() }

实际上是一个被 远端 gRPC 推送唤醒的阻塞等待

这就是为什么 listener 不需要轮询或定时器:

它在一个长连接的 gRPC 流上,Dapr 一推就收到。


🧠 四、时序图展示整个推送过程

scss 复制代码
外部消息源 (RabbitMQ)
     │
     │ 发布事件
     ▼
[Dapr Sidecar]
     │ 从 Pub/Sub 插件接收消息
     │
     │ 通过 gRPC stream 推送 TopicEvent
     ▼
[Dapr SDK in your app]
     │ s.Receive() 被唤醒
     │ 调用 handler(msg.TopicEvent)
     │ handler 完成 → respondStatus()
     ▼
[Dapr Sidecar]
     │ 收到 ack → 向 Pub/Sub 发确认
     ▼
[消息系统]
     │ 从队列移除消息

🧩 五、结合你贴的 SubscribeWithHandler

现在你可以更清楚地理解这几行的意义了:

scss 复制代码
for {
    msg, err := s.Receive()  // ← 阻塞等待 gRPC stream 推送
    go func() {
        handler(msg.TopicEvent)      // ← 调用你的业务逻辑
        msg.respondStatus(...)       // ← 发送 ack 给 Dapr runtime
    }()
}
  • Receive() 实际是等 Dapr runtime 通过 gRPC stream 送过来的事件;
  • handler() 是你注册的回调(例如 processCreated);
  • respondStatus() 是响应结果给 Dapr runtime;
  • 整个过程由 sidecar 主动推送消息 触发。

✅ 六、总结

环节 作用
Dapr runtime (sidecar) 从 Pub/Sub 拉取消息,通过 gRPC 推送到你的应用
gRPC 双向流 保持一个长连接通道,用于持续推送消息和接收 ack
SDK SubscribeWithHandler 启动 goroutine,阻塞 Receive(),自动处理每条消息
handler() 你的业务逻辑,执行完后返回状态
listenerRegistry 只负责收集 listener,不参与推送逻辑

💬 一句话总结

Dapr runtime 主动通过 gRPC 长连接推送消息到你的应用,

你的应用通过 SubscribeWithHandler 启动 goroutine 阻塞等待 Receive()

当 Dapr 发送新消息时,它被唤醒并调用你的回调函数处理。

相关推荐
闲云散3 小时前
WebClient 简述
java·后端
狗头大军之江苏分军4 小时前
请不要在感情里丢掉你的“我”
前端·后端
BingoGo4 小时前
2025 年必须尝试的 5 个 Laravel 新特性
后端
豆浆Whisky4 小时前
掌握Go context:超越基础用法的正确实践模式|Go语言进阶(13)
后端·go
用户68545375977694 小时前
📁 设计一个文件上传和存储服务:云盘的秘密!
后端
Merrick4 小时前
亲手操作Java抽象语法树
java·后端
今天没ID4 小时前
高阶函数
后端
_光光4 小时前
大文件上传服务实现(后端篇)
后端·node.js·express