Flask-SocketIO 中文文档
Flask-SocketIO使Flask应用程序能够访问客户端和服务器之间的低延迟双向通信。客户端应用程序可以使用 Javascript、Python、C++、Java 和 Swift 中的任何一个SocketIO客户端库,或任何其他兼容的客户端来建立与服务器的永久连接。
介绍(Introduction)
安装(Installation)
你可以用通常的方式用pip
来安装这个包:
1 |
|
要求(Requirements)
Flask-SocketIO与Python 3.6以上版本兼容。这个包所依赖的异步服务可以在三种选择中选择:
- eventlet是性能最好的选择,它支持长轮询和WebSocket传输。
- gevent支持多种不同的配置。gevent包完全支持长轮询运输,但与eventlet不同,gevent没有原生的WebSocket支持。要添加对WebSocket的支持,目前有两种选择。安装gevent-websocket包可以为gevent添加WebSocket支持,或者可以使用自带WebSocket功能的uWSGI网络服务器。使用gevent也是一个高性能的选择,但比eventlet略低。
- 也可以使用基于Werkzeug的Flask开发服务器,但需要注意的是,这个网络服务器只用于开发使用,所以它只能用于简化开发工作流程,不能用于生产。
该扩展会根据安装的内容自动检测要使用的异步框架。优先考虑eventlet,其次是gevent。对于gevent的WebSocket支持,首选uWSGI,其次是gevent-websocket。如果eventlet和gevent都没有安装,那么就使用Flask开发服务器。
如果使用多个进程,必须配置一个消息队列服务,以使服务器能够协调诸如广播等操作。支持的队列有Redis、RabbitMQ、Kafka,以及Kombu包支持的任何其他消息队列。
在客户端,可以使用官方的Socket.IO Javascript客户端库来建立与服务器的连接。也有用Swift、Java和C++编写的官方客户端。非官方的客户端也可以工作,只要它们实现了Socket.IO协议。python-socketio包(提供Flask-SocketIO使用的Socket.IO服务器实现)包括一个Python客户端。
版本兼容性(Version compatibility)
Socket.IO协议已经经历了多次修订,其中一些修订引入了向后不兼容的变化,这意味着客户端和服务器必须使用兼容的版本才能工作。
下面的版本兼容性图表将这个包的版本与JavaScript参考实现的版本以及Socket.IO和Engine.IO协议的版本相匹配。
开始(Getting Started)
初始化(Initialization)
下面的代码示例展示了如何将Flask-SocketIO添加到Flask应用程序中:
1 |
|
也支持init_app()
风格的初始化。要启动Web服务器,只需执行你的脚本。注意网络服务器的启动方式。socketio.run()
函数封装了Web服务器的启动,并取代了app.run()
标准Flask开发服务器的启动。当应用程序处于调试模式时,Werkzeug开发服务器仍被使用,并在socketio.run()
中正确配置。在生产模式下,如果有的话,会使用eventlet web服务器,否则会使用gevent web服务器。如果eventlet和gevent没有安装,则使用Werkzeug开发的web服务器。
Flask 0.11中引入的flask run
命令可用于启动基于Werkzeug的Flask-SocketIO开发服务器,但由于缺乏WebSocket支持,不建议使用这种方法启动Flask-SocketIO服务器。该软件包的先前版本包括一个自定义版本的flask run
命令,允许在eventlet和gevent生产服务器上使用WebSocket,但这一功能已停止使用,而采用上文所示的socketio.run(app)
启动方法,该方法更加强大。
应用程序必须向客户端提供一个页面,加载Socket.IO库并建立一个连接:
1 |
|
接收消息(Receiving Messages)
当使用SocketIO时,消息被双方作为事件接收。在客户端使用的是 Javascript 回调。使用 Flask-SocketIO,服务器需要为这些事件注册处理程序,类似于视图函数处理路由的方式。
下面的例子为一个未命名的事件创建了一个服务器端的事件处理程序:
1 |
|
上面的例子使用了字符串信息。另一种类型的未命名事件使用JSON数据:
1 |
|
最灵活的事件类型使用自定义事件名称。这些事件的消息数据可以是字符串、字节、int或JSON:
1 |
|
自定义命名的事件也可以支持多个参数:
1 |
|
当事件的名称是一个有效的Python标识符,不与其他定义的符号相冲突时,@socketio.event
装饰器提供了一个更紧凑的语法,从装饰的函数中获取事件名称:
1 |
|
命名的事件是最灵活的,因为它们不需要包括额外的元数据来描述消息类型。message
、json
、connect
和disconnect
这些名称是保留的,不能用于命名事件。
Flask-SocketIO还支持SocketIO命名空间,它允许客户端在同一个物理套接字上复用几个独立的连接:
1 |
|
当没有指定命名空间时,会使用默认的全局命名空间,名称为'/'
。
对于装饰器语法不方便的情况,可以使用on_event
方法:
1 |
|
客户端可以请求一个确认回调,确认收到他们发送的消息。从处理函数返回的任何值将作为回调函数的参数传递给客户端:
1 |
|
在上面的例子中,客户端回调函数将被调用,有两个参数,'one'
和2
。如果一个处理函数没有返回任何值,客户端回调函数将被调用,没有参数。
发送消息(Sending Messages)
如上节所示定义的SocketIO事件处理程序可以使用send()
和emit()
函数向连接的客户端发送回复信息。
下面的例子将收到的事件反弹到发送事件的客户端:
1 |
|
注意send()
和emit()
是如何分别用于无名和有名事件的。
当使用命名空间时,send()
和emit()
默认使用传入消息的命名空间。不同的命名空间可以通过可选的namespace
参数来指定:
1 |
|
要发送一个有多个参数的事件,请发送一个元组:
1 |
|
SocketIO支持确认回调,确认客户端收到了消息:
1 |
|
当使用回调时,Javascript客户端会收到一个回调函数,以便在收到消息时调用。在客户端应用程序调用回调函数后,服务器会调用相应的服务器端回调。如果客户端的回调被调用时带有参数,这些参数也会作为参数提供给服务器端的回调。
广播(Broadcasting)
SocketIO的另一个非常有用的功能是消息的广播。Flask-SocketIO通过send()
和emit()
的broadcast=True
可选参数来支持这一功能:
1 |
|
当启用广播选项发送消息时,连接到命名空间的所有客户都会收到该消息,包括发送者。当不使用命名空间时,连接到全局命名空间的客户端会收到该消息。请注意,对于广播消息,回调不会被调用。
在这之前显示的所有例子中,服务器都对客户端发送的事件做出响应。但是对于某些应用,服务器需要成为消息的发起者。这对于向客户端发送源于服务器的事件的通知很有用,例如在一个后台线程中。socketio.send()
和socketio.emit()
方法可以用来向所有连接的客户端广播:
1 |
|
注意socketio.send()
和socketio.emit()
与上下文感知的send()
和emit()
是不同的函数。还要注意的是,在上面的用法中,没有客户端上下文,所以broadcast=True
是假定的,不需要指定。
房间(Rooms)
对于许多应用来说,有必要将用户分成可以一起处理的子集。最好的例子是具有多个房间的聊天应用程序,用户可以从他们所在的房间或房间接收消息,但不能从其他用户所在的房间接收。Flask-SocketIO通过 join_room()
和leave_room()
函数支持这种房间的概念:
1 |
|
在上面的例子中,send()
和emit()
函数接受一个可选的to
参数,导致消息被发送到所有在给定房间的客户端。
所有客户端在连接时都被分配了一个房间,用连接的会话ID命名,这个ID可以从request.sid
获得。一个给定的客户端可以加入任何房间,这些房间可以被赋予任何名字。当客户端断开连接时,它将从它所在的所有房间中删除。无上下文的socketio.send()
和socketio.emit()
函数也接受一个to
参数,用于向一个房间的所有客户端广播。
由于所有的客户端都被分配了一个个人房间,为了向单个客户端发送消息,可以使用客户端的会话ID作为到参数。
连接事件(Connection Events)
Flask-SocketIO还分派连接和断开事件。下面的例子展示了如何为它们注册处理程序:
1 |
|
连接处理程序中的auth
参数是可选的。客户端可以用它来传递认证数据,如字典格式的令牌。如果客户端不提供认证细节,那么这个参数被设置为None
。如果服务器定义的连接事件处理程序没有这个参数,那么客户端传递的任何认证数据将被丢弃。
连接事件处理程序可以返回False
以拒绝连接,或者也可以引发ConnectionRefusedError。这是为了让客户端在这时可以进行身份验证。当使用异常时,传递给异常的任何参数都会在错误数据包中返回给客户端。例子如下:
1 |
|
请注意,连接和断开连接事件是在每个使用的命名空间上单独发送的。
基于类的命名空间(Class-Based Namespaces)
作为上述基于装饰器的事件处理程序的替代方案,属于命名空间的事件处理程序可以作为类的方法来创建。flask_socketio.Namespace被提供为基类,以创建基于类的命名空间:
1 |
|
当使用基于类的命名空间时,服务器收到的任何事件都会被派发到一个以事件名称命名的方法中,并加上on_
的前缀。例如,事件my_event
将被一个名为on_my_event
的方法处理。如果收到的事件在命名空间类中没有定义相应的方法,那么该事件将被忽略。在基于类的命名空间中使用的所有事件名称必须使用方法名称中合法的字符。
为了方便在基于类的命名空间中定义的方法,命名空间实例包括flask_socketio.SocketIO类中几个方法的版本,当没有给出namespace
参数时,这些方法默认为适当的命名空间。
如果一个事件在基于类的命名空间中有一个处理程序,也有一个基于装饰器的函数处理程序,那么只有装饰的函数处理程序被调用。
错误处理(Error Handling)
Flask-SocketIO还可以处理异常:
1 |
|
错误处理函数将异常对象作为一个参数。
当前请求的消息和数据参数也可以通过request.event
变量进行检查,这对于事件处理程序之外的错误记录和调试非常有用:
1 |
|
调试和故障排除(Debugging and Troubleshooting)
为了帮助你调试问题,可以将服务器配置为向终端输出日志:
1 |
|
,logger
参数控制与Socket.IO协议相关的日志,而engineio_logger
控制源于低级别的Engine.IO传输的日志。这些参数可以被设置为True,以便将日志输出到stderr
,或者输出到与Python的logging
包兼容的对象,日志应该被排放到那里。值为False
则禁用日志记录。
日志可以帮助确定连接问题、400响应、性能不良和其他问题的原因。
实施说明(Implementation Notes)
访问Flask的上下文全局(Access to Flask’s Context Globals)
SocketIO事件的处理程序与路由的处理程序不同,这给SocketIO处理程序中能做什么和不能做什么带来了很多困惑。主要的区别是,所有为客户端产生的SocketIO事件都发生在一个长期运行的请求的上下文中。
尽管存在这些差异,Flask-SocketIO 试图通过使环境与普通的 HTTP 请求相似,使使用 SocketIO 事件处理程序变得更容易。下面的列表描述了哪些是可行的,哪些是不可行的。
- 在调用事件处理程序之前会推送一个应用程序上下文,使处理程序可以使用
current_app
和g
。 - 请求上下文在调用处理程序之前也被推送,同样使
request
和session
可用。但请注意,WebSocket事件没有与之相关联的单个请求,因此开始连接的请求上下文被推送给在连接期间派发的所有事件。 request
上下文全局被增强了一个sid
成员,它被设置为连接的唯一会话ID。这个值被用作添加客户端的初始房间。request
上下文全局被增强了naomespace
和event
成员,这些成员包含当前处理的命名空间和事件论据。event
成员是一个带有message
和args
键的字典。session
上下文全局的行为方式与普通请求不同。在建立SocketIO连接时,用户会话的副本被提供给在该连接的上下文中调用的处理程序使用。如果一个SocketIO处理程序修改了会话,被修改的会话将被保留给未来的SocketIO处理程序,但常规的HTTP路由处理程序不会看到这些变化。实际上,当SocketIO处理程序修改会话时,会话的一个 “分叉 “是专门为这些处理程序创建的。这种限制的技术原因是,为了保存用户会话,需要向客户端发送cookie,而这需要HTTP请求和响应,这在SocketIO连接中不存在。当使用服务器端的会话时,如Flask-Session或Flask-KVSession扩展提供的会话,只要会话没有在SocketIO处理程序中被修改,那么在HTTP路由处理程序中对会话所做的改变就可以被SocketIO处理程序看到。before_request
和after_request
hooks不会被SocketIO事件处理程序调用。- SocketIO处理程序可以使用自定义的装饰器,但大多数Flask装饰器不适合用于SocketIO处理程序,因为在SocketIO连接中没有
response
对象的概念。
认证(Authentication)
应用程序的一个常见需求是验证其用户的身份。基于Web表单和HTTP请求的传统机制不能在SocketIO连接中使用,因为没有地方可以发送HTTP请求和响应。如果有必要,应用程序可以实现一个定制的登录表单,当用户按下提交按钮时,它将凭证作为SocketIO消息发送到服务器。
然而,在大多数情况下,在建立SocketIO连接之前执行传统的认证过程会更方便。然后,用户的身份可以被记录在用户会话或cookie中,以后当SocketIO连接建立时,这些信息将被SocketIO事件处理程序访问。
最近对Socket.IO协议的修订包括在连接过程中传递带有认证信息的字典的能力。这是客户端包括令牌或其他认证细节的理想场所。如果客户端使用这种能力,服务器将提供这个字典作为connect
事件处理程序的参数,如上所示。
使用 Flask-Login与Flask-SocketIO(Using Flask-Login with Flask-SocketIO)
Flask-SocketIO可以访问由Flask-Login维护的登录信息。在执行常规的Flask-Login身份验证并调用login_user()
函数将用户记录在用户会话中后,任何SocketIO连接都可以访问current_user
环境变量:
1 |
|
请注意,login_required
装饰器不能与SocketIO事件处理程序一起使用,但可以按以下方式创建一个断开非认证用户连接的自定义装饰器:
1 |
|
部署(Deployment)
部署Flask-SocketIO服务器有许多选项,从简单到极其复杂不等。在本节中,将介绍最常用的选项。
嵌入式服务器(Embedded Server)
最简单的部署策略是通过调用socketio.run(app)
启动Web服务器,如上面的例子所示。这将在已安装的软件包中寻找最佳可用的Web服务器,在其上启动应用程序。目前被评估的Web服务器选择是eventlet
、gevent
和Flask开发服务器。
如果eventlet或gevent可用,socketio.run(app)
会使用这些框架中的一个来启动一个可生产的服务器。如果这些都没有安装,那么就使用Flask开发网络服务器,在这种情况下,服务器不打算用于生产部署。
不幸的是,在使用gevent和uWSGI时,这个选项是不可用的。关于这个选项的信息,请看下面的uWSGI部分。
Gunicorn web服务器(Gunicorn Web Server)
,socketio.run(app)
的另一个选择是使用gunicorn作为网络服务器,使用eventlet或gevent工作者。对于这个选项,除了gunicorn外,还需要安装eventlet或gevent。通过gunicorn启动eventlet服务器的命令行是:
1 |
|
如果你喜欢使用gevent,启动服务器的命令是:
1 |
|
当使用gunicorn与gevent worker和gevent-websocket提供的WebSocket支持时,必须修改启动服务器的命令,以选择支持WebSocket协议的自定义gevent网络服务器。修改后的命令是:
1 |
|
Gunicorn的第三个选择是使用线程工作者,同时使用simple-websocket包来支持WebSocket。对于那些CPU负荷较重的应用程序,或者与eventlet和gevent使用绿色线程不兼容的应用程序,这是一个特别好的解决方案。启动线程化Web服务器的命令是:
1 |
|
在所有这些命令中,module
是定义应用程序实例的Python模块或包,而app
是应用程序实例本身。
由于 gunicorn 使用的有限的负载平衡算法,在使用这个 web 服务器时不可能使用一个以上的工作进程。出于这个原因,上面的所有例子都包括了-w 1
选项。
使用gunicorn的多个工作进程的变通方法是启动几个单一的工作实例,并把它们放在一个能力更强的负载均衡器后面,如nginx。
uWSGI web服务器(uWSGI Web Server)
当uWSGI服务器与gevent结合使用时,Socket.IO服务器可以利用uWSGI的本地WebSocket支持。
对uWSGI服务器的配置和使用的完整解释超出了本文档的范围。uWSGI服务器是一个相当复杂的包,提供了大量全面的选项。它必须在编译时支持WebSocket和SSL,才能使用WebSocket传输。作为介绍,下面的命令在5000端口为示例程序app.py启动一个uWSGI服务器:
1 |
|
使用nginx作为WebSocket反向代理(Using nginx as a WebSocket Reverse Proxy)
可以将nginx作为一个前端反向代理,将请求传递给应用程序。然而,只有nginx 1.4和更新的版本支持WebSocket协议的代理。下面是一个基本的nginx配置,可以代理HTTP和WebSocket请求:
1 |
|
下面的例子增加了对多个Socket.IO服务器的负载平衡的支持:
1 |
|
虽然上述例子可以作为初始配置,但要注意的是,在生产中安装nginx将需要一个更完整的配置,包括其他部署方面,如SSL支持。
使用多个工作器(Using Multiple Workers)
从2.0版开始,Flask-SocketIO支持负载平衡器后面的多个工作器。部署多个工作器使使用Flask-SocketIO的应用程序能够将客户端连接分散到多个进程和主机中,并以这种方式扩展到支持大量的并发客户端。
使用多个Flask-SocketIO工作器有两个要求:
- 负载均衡器必须被配置为将来自特定客户端的所有 HTTP 请求始终转发到同一个工作器。这有时被称为 “粘性会话(sticky sessions)”。对于nginx,使用
ip_hash
指令来实现这一点。Gunicorn不能用于多个工作器,因为它的负载平衡算法不支持粘性会话。 - 由于每个服务器只拥有客户端连接的一个子集,服务器使用Redis或RabbitMQ等消息队列来协调广播和房间等复杂操作。
当使用消息队列时,需要安装一些额外的依赖项:
- 对于Redis,必须安装
redis
包(pip install redis
)。 - 对于RabbitMQ,必须安装
kombu
包(pip install kombu
)。 - 对于Kafka,必须安装
kafka-python
包(pip install kafka-python
)。 - 对于Kombu支持的其他消息队列,请参见Kombu文档以了解需要哪些依赖。
- 如果使用eventlet或gevent,那么通常需要对Python标准库进行monkey patch,以强制消息队列包使用coroutine友好的函数和类。
对于eventlet,monkey patching是通过以下方式完成的:
1 |
|
对于gevent,你可以monkey patch修补标准库:
1 |
|
在这两种情况下,建议你在主脚本的顶部应用monkey patching,甚至在你的导入之上。
要启动多个Flask-SocketIO服务器,你必须首先确保你已经运行了消息队列服务。要启动一个Socket.IO服务器并让它连接到消息队列,请在SocketIO
构造函数中添加message_queue
参数:
1 |
|
,message_queue
参数的值是所使用的队列服务的连接URL。对于与服务器在同一主机上运行的 redis 队列,可以使用'redis://'
URL。同样,对于一个默认的RabbitMQ队列,可以使用'amqp://'
URL。对于Kafka,使用kafka://
URL。Kombu包有一个文档部分,描述了所有支持的队列的URL的格式。
从一个外部过程中发出事件(Emitting from an External Process)
对于许多类型的应用程序,有必要从一个不是SocketIO服务器的进程发出事件,例如Celery工作器。如果SocketIO服务器或服务器被配置为在消息队列上监听,如上一节所示,那么任何其他进程都可以创建自己的SocketIO
实例,并以服务器的方式使用它来发出事件。
例如,对于一个在eventlet web服务器上运行并使用Redis消息队列的应用程序,下面的Python脚本向所有客户端广播了一个事件:
1 |
|
当以这种方式使用SocketIO
实例时,Flask应用程序实例不会被传递给构造函数。
SocketIO的channel
参数可以用来选择通过消息队列进行通信的特定通道。当有多个独立的SocketIO服务共享同一个队列时,使用一个自定义的通道名称是必要的。
当使用eventlet或gevent时,Flask-SocketIO不应用monkey patching。但是当使用消息队列时,如果Python标准库没有被monkey patched,与消息队列服务对话的Python包很可能会挂掉。
需要注意的是,想要连接到SocketIO服务器的外部进程不需要像主服务器那样使用eventlet或gevent。让服务器使用一个coroutine框架,而外部进程并不是一个问题。例如,Celery工作器不需要因为主服务器使用eventlet或gevent而被配置为使用eventlet或gevent。但是,如果你的外部进程由于某种原因使用了一个coroutine框架,那么可能需要进行monkey patching,以便消息队列访问coroutine友好的函数和类。
跨源控制(Cross-Origin Controls)
出于安全考虑,本服务器默认执行同源策略。在实践中,这意味着以下几点:
- 如果一个传入的HTTP或WebSocket请求包括
Origin
头,这个头必须与连接URL的方案和主机相匹配。在不匹配的情况下,将返回400状态代码响应,并拒绝连接。 - 对于不包括
Origin
头的传入请求,没有任何限制。
如果需要,可以使用cors_allowed_origins
选项来允许其他来源。这个参数可以设置为一个字符串,以设置一个允许的起源,或者设置为一个列表,以允许多个起源。一个特殊的值'*'
可以用来指示服务器允许所有的来源,但这应该小心行事,因为这可能使服务器容易受到跨站请求伪造(CSRF)攻击。
Flask-SocktetIO从4.x版本升级到5.x(Upgrading to Flask-SocketIO 5.x from the 4.x releases)
Socket.IO协议最近引入了一系列向后不兼容的变化。Flask-SocketIO的5.x版本采用了这些变化,为此,它只能与也已更新到当前版本协议的客户端一起使用。特别是,这意味着JavaScript客户端必须升级到3.x版本,如果你的客户端还没有升级到Socket.IO协议的最新版本,那么你必须使用Flask-SocketIO 4.x版本。
以下协议变化非常重要,因为它们可能会影响现有的应用程序:
- 默认命名空间
'/'
不再自动连接,现在与其他命名空间的处理方式相同。 - 每个命名空间的连接都有自己的
sid
值,与其他的不同,也与Engine.IO的sid
不同。 - Flask-SocketIO现在使用与JavaScript参考实现相同的ping间隔和超时值,分别为25和5秒。
- ping/pong机制已经被颠覆。在当前版本的协议中,服务器发出 ping,而客户端则以 pong 来回应。
- 长时间轮询数据包的默认允许有效载荷大小已从100MB降至1MB。
- io cookie在默认情况下不再被发送到客户端。
API参考(API Reference)
我累了:https://flask-socketio.readthedocs.io/en/latest/api.html
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!