在Flask中实现websocket

别来无恙 提交于 2020-02-11 12:18:39

前言

前段时间在做自有性能测试平台时,需要加入一个前端实时获取后端数据的功能。整个的项目前后端是分开的,后端项目用python的Flask框架实现,前端项目用Vue框架实现。因为我做后端,所以最开始想到的对我来说最简单的方法就是提供一个读取数据的接口,前端通过Ajax轮询的方式实时获取数据。但是轮询对前端来讲似乎工作量和复杂度高,而且考虑到网络资源的消耗,轮询显然不是一个最优的解决办法,于是想到通过websocket来解决。

知识点

websocket

对websocket的第一理解就是用于前后端实时通信,再深入研究发现其实它是一种协议。

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
AJAX轮询与websocket
----摘自菜鸟教程

flask-socketio

后端选用了flask-socketio库来实现websocket功能。

初始化

from flask import Flask, render_template
from flask_socketio import SocketIO

app = Flask(__name__)
app.config.from_object("config.{}".format(os.getenv('FLASK_CONFIG') or "dev"))
socketio = SocketIO(app, cors_allowed_origins='*', async_mode='eventlet', engineio_logger=True)

if __name__ == '__main__':
    socketio.run(app)

接收消息

使用flask-SocketIO,服务端需要为这些事件注册处理程序,这与视图函数如何处理路由类似。

命名事件

以下示例为未命名事件创建服务器端事件处理程序:

@socketio.on('message')
def handle_message(message):
    print('received message: ' + message)

上面的示例使用字符串消息。另一种类型的未命名事件使用JSON数据:

@socketio.on('json')
def handle_json(json):
    print('received json: ' + str(json))

最灵活的事件类型使用自定义事件名称。这些事件的消息数据可以是字符串,字节,int或JSON:

@socketio.on('my event')
def handle_my_custom_event(json):
    print('received json: ' + str(json))

自定义命名事件还可以支持多个参数:

@socketio.on('my event')
def handle_my_custom_event(arg1, arg2, arg3):
    print('received args: ' + arg1 + arg2 + arg3)

命名事件是最灵活的,因为它们消除了包含其他元数据来描述消息类型的需求。名称message, json,connect以及disconnect被保留,不能用于命名事件

命名空间

flask-SocketIO除了支持命名时间,还支持SocketIO命名空间,该名称空间允许客户端在同一物理套接字上多路复用多个独立的连接:

@socketio.on('my event', namespace='/test')
def handle_my_custom_namespace_event(json):
    print('received json: ' + str(json))

对于使用装饰器语法不方便的情况下,可以使用socketio.on_event()方法,通过指定事件名称、对应的处理函数和命名空间来使用on_event方法:

def my_function_handler(data):
    pass

socketio.on_event('my event', my_function_handler, namespace='/test')

发送消息

flask-SocketIO使用send()和emit() 函数将答复消息发送到连接的客户端,以下示例将收到的事件退回给发送事件的客户端:

from flask_socketio import send, emit

@socketio.on('message')
def handle_message(message):
    send(message)

@socketio.on('json')
def handle_json(json):
    send(json, json=True)

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json)

当定义了命名空间时,send()并emit()可以使用可选namespace参数指定其命名空间。

@socketio.on('message')
def handle_message(message):
    send(message, namespace='/chat')

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json, namespace='/chat')

SocketIO支持确认回调,以确认客户端已接收到消息:

def ack():
    print 'message was received!'

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json, callback=ack)

连接和断开

flask-SocketIO管理和定义了连接、断开事件。

@socketio.on('connect')
def test_connect():
    emit('my response', {'data': 'Connected'})

@socketio.on('disconnect')
def test_disconnect():
    print('Client disconnected')

连接事件处理程序可以返回False以拒绝连接,也可以引发ConectionRefusedError。这样一来,客户端就可以在此时进行身份验证了。使用异常时,传递给异常的所有参数都将在错误包中返回给客户端。请注意,连接和断开连接事件在使用的每个名称空间上单独发送。

from flask_socketio import ConnectionRefusedError

@socketio.on('connect')
def connect():
    if not self.authenticate(request.args):
        raise ConnectionRefusedError('unauthorized!')

基于类的命名空间

除了使用基于装饰器的时间处理函数外,还可以使用基于类的命名空间,将属于命名空间的所有事件处理程序封装在一个类中。flask_socketio.Namespace是创建这些类的基类。具体使用方法如下:

from flask_socketio import Namespace, emit

class MyCustomNamespace(Namespace):
    def on_connect(self):
        pass

    def on_disconnect(self):
        pass

    def on_my_event(self, data):
        emit('my_response', data)

socketio.on_namespace(MyCustomNamespace('/test'))

本项目中使用的就是这种方式。
当使用基于类的命名空间时,服务端接收到的所有事件都将分派给一个对应的时间处理方法,该方法带有on_前缀。例如,事件my_event对应的时间处理方法为on_my_event()。如果服务端接收到事件,但在命名空间类中没有定义相应的方法,则该事件被忽略。在基于类的命名空间中使用的所有事件名称都必须是合法的方法名称。
如果事件在基于类的命名空间中有对应的处理函数,而且在基于装饰器的函数处理程序中也有该事件的处理函数,则基于装饰器的处理函数会生效。

广播&房间

这两个知识点在本次的项目中没有用到,暂时不做记录。

认证

后期项目中可能会用到,留坑后续研究

部署

项目本身在生产环境中是使用gunicorn进行部署的,之前的gunicorn worker类型为gthread,但加入websocket后,需要将worker类型调整为eventlet或者gevent,因为只有这两种类型支持websocket。
项目中使用的配置文件具体如下,使用了1个worker

# 启动的进程数
workers = 1
threads = multiprocessing.cpu_count() * 2 + 1
worker_class = "eventlet"
worker_connections = 100

实现

项目中的websocket实现代码,使用了基于类的命名空间,在SocketData类中定义了connect、send_message、disconnect三个时间对应的处理函数。数据发送功能由Worker类完成。
下面是工程中与websocket相关的一些代码,罗列了实现websocket基本功能的代码。

socketio = SocketIO(app, cors_allowed_origins='*', async_mode='eventlet', engineio_logger=True)

class Worker(object):
    switch = False
    unit_of_work = 0

    def __init__(self, socketio):
        """
        assign socketio object to emit
        """
        self.socketio = socketio
        self.switch = True

    def do_work(self, task_id):
        """
        do work and emit message
        """

        while self.switch:
            # must call emit from the socket io
            # must specify the namespace
            data = LogData.get_log_data(task_id)
            if "end of run" in data:
                app.logger.info(f"last_time:send_message:{data}\n")
                self.socketio.emit('send_message', {'data': {'msg': data, 'flag': False}}, namespace='/lptslog')
                # self.switch = False
                # disconnect()
            else:
                app.logger.info(f"send_message:{data}")
                self.socketio.emit('send_message', {'data': {'msg': data, 'flag': True}}, namespace='/lptslog')

            db.session.commit()
            db.session.close()
            # important to use eventlet's sleep method
            eventlet.sleep(5)

    def stop(self):
        """
        stop the loop
        """
        self.switch = False


class SocketData(Namespace):

    def on_connect(self):
        app.logger.info(f"connect...\nsession id={request.sid}")
        global worker
        worker = Worker(socketio)

    def on_send_message(self, data):
        app.logger.info(f"data: {data}")
        task_id = data.get('taskId')
        if task_id != '':
            socketio.start_background_task(worker.do_work, task_id)

    def on_disconnect(self):
        app.logger.info(f"positive disconnect...\nsession id={request.sid}")
        worker.stop()


socketio.on_namespace(SocketData('/lptslog'))
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!