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 发送新消息时,它被唤醒并调用你的回调函数处理。

相关推荐
雪隐37 分钟前
个人电脑玩AI00-前言
人工智能·后端
我是一颗柠檬1 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
前端Hardy1 小时前
CSS 动画真的比 JS 快?Josh Comeau 做了组实验,结果跟直觉不一样
前端·javascript·后端
Front思1 小时前
调取支付宝支付正式环境不可以唤起来,但是沙箱可以
后端
foggyprojects1 小时前
AI 生成 SQL 模板以后,为什么还需要固定 helper 规则
后端
明天一点1 小时前
Cloudflare 通知转发钉钉机器人
前端·后端
前端Hardy1 小时前
前端日历组件,要变天了?Schedule-X v4.6 彻底杀疯了
前端·javascript·后端
Oo_行者_oO1 小时前
微服务 Feign 从“万能公共服务”到“业务客户端”
后端·架构
wei_shuo1 小时前
别再踩坑了!KingbaseES 存储过程与触发器开发避坑实录
后端
元宝骑士1 小时前
MySQL 实战:跨表排序 + 指定类型置顶四种写法
后端·mysql