Ruby (代码) 用 Rails 实现一个 sse API

308820773 · 2025年08月20日 · 最后由 308820773 回复于 2025年08月21日 · 191 次阅读
class StreamController < ActionController::API
  include ActionController::Live

  before_action do
    response.headers['Content-Type']                 = 'text/event-stream'
    response.headers["Last-Modified"]                = Time.now.httpdate   # TODO 3 
    response.headers['Access-Control-Allow-Origin']  = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
    response.headers['Cache-Control']                = 'no-cache'
    response.headers['Connection']                   = 'keep-alive'

    @sse = SSE.new(response.stream) # TODO 1 
  end

  rescue_from(ActiveRecord::RecordNotFound) do |e|
    @sse.write(status: 404, message: e.message.to_s)
    @sse.close
  end

  # params
  # {
  #   chat_id: 1,
  #   content: 'Hello',
  # }
  def talk
    Chats::GetAnswer.execute(params, @sse)
    # ensure # TODO 2 
    #   @sse.close
  end
end

开发时候主要碰到几个问题

TODO 1. SSE.new(response.stream, retry: 300), 返回 sse 数据时,会把 retry: 300 返回,这个是正常?

TODO 2. ensure 这里不可以 close, 因为 rescue_from 的执行是在 ensure 之后的,有没有更优雅的写法

TODO 3. 感谢 rennyallen 的 https://ruby-china.org/topics/43052 这篇文章,rack >= 2.2x 需要加上这行

TODO 4. 如果你用 nginx proxy, 还需要再 conf 文件里加上 proxy_set_header Connection ''; proxy_http_version 1.1; chunked_transfer_encoding off;

总结:sse 在 rails 里还是做不到开箱即用,有些细节和小坑要注意下

0 楼 已删除
1 楼 已删除

我编辑 0 楼回复不小心删掉了。原话是

原话是 Rails 不适合做 SSE,一个 SSE 要占用一个 puma thread。不如直接用 websocket,链接不占用 puma thread。 实在要用 SSE 可以用 go 实现 SSE,类似于https://anycable.io/ 的架构,SSE 链接是 go server 维护,Rails application 给 go server 发请求让 go server 执行 SSE 推送。

piecehealth 回复

还以为是我删掉的。之前使用 ws 实现的,相应的对前端的要求就会高了,要处理 ws 的订阅,对话间切换,取消订阅。对话多了,很容易处理不好。

我其实之前做过一个 go 的 sse 转发服务。大体是

  1. rails application 有一个 api 生成 sse token。
  2. 浏览器用 sse token 尝试建立 go server 的 sse 链接。
  3. go server 拿 token 到 rails application 验证 token,验证成功就建立 sse 链接。
  4. rails application 请求 go server 的 private api 来发送 sse 消息。
308820773 回复

你要是用纯血 rails 的话,Hotwire 那一套对 websocket 更友好,sse 查无此人了。

回答最初的问题

TODO 1. SSE.new(response.stream, retry: 300), 返回 sse 数据时,会把 retry: 300 返回,这个是正常?

正常,https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation sse 返回的 field names 包括 event, data, id, retry,你代码中的 status:, message:不是标准的用法。

TODO 2. ensure 这里不可以 close, 因为 rescue_from 的执行是在 ensure 之后的,有没有更优雅的写法

ensure 应该放到def talk里。正常的 talk 方法应该是

def talk
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream, retry: 300, event: "event-name")

   # sse的action一定是一个持续很长时间的action。
   loop do
     finished = Chats::GetAnswer.execute(params, sse)

     break if finished
   end
ensure
    sse.close
end
piecehealth 回复

谢谢大神,我研究研究

piecehealth 回复

昨天调用 sse 一直没成功,反复排查问题发现在 middleware 里调用了

request = ActionDispatch::Request.new(env)
request.body.body 
需要 登录 后方可回复, 如果你还没有账号请 注册新账号