如何在Flask中关闭Server-Sent Events连接?

14

似乎 Flask 的作者还没有计划支持此功能。对于 “服务端发送事件(Server-Sent Events)”,最好使用像 NodeJS 这样的事件驱动架构。 - hllau
这条评论对于一个类似的问题也非常有帮助:this comment - colidyre
3个回答

9
重要的是请注意,您需要在客户端上关闭连接,否则它将在retry超时后尝试重新打开连接。这一点曾经让我感到困惑,直到我看到了这篇文章:https://dev59.com/I5ffa4cB1Zd3GeqP6Vj0#38235218 来自链接中的内容:
问题在于服务器意外关闭了连接,而不是在保持连接的情况下完成其工作。当发生这种情况时,客户端会重新发送请求以打开连接并开始流式传输服务器发送的事件。然后,服务器再次关闭连接,导致无限循环。
看完后,我的首选解决方案是通过产生预期的数据消息从服务器端关闭连接。 在客户端(浏览器)上:
var sse = new EventSource('/sse');
sse.addEventListener('message', function(e) {
  console.log(e);
  var data = e.data;
  if (!data) {
      console.log('no data in event');
      return;
  }
  if (data === 'finished') {
      console.log('closing connection')
      sse.close()
  }
  // my work here based on message
});

我的Flask服务器

from flask import Response

@app_api.route("/sse")
def stream():
    def trackerStream():
        finished = check_if_finished()
        while not finished:
            sleep(1) # poll timeout
            if should_send_event():
                yield f"data: {mydata()}\n\n"
            else:
                yield "data: nodata\n\n"

            finished = check_if_finished()

        yield "data: finished\n\n"

    return Response(trackerStream(), mimetype="text/event-stream")

注意:确保服务器向客户端定期发送事件。 如果用户关闭浏览器,Flask将在尝试写入套接字时出现错误,并会为您关闭流。 如果您没有在某个间隔写入客户端,即使您仅写入data: nodata\n\n,服务器也可能陷入循环中。

1
这真是太有用了!我感到惭愧的是,在输出完成后,我一直在使用服务器生成的XSS重定向负载将浏览器返回到主页面,以避免重新打开连接。在看到这个答案之前,这是我能想到的唯一方法来摆脱循环。非常有帮助。 - rubynorails

5

这取决于你的应用程序架构。

让我给你举个例子(请参见此代码:https://github.com/jkbr/chat/blob/master/app.py):

def event_stream():
    pubsub = red.pubsub()
    pubsub.subscribe('chat')
    for message in pubsub.listen():
        print message
        yield 'data: %s\n\n' % message['data']

@app.route('/stream')
def stream():
    return flask.Response(event_stream(),
                          mimetype="text/event-stream")

Flask稳定地向Redis请求新消息(锁定操作),但当Flask发现流终止时(如果您不是Python的新手,则为 StopIteration ),它会返回。

def event_stream():
    pubsub = red.pubsub()
    pubsub.subscribe('chat')
    for message in pubsub.listen():
        if i_should_close_the_connection:
            break
        yield 'data: %s\n\n' % message['data']

@app.route('/stream')
def stream():
    return flask.Response(event_stream(),
                          mimetype="text/event-stream")

2
i_should_close_the_connection是什么?你能再解释一下吗? - hllau
这是一个布尔值。你可以像在Python中一样使用那个 if 语句。 - gioi
1
我相信listen()是一个阻塞调用。如果没有发布任何内容并且客户端断开连接,服务器将永远挂起。我曾经遇到过这个问题。 - David Xia
是的,这是一个阻塞调用。您可能希望使用stdlib中的超时,并使用pubsub.close()关闭连接。它“应该”可以工作,但我还没有检查过。 - gioi

0

我曾经遇到同样的问题,最终在这里找到了以下解决方案。

import time
from flask import Flask, Response, stream_with_context

app = Flask(__name__)

@app.route('/stream')
def stream():
    def gen():
        try:
            i = 0
            while True:
                data = 'this is line {}'.format(i)
                print(data)
                yield data + '<br>'
                i += 1
                time.sleep(1)
        except GeneratorExit:
            print('closed')

    return Response(stream_with_context(gen()))

您可以通过向Redis订阅一个监听器来启动该方法。您可以等待Redis的listen()方法返回值,而不是使用time.sleep(1)进行等待。您可以取消订阅Redis,而不是使用print('closed')。唯一剩下的问题是GeneratorExit异常仅在将yield值发送到客户端时才会引发。因此,如果Redis的listen()永远不会结束,则永远无法发现连接已断开。


我花了一些时间才意识到服务器在浏览器关闭后可能会保持连接一段时间。(在我的情况下,服务器只有在大约150秒后才关闭连接。)关键是,它确实有效! :) - Arel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接