我正在尝试在 Flask 微网络框架上构建一个具有服务器推送功能的小型站点,但我不知道是否有可以直接使用的框架。
我使用了Juggernaut,但在当前版本中它似乎无法与redis-py一起使用,并且 Juggernaut 最近已被弃用。
有人对我的案子有什么建议吗?
我正在尝试在 Flask 微网络框架上构建一个具有服务器推送功能的小型站点,但我不知道是否有可以直接使用的框架。
我使用了Juggernaut,但在当前版本中它似乎无法与redis-py一起使用,并且 Juggernaut 最近已被弃用。
有人对我的案子有什么建议吗?
看看Server-Sent Events。Server-Sent Events 是一种浏览器 API,可让您保持打开服务器的套接字,订阅更新流。欲了解更多信息,请阅读 Alex MacCaw(Juggernaut 的作者)发布的关于他为何杀死 juggernaut以及为什么在许多情况下更简单的服务器发送事件是比 Websockets 更好的工作工具的帖子。
该协议非常简单。只需将 mimetype 添加text/event-stream
到您的响应中。浏览器将保持连接打开并监听更新。从服务器发送的事件是一行以换行符开头的文本data:
。
data: this is a simple message
<blank line>
如果您想交换结构化数据,只需将您的数据转储为 json 并通过网络发送 json。
一个优点是您可以在 Flask 中使用 SSE,而无需额外的服务器。github 上有一个简单的聊天应用程序示例,它使用 redis 作为 pub/sub 后端。
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('/post', methods=['POST'])
def post():
message = flask.request.form['message']
user = flask.session.get('user', 'anonymous')
now = datetime.datetime.now().replace(microsecond=0).time()
red.publish('chat', u'[%s] %s: %s' % (now.isoformat(), user, message))
@app.route('/stream')
def stream():
return flask.Response(event_stream(),
mimetype="text/event-stream")
您不需要使用 gunicron 来运行示例应用程序。只需确保在运行应用程序时使用线程,否则 SSE 连接将阻塞您的开发服务器:
if __name__ == '__main__':
app.debug = True
app.run(threaded=True)
在客户端,您只需要一个 Javascript 处理函数,当从服务器推送新消息时将调用该函数。
var source = new EventSource('/stream');
source.onmessage = function (event) {
alert(event.data);
};
最近的 Firefox、Chrome 和 Safari 浏览器支持服务器发送事件。Internet Explorer 尚不支持服务器发送事件,但预计将在版本 10 中支持它们。推荐使用两种 Polyfill 来支持旧版浏览器
聚会迟到了(像往常一样),但恕我直言,使用 Redis 可能有点矫枉过正。
只要您使用 Python+Flask,就可以考虑使用Panisuan Joe Chasinga 的这篇优秀文章中描述的生成器函数。它的要点是:
var targetContainer = document.getElementById("target_div");
var eventSource = new EventSource("/stream")
eventSource.onmessage = function(e) {
targetContainer.innerHTML = e.data;
};
...
<div id="target_div">Watch this space...</div>
def get_message():
'''this could be any function that blocks until data is ready'''
time.sleep(1.0)
s = time.ctime(time.time())
return s
@app.route('/')
def root():
return render_template('index.html')
@app.route('/stream')
def stream():
def eventStream():
while True:
# wait for source data to be available, then push it
yield 'data: {}\n\n'.format(get_message())
return Response(eventStream(), mimetype="text/event-stream")
作为@peter-hoffmann 回答的后续,我编写了一个专门用于处理服务器发送事件的 Flask 扩展。它称为Flask-SSE,可在 PyPI 上使用。要安装它,请运行:
$ pip install flask-sse
你可以像这样使用它:
from flask import Flask
from flask_sse import sse
app = Flask(__name__)
app.config["REDIS_URL"] = "redis://localhost"
app.register_blueprint(sse, url_prefix='/stream')
@app.route('/send')
def send_message():
sse.publish({"message": "Hello!"}, type='greeting')
return "Message sent!"
要从 Javascript 连接到事件流,它的工作方式如下:
var source = new EventSource("{{ url_for('sse.stream') }}");
source.addEventListener('greeting', function(event) {
var data = JSON.parse(event.data);
// do what you want with this data
}, false);
文档可在 ReadTheDocs 上找到。请注意,您需要一个正在运行的Redis服务器来处理发布/订阅。
作为https://github.com/WolfgangFahl/pyFlaskBootstrap4的提交者,我遇到了同样的需求,并为不依赖于 redis 的服务器发送事件创建了一个烧瓶蓝图。
该解决方案建立在过去在这里给出的其他答案的基础上。
https://github.com/WolfgangFahl/pyFlaskBootstrap4/blob/main/fb4/sse_bp.py有源代码(另见下面的 sse_bp.py)。
在https://github.com/WolfgangFahl/pyFlaskBootstrap4/blob/main/tests/test_sse.py有单元测试
这个想法是您可以使用不同的模式来创建您的 SSE 流:
截至 2021 年 2 月 12 日,这是我想分享的 alpha 代码。请在此处发表评论或作为项目中的问题发表评论。
http://fb4demo.bitplan.com/events有一个演示,示例使用说明,例如进度条或时间显示:http ://wiki.bitplan.com/index.php/PyFlaskBootstrap4#Server_Sent_Events
示例客户端 javascript/html 代码
<div id="event_div">Watch this space...</div>
<script>
function fillContainerFromSSE(id,url) {
var targetContainer = document.getElementById(id);
var eventSource = new EventSource(url)
eventSource.onmessage = function(e) {
targetContainer.innerHTML = e.data;
};
};
fillContainerFromSSE("event_div","/eventfeed");
</script>
示例服务器端代码
def getTimeEvent(self):
'''
get the next time stamp
'''
time.sleep(1.0)
s=datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
return s
def eventFeed(self):
'''
create a Server Sent Event Feed
'''
sse=self.sseBluePrint
# stream from the given function
return sse.streamFunc(self.getTimeEvent)
sse_bp.py
'''
Created on 2021-02-06
@author: wf
'''
from flask import Blueprint, Response, request, abort,stream_with_context
from queue import Queue
from pydispatch import dispatcher
import logging
class SSE_BluePrint(object):
'''
a blueprint for server side events
'''
def __init__(self,app,name:str,template_folder:str=None,debug=False,withContext=False):
'''
Constructor
'''
self.name=name
self.debug=debug
self.withContext=False
if template_folder is not None:
self.template_folder=template_folder
else:
self.template_folder='templates'
self.blueprint=Blueprint(name,__name__,template_folder=self.template_folder)
self.app=app
app.register_blueprint(self.blueprint)
@self.app.route('/sse/<channel>')
def subscribe(channel):
def events():
PubSub.subscribe(channel)
self.stream(events)
def streamSSE(self,ssegenerator):
'''
stream the Server Sent Events for the given SSE generator
'''
response=None
if self.withContext:
if request.headers.get('accept') == 'text/event-stream':
response=Response(stream_with_context(ssegenerator), content_type='text/event-stream')
else:
response=abort(404)
else:
response= Response(ssegenerator, content_type='text/event-stream')
return response
def streamGen(self,gen):
'''
stream the results of the given generator
'''
ssegen=self.generateSSE(gen)
return self.streamSSE(ssegen)
def streamFunc(self,func,limit=-1):
'''
stream a generator based on the given function
Args:
func: the function to convert to a generator
limit (int): optional limit of how often the generator should be applied - 1 for endless
Returns:
an SSE Response stream
'''
gen=self.generate(func,limit)
return self.streamGen(gen)
def generate(self,func,limit=-1):
'''
create a SSE generator from a given function
Args:
func: the function to convert to a generator
limit (int): optional limit of how often the generator should be applied - 1 for endless
Returns:
a generator for the function
'''
count=0
while limit==-1 or count<limit:
# wait for source data to be available, then push it
count+=1
result=func()
yield result
def generateSSE(self,gen):
for result in gen:
yield 'data: {}\n\n'.format(result)
def enableDebug(self,debug:bool):
'''
set my debugging
Args:
debug(bool): True if debugging should be switched on
'''
self.debug=debug
if self.debug:
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s.%(msecs)03d %(levelname)s:\t%(message)s', datefmt='%Y-%m-%d %H:%M:%S')
def publish(self, message:str, channel:str='sse', debug=False):
"""
Publish data as a server-sent event.
Args:
message(str): the message to send
channel(str): If you want to direct different events to different
clients, you may specify a channel for this event to go to.
Only clients listening to the same channel will receive this event.
Defaults to "sse".
debug(bool): if True enable debugging
"""
return PubSub.publish(channel=channel, message=message,debug=debug)
def subscribe(self,channel,limit=-1,debug=False):
def stream():
for message in PubSub.subscribe(channel,limit,debug=debug):
yield str(message)
return self.streamGen(stream)
class PubSub:
'''
redis pubsub duck replacement
'''
pubSubByChannel={}
def __init__(self,channel:str='sse',maxsize:int=15, debug=False,dispatch=False):
'''
Args:
channel(string): the channel name
maxsize(int): the maximum size of the queue
debug(bool): whether debugging should be switched on
dispatch(bool): if true use the pydispatch library - otherwise only a queue
'''
self.channel=channel
self.queue=Queue(maxsize=maxsize)
self.debug=debug
self.receiveCount=0
self.dispatch=False
if dispatch:
dispatcher.connect(self.receive,signal=channel,sender=dispatcher.Any)
@staticmethod
def reinit():
'''
reinitialize the pubSubByChannel dict
'''
PubSub.pubSubByChannel={}
@staticmethod
def forChannel(channel):
'''
return a PubSub for the given channel
Args:
channel(str): the id of the channel
Returns:
PubSub: the PubSub for the given channel
'''
if channel in PubSub.pubSubByChannel:
pubsub=PubSub.pubSubByChannel[channel]
else:
pubsub=PubSub(channel)
PubSub.pubSubByChannel[channel]=pubsub
return pubsub
@staticmethod
def publish(channel:str,message:str,debug=False):
'''
publish a message via the given channel
Args:
channel(str): the id of the channel to use
message(str): the message to publish/send
Returns:
PubSub: the pub sub for the channel
'''
pubsub=PubSub.forChannel(channel)
pubsub.debug=debug
pubsub.send(message)
return pubsub
@staticmethod
def subscribe(channel,limit=-1,debug=False):
'''
subscribe to the given channel
Args:
channel(str): the id of the channel to use
limit(int): limit the maximum amount of messages to be received
debug(bool): if True debugging info is printed
'''
pubsub=PubSub.forChannel(channel)
pubsub.debug=debug
return pubsub.listen(limit)
def send(self,message):
'''
send the given message
'''
sender=object();
if self.dispatch:
dispatcher.send(signal=self.channel,sender=sender,msg=message)
else:
self.receive(sender,message)
def receive(self,sender,message):
'''
receive a message
'''
if sender is not None:
self.receiveCount+=1;
if self.debug:
logging.debug("received %d:%s" % (self.receiveCount,message))
self.queue.put(message)
def listen(self,limit=-1):
'''
listen to my channel
this is a generator for the queue content of received messages
Args:
limit(int): limit the maximum amount of messages to be received
Return:
generator: received messages to be yielded
'''
if limit>0 and self.receiveCount>limit:
return
yield self.queue.get()
def unsubscribe(self):
'''
unsubscribe me
'''
if self.dispatch:
dispatcher.disconnect(self.receive, signal=self.channel)
pass