关于flask-SocketIO 开发部署详解

左丘阳晖
2023-12-01

上回我们说到了一些小功能,这次再对flask应用做一个补充,交代flask的上下文变量,身份认证等功能。

当我们的flask应用做好之后就不得不涉及到部署,本文将会讨论flask-socketio的三种不同类型的服务器部署,nginx的反向代理,外部进程消息的处理等

到今天为止,flask-socketio官方文档,除了api,其它都已经翻译完毕,感谢各位的赏脸。_

11.访问flask上下文全局变量

SocketIO活动处理不同于路由处理,在于它引入了许多容易混淆的东西,围绕着SocketIO什么可以做,什么不可以做。最主要的区别就是SocketIO活动发生在单个长期运行在上下文的请求之中。

尽管有所不同,Flask-SocketIO将环境改造成类似于常规HTTP请求,使SocketIO活动处理更加轻松。接下来的列表描述了什么将会生效,什么不会。

  • 在活动处理函数之前推送应用的上下文使得current_appg可以在处理函数中可用。

  • 这个请求的上下文同样在回调处理函数前被启用,也使requestsession可用。但是注意到WebSocket活动与之并没有独立的联系,因此为连接期间分派的所有事件推送启动连接的请求上下文。

  • request上下文全局变量随一个sid成员增加,这个成员是为了给连接一个独特的会话编号(session ID)。这个值在客户端刚刚添加的时候,就被最初的房间使用了。

  • request上下文全局变量由包含了当前处理函数的命名空间和活动参数的argumentevent来增加。这个活动成员是一个包含了messageargs键值的字典。

  • session上下文全局变量表现得和通常的请求不一样。在连接开始建立的时候,就会复制一份用户的会话在这个连接上下文中给处理器调用。如果SocketIO处理器修改了这个会话,这个修改过的会话就会为未来的SocketIO处理器保留,但是正常的HTTP路由处理器不会察觉这些改变。有效率的是,当SocketIO处理器改变这个会话的时候,会话就会为这些处理器创建一个“分支”(fork)。这个限制的技术原因是用户的会话cookie必须要发送到客户端,这需要HTTP请求和应答而不是SocketIO连接。在使用服务端的会话时,比如那些由Flask-Session或者Flask-KVSession扩展提供的会话,在HTTP处理器中的会话改变也可以在SocketIO处理器中可见,只要这个会话不是在SocketIO处理器中修改的。

  • before_requestafter_request钩子不会调用SocketIO活动处理器。

  • SocketIO处理器可以使用自定义的装饰器,但是大多数Flask装饰器并不适于SocketIO处理器,考虑到SocketIO连接中没有Response对象这一概念。

12.身份认证

应用的共同需要就是验证他们用户的身份。自从SocketIO没有使用HTTP请求和应答,传统的基于网页表单和HTTP请求的机制不能用于SocketIO连接。如果需要的话,应用可以实施自定义的登陆表单,当用户按下提交按钮时,它利用一个SocketIO消息将证书发送到服务器。

然而,在大多数情况下,在SocketIO连接建立之前使用传统的身份验证方式会更加方便,用户的身份信息可以被记录下来作为用户会话或者cookie,之后在SocketIO连接建立起来的时候,这些信息也可以被SocketIO活动处理器得到。

13.使用Flask-SocketIO的Flask-Login模块

Flask-SocketIO可以获得由Flask-Login维护的登陆信息。在一个正常的Flask-Login身份认证被使用的时候,login_user()函数将会被调用去记录用户会话中的用户,任何SocketIO连接都可以得到current_user上下文变量:

 

@socketio.on('connect')
def connect_handler():
    if current_user.is_authenticated:
        emit('my response',
            {'message':'{0} has joined'.format(current_user.name)},
            broadcast=True
        )
    else:
        return False # not allowed here

注意到login_required装饰器不能和SocketIO活动处理器一起使用,但是一个自定义的关闭连接无身份认证的装饰器可以按下面的方式创建:

 

import functools
from flask import request
from flask_login import current_user
from flask_socketio import disconnect

def authenticated_only(f):
    @functools.wraps(f):
    def wraped(*args, **kwargs):
        if not current_user.is_authenticated:
            disconnect()
        else:
            return f(*args, **kwargs)
        return wraped

@socketio.on('my event')
@authenticated_only
def handle_my_custom_event(data)
    emit('my response',
        {'message': '{0} has joined'.format(current_user.name)},
        broadcast=True
    )

14.部署

我们有多种部署Flask-SocketIO服务器的选择,从最简单到疯狂地复杂。在这一章节里,我们将会介绍最普遍的选择。

嵌入式服务器

最简单的策略是安装eventlet或者gevent,并且就像前面章节的例子中引用socketio.run(app)的方式来启动网络服务器。这个将会在eventlet或者gevent网络服务器中启动这个应用,被嵌入的网络服务器是哪一个取决于是安装的是哪一个。

注意到socketio.run(app)运行在eventlet或gevent已安装上的生产服务器中。如果它们中没有一个被安装,那么这个应用运行在Flask开发服务器中,这并不适于生产环境的使用。

不幸的是,这个选择并不能在带有uWSGI的gevent服务器上使用,你可以在下面获取更多有关这个选项的信息。

Gunicorn网络服务器

作为Socketio.run(app)替代方法的就是使用gunicorn作为网络服务器,工作在eventlet或gevent下。这个选择下,除了gunicorn要安装,eventlet或者gevent也是不可缺少的。这个条命令将会启动这个基于gunicorn的eventlet服务器:
gunnicorn --worker--class eventlet -w 1 module:app

如果你更倾向于使用gevent,启动服务器的命令如下:
gunicorn -k gevent -w 1 module:app

当使用gunicorn作为gevent的工作站并且websocket支持也被提供的时候,上述命令就必须被改成选择一个自定义的gevent网络服务器来支持websocket协议。修改后的命令如下:
gunicorn -k geventwebsocket.gunicorn.worker.GeventWebSocketWorker -w 1 module:app

在上述这些命令中,module是python模块或者是定义了应用实例的包,此外,app是应用实例本身。

Gunicorn 18.0版本是被推荐和Flask-SocketIO搭配的版本。19.x版本已知在带有WebSocket的一些特定部署场景下存在不兼容的情况。

gunicorn由于使用了有限的负载均衡算法,不可能在使用这种网络服务器时调用两个以上工作进程因为这个原因,上面的所有例子中都包含了-w 1的可选参数。

15.uWSGI网络服务器

当使用uWSGI网络服务器搭配geventd的时候,Socket.IO服务器的时候,可以利用uWSGI原生的WebSocket支持。

一个配置和运用uWSGI服务器完整的解释超出了本文的论述范围。uWSGI服务器确实是一个比较复杂的,它提供了大量而又详尽的设置选项。它必须使用Websocket和SSL编译才能支持WebSocket传输。作为介绍,下面的命令启动了一个uWSGI服务器作为范例,这个应用app.py运行在端口5000:
uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file app.py --callable app

16.使用nginx作为反向代理服务器

使用nginx作为前端的反向代理将请求传递给应用是可行的。然而,只有nginx 1.4版本以上才支持WebSocket协议。下面是nginx代理HTTP和WebSocket请求的一个最基本的配置:

 

    server {
        listen 80;
        server_name _;
        
        location / {
            include proxy_params;
            proxy_pass http://127.0.0.1:5000;
        }
        
        location /socket.io {
            include proxy_params;
            proxy_http_version 1.1;
            proxy_buffering off;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
            proxy_pass http://127.0.0.1:5000/socket.io;
        }
    }

下面的例子增加了对负载平衡多个服务器的支持:

 

    upstream socketio_nodes {
        ip_hash;
        
        server 127.0.0.1:5000;
        server 127.0.0.1:5001;
        server 127.0.0.1:5002;
        # to scale the app, just add more nodes here!
    }
    
    server {
        listen 80;
        server_name _;
        
        location / {
            include proxy_params;
            proxy_pass http://127.0.0.1:5000;
        }
        
        location /socket.io {
            include proxy_params;
            proxy_http_version 1.1;
            proxy_buffering off;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
            proxy_pass http://socketio_nodes/socket.io;
        }
    }

虽然上面的例子可以作为最初的配置工作,要知道生产环境安装的nginx需要一个完整的配置,包括部署的其它方面,例如服务于静态文件的assert和SSL支持。

17.使用多个工作站

Flask-SocketIO从2.0版本起带有负载均衡器支持多个工作站。部署多个工作站给了使用Flask-SocketIO的应用程序有能力在多进程和多主机之间传播客户端链接,这种方式的扩展支持极大规模的并发客户端。

使用多个Flask-SocketIO工作站需要两个依赖:

  • 负载均衡器必须要配置成总是将所有的HTTP请求从一个给定的客户端转发到同样的工作站中。这有时会作为"sticky session"被提及。对于nginx,使用这个ip_bash指示来达到上述要求。Gunicorn不能用于多工作站,因为它的负载均衡算法并不支持粘性会话(sticky session)。
  • 一旦每个服务器只拥有一个客户端连接,在Redis、RabbitMQ等例子中,消息队列将会被使用,来协调复杂的操作,比如:广播和房间。

当使用消息队列的时候,有许多额外的依赖包需要被安装:

  • 对于Redis,redis包必须被安装(pip install redis)
  • 对于RabbitMQ,kombu包必须要被安装(pip install kombu)
  • 对于其它Kombu支持的消息队列,Kombu documentation里可以找到需要的依赖
  • 如果使用了eventlet或者gevent,那么通常需要使用猴子(Monkey)修补Python标准库来强制消息队列包使用协同友好的函数和类。

为了启动多个Flask-SocketIO服务器,你必须首先确保消息队列服务正在运行。为了开启一个Socket.IO服务器,使他连接到一个消息队列,,需要添加参数message_queue到构造函数SockIO:
socketio=SocketIO(app,message_queue='redis://')
参数message_queue的值就是队列服务所使用的连接URL。对于一个运行在同一个作为服务器的主机中的Redis队列来说,可以使用'redis://'这样的URL。同样,对于一个默认的RabbitMQ队列可以使用'amqp://'开头的URL。Kombu包有一个文档章节阐述了对于所有支持队列的URL格式。

18.外部进程消息

对于许多类型的应用,从非服务端创建会话活动很有必要,例如一个Celery工作站。如果SocketIO服务器并没有按照前面章节那样配置监听队列,那么所有其它的进程可以像服务器那样创建它自己的SocketIO实例来创建消息活动。

例如,一个运行在eventlet网络服务器上的应用,使用了Redis消息队列,下面的Python脚本将向所有的客户端广播一个消息活动:

 

socketio=SocketIO(message_queue='redis://')
socketio.emit('my event', {'data': 'foo'}, namespace='/test')

当使用这种方法引用SocketIO实例,Flask应用实例将不会传递到构造函数

当SocketIO通过消息队列使用参数channel来选择一个具体channel的对话。当很多独立的SocketIO服务公用一个队列的时候,使用一个自定义的channel名称将是很有必要的。

Flask-SocketIO并没有在使用eventlet或者gevent时应用猴子(monkey)来修补。但是当使用消息队列的时候,如果Python标准库没有使用猴子来修补,那么消息队列服务的Python包很可能会挂起。

很重要的一点是:外部进程想连接到SocketIO服务器并不需要像主服务器那样使用eventlet或者gevent。使一个服务器使用了协同框架,外部进程不是一个阻力。例如,Celery工作站并不需要配置使用eventlet或者gevent,是因为主服务器已经有了。但是,如果你的外部进程因为某种原因
使用了协同框架,那么monkey修复就很可能是需要的,那么消息队列就可以获得协同友好的函数和类。

19.从Flask-SocketIO 0.x 升级到 1.x 和 2.x 版本

老版本的Flask-SocketIO有完全不同的一系列依赖包。老版本依赖gevent-socketio和gevent-websocket,这些包 1.0 版本都不需要了。

尽管依赖的改变,但是 1.0 版本却没有太多重要的改变。下面是一个实际改变的详细的清单:

  • 1.0 版本放弃支持Python 2.6,增加了对Python 3.3, Python 3.4 和 pypy 的支持。
  • 0.x 版本需要老版本的Socket.IO javascript客户端。从 1.0 版本开始,支持新发布的Socket.IO和Engin.IO。1.0版本以前的Socket.IO将不再被支持。Swift和C++官方的Socket.IO客户端也被支持。
  • 0.x 版本依赖gevent,gevent-socketio和gevent-websocket.1.0 版本以后将不再使用。在Flask开发的网络服务器中,gevent是三种后端网络服务器选择之一,另外两个是eventlet和其它常规多线程WSGI服务器。
  • Socket.IO服务器选项在 1.0 版本中也有所改变。它们可以由SocketIO构造函数来提供,或者由run()调用。这些选项在使用前在这两者中被合并。
  • 0.x 版本暴露了gevent-socketio在连接中作为request.namespace。在 1.0 版本中它不再被使用。这个请求对象定义了request.namespace作为待处理的命令空间。并且增加了request.aid,为客户端连接定义了一个独有的会话ID,request.event包含了活动名称和参数。
  • 为了获得房间列表,0.x版本需要应用使用私有gevent-socketio结构,包含request.namespace.rooms表达式。这是在 1.0 版本中将不再出现,因为它包含了一个合适的room()函数。
  • 这个推荐的“把戏(trick)”发送消息到一个独立的客户端将消息分发到每个客户端所在的独立的房间内,这个地址消息对应着目的房间(desired room)。这个特性在 1.0 版本中被正式化了,当客户端连接到服务器时,它会立即自动地被分配到一个特定的房间内。
  • 全局命名空间的connect活动在 1.0 版本之前并没有被触发。这bug已经被修复了并且按照预期触发。
  • 在 1.0 版本增加了对客户端的回调函数的支持。

为了升级到新的Flask-SocketIO版本,你需要升级你的Socket.IO客户端到兼容Socket.IO 1.0 协议。对于Javascript客户端,1.3.x和1.4.x版本经过充分地测试,发现是兼容的。

在服务端,有一些要点是要被考虑到的:

  • 如果你想继续使用gevent,那么gevent-socketio需要从你的虚拟环境中卸载,因为这个包将不再需要并且可能会与它的替代——python-socketio相冲突。
  • 如果你想轻微地提高性能和稳定性,那么推荐你转而使用eventlet。为了做到这一点,需要卸载gevent、gevent-socketio和gevent-websocket,然后安装eventlet。
  • 如果你的应用使用了猴子修复了并转向了eventlet,需要调用eventlet.monkey_patch()来代替gevent中的monkey.patch_all()。此外,任何对gevent的调用必须被同等条件下的对eventlet调用替代。
  • 任何使用request.namespace需要被直接调用Flask-SocketIO函数替代。例如,request.namespace.rooms要用rooms()函数替换。
  • 任何使用内置的gevent-socketio的对象都必须被去除,当这个包不再是所需的依赖的时候。



 

 类似资料: