Python - Flask-SocketIO 在线程中发送消息:并不总是有效

31

我遇到了这样一种情况:我从客户端接收到消息。在处理该请求的函数 (@socketio.on) 中,我想调用一个执行某些重型工作的函数。这不应该导致主线程被阻塞,而且应该在工作完成后通知客户端。因此我启动了一个新线程。

现在我遇到了一个非常奇怪的行为:

消息永远不会到达客户端。然而,代码却到达了发送消息的那个特定点。

更令人惊讶的是,如果线程中除了向客户端发送消息之外没有其他活动,则答复实际上会传递到客户端。

总之:

如果在发送消息之前发生了计算密集型操作,则消息将无法传递,否则可以传递。

就像这里这里所说的那样,从线程发送消息到客户端根本不是问题:

在目前为止显示的所有示例中,服务器都会响应客户端发送的事件。但对于某些应用程序,服务器需要是消息的发起者。这可以将服务器中发生的事件通知客户端,例如在后台线程中。

以下是示例代码。当删除注释符号(#)时,消息('foo from thread')不会传递到客户端,否则会传递。

from flask import Flask
from flask.ext.socketio import SocketIO, emit
app = Flask(__name__)
socketio = SocketIO(app)

from threading import Thread
import time 

@socketio.on('client command')
def response(data):
    thread = Thread(target = testThreadFunction)
    thread.daemon = True
    thread.start()

    emit('client response', ['foo'])

def testThreadFunction():
#   time.sleep(1)

    socketio.emit('client response', ['foo from thread'])

socketio.run(app)

我正在使用Python 3.4.3,Flask 0.10.1,flask-socketio1.2和eventlet 0.17.4。

这个示例可以复制并粘贴到.py文件中,行为可以立即重现。

有人能解释一下这种奇怪的行为吗?

更新

看起来这是eventlet的一个bug。如果我执行:

socketio = SocketIO(app, async_mode='threading')

即使已经安装,它也会强制应用程序不使用eventlet。

然而,对我来说,这不是可行的解决方案,因为使用'threading'作为async_mode会拒绝接受二进制数据。每次我从客户端发送一些二进制数据到服务器时,它会显示:

WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.

第三个选项,使用gevent作为async_mode对我也不起作用,因为gevent还不支持python 3。

还有其他建议吗?


@rfkortekaas 在异步协议中这有什么意义呢?除此之外,响应已经被发送了。这就是 "emit" 的作用。另一件事情是在一个单独的线程中发生的。 - Schnodderbalken
我删除了我的评论,因为我误读了问题。 - rfkortekaas
我遇到了一个非常类似的问题,但我们没有进行任何计算上昂贵的操作。这似乎是在调用socketio.emit时抓取正确的“socketio”时出现的问题。因为发射确实发生了,但没有客户端收到消息。添加async_mode ='threading'似乎解决了问题。但希望有更好的方法。 - AMB0027
@AMB0027 你认为这真的是问题所在吗?在生产中不建议使用基于Werkzeug的线程,因为它缺乏性能。此外,它只支持长轮询传输。使用Monkeypatching eventlet也没有帮助吗? - Schnodderbalken
@Schnodderbalken 猴子补丁解决了一些问题,但也破坏了其他东西。所以,不幸的是这并不是一个很好的解决方案。 - AMB0027
3个回答

17

7

我也遇到了同样的问题。

不过我想我找到了问题所在。

当使用以下代码启动SocketIO并创建像您一样的线程时,客户端无法接收由服务器发出的消息。

socketio.run()

我发现flask_socketio提供了一个名为start_background_task的函数,文档链接:document
以下是它的描述:

start_background_task(target, *args, **kwargs)

使用适当的异步模型启动后台任务。这是应用程序可以使用的实用程序函数,以使用与所选异步模式兼容的方法启动后台任务。

参数:

  • target:要执行的目标函数。
  • args:要传递给函数的参数。
  • kwargs:要传递给函数的关键字参数。

此函数返回与Python标准库中的Thread类兼容的对象。

此对象上的start()方法已由此函数调用。

因此,我将我的代码thread=threading(target=xxx)替换为socketio.start_background_task(target=xxx),然后运行socketio.run()。当进入线程时,服务器卡住了,这意味着start_background_task函数只有在线程完成后才返回。
然后我尝试使用gunicorn运行我的服务器:gunicorn --worker-class eventlet -w 1 web:app -b 127.0.0.1:5000
然后一切正常!
因此,让start_background_task选择适当的方式来启动线程。

我不明白。背景:我可能有同样的问题。我有一个运行线程的应用程序。这个线程应该经常发出状态,然后通过websocket发送到网页。但是只有第一个消息被发送了。a)我不明白这种行为的技术原因。你能解释一下吗?b)由于我的后台线程不能被我更改,如果start_background_task()显然不是一个选项,那么有什么解决方案可以将数据发送到客户端? - Regis May

3
您遇到的问题是由于eventlet和gevent(socket.io的两种线程模式)不支持多进程引起的。因此,这不是一个错误,而是实现方式。为了使其正常工作,您可以使用async_mode = threading,或者您可以猴子补丁evenlet以启用后台线程的使用。
socketio = SocketIO(app, async_mode='eventlet')
import eventlet
eventlet.monkey_patch()

使用async_mode=threading可以帮助阻止Flask服务器中的start_background_task阻塞。 - omerts

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