【Flask】使用Websocket协议(Flask的socketio)进行服务端和客户端通信,动态更新前端内容

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_41427568/article/details/102670035

有个坑我觉得有必要填一下,那就是使用Flask作为服务端,使用while循环往客户端发送Websocket数据时,客户端接受不到消息的问题。

Websocket协议

首先介绍一下WebSocket 协议,WebSocket 的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。
WebSocket 是一个新协议,它和HTTP协议都属于应用层协议,它跟HTTP协议基本上没什么关系,但是为了兼容目前的主流浏览器,在握手阶段使用了HHTP。
HTTP 的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,那么在 HTTP1.0 中,这次 HTTP 请求就结束了。具体区别见下图:
websocket和http协议的区别
图源:什么是WebSocket (经常听别人讲感觉很高大上其实不然)
在 HTTP1.1 中进行了改进,使得有一个 keep-alive,也就是说,在一个 HTTP 连接中,可以发送多个 Request,接收多个 Response。但是请记住 Request = Response, 在 HTTP 中永远是这样,也就是说一个 Request 只能有一个 Response。而且这个 Response 也是被动的,不能主动发起。
可以看到这两种方式都体现出了HTTP协议的被动性,也就是说服务端不能主动联系客户端,只能有客户端发起。而Websocket就解决了这个问题,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端了。
具体其他方面的介绍可以看这篇博文:
什么是WebSocket (经常听别人讲感觉很高大上其实不然)
我觉得写的很有意思,其中对Ajax还有long poll也有一定介绍。

使用Flask和JS实现WebSocket协议通信

安装

除了要安装正常的Flask组件之外,还应该安装flask_socketio模块,这个模块实现了Flask对websocket的封装,从而允许建立在flask上的应用的服务端和客户端建立全双工通信。以下为安装flask-socketio的pip指令。

pip install flask-socketio

启动flask的socketio

socketio是在正常flask提供的服务上的进一步封装。

from flask import Flask, render_template
from flask_socketio import SocketIO,emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

if __name__ == '__main__':
    socketio.run(app, debug=True,host='127.0.0.1',port=11000)

使用socketio.run()代替原来的app.run()启动flask服务端。

服务端向客户端推送消息的函数send和emit

当需要推送消息时,使用socketio的两个函数send()和emit()都可以实现消息推送,send()用于推送无名事件,emit()用于推送命名事件。
实例代码:

@socketio.on('connect',namespace='/test_conn')
def handle_message(message):
     send(data=message, namespace='/test_conn')
 
@socketio.on('connect',namespace='/test_conn')
def handle_my_custom_event(json):
      emit('my response', data=json, namespace='/test_conn')

可以看到,使用emit()推送了命名的事件消息“my response”,这个事件的名称需要在客户端的js代码中指明。而send()推送了无名事件。他们二者都要指定一个namespace,namespace是什么我们放在后面讲。

服务端向客户端推送单条消息

从上文我们可知,socketio的两个函数send()和emit()都可以实现消息发送,前者用于无名事件,后者用于命名的事件。
事件是消息的名称,这个名称规定了这条消息送往客户端或者服务端的某个函数,也就是说需要在客户端或者服务端建立一个接受这个事件的处理函数。
以下是一个简单的例子,这个例子是在服务端向客户端推送单条消息,服务端的代码:

from flask import Flask, render_template
from flask_socketio import SocketIO,emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

@socketio.on('connect', namespace='/test_conn')
def test_connect():
        socketio.emit('server_response',
                      {'data': ‘connect’},namespace='/test_conn')

if __name__ == '__main__':
    socketio.run(app, debug=True,host='127.0.0.1',port=11000)

@socketio.on(‘connect’, namespace=’/test_conn’)中的connect是socketio的内置事件。当客户端和服务端连接后,前端和后端都会收到一个名为connect的事件,服务端接受到这个事件就会执行test_connect()函数中的内容,然后服务端使用emit()函数向前端推送消息。
然后我们来看namesapce,namespace可以标志多个事件。官方文档的解释是:“当一个客户端连接服务器的不同命名域的时候,可以在同一个socket连接里完成”。结合网上的观点以及我个人的理解,一个namespace定义了一个后端的websocket连接接口,客户端和服务器通过三次握手建立socket连接后,连接不同的服务器接口,socket连接并不会断开。而一个后端接口可以接受多个客户端的socket连接,如果在后端的emit中定义‘broadcast=True’,那么所有连接到这个命名域的客户端都会收到这个消息。不同命名域之间可以通过发送消息指定命名域的方式来相互通信。
再看soketio.emit,第一个参数’server_response’是服务端发送这个消息的事件名,在客户端要建立一个接受这个事件的函数处理(客户端代码在后面,我们使用js作为客户端),后面的字典就是消息内容,namespace=’/test_conn’表示这个消息发送到信道(test_conn)中。

服务端向客户端循环推送消息

当需要服务端循环或者定时推送消息时,网上大部分的服务端代码是使用while循环实现的:

from flask import Flask, render_template
from flask_socketio import SocketIO,emit
import random
async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('connect', namespace='/test_conn')
def test_connect():
	#核心代码在while循环这
    while True:
        socketio.sleep(5)
        t = random.randint(1, 100)
        socketio.emit('server_response',
                      {'data': t},namespace='/test_conn')

if __name__ == '__main__':
    socketio.run(app, debug=True,host='127.0.0.1',port=11000)

这时候坑来了,事实证明这样是行不通的。服务端使用while来实现循环执行emit语句,所以理论上客户端应该可以定时收到服务端的随机数。但是结果是客户端根本接收不到,连js端的soket.on函数都没有触发运行。
原因应该是当服务端陷入while死循环,会影响与客户端之间的websocket连接,总之写while true需谨慎。
在例程中,我们可以使用后台线程进行while循环,来执行emit函数,从而解决这个问题。

from flask import Flask, render_template
from flask_socketio import SocketIO,emit
from threading import Lock
import random
async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
thread = None
thread_lock = Lock()

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('connect', namespace='/test_conn')
def test_connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(target=background_thread)

def background_thread():
    while True:
        socketio.sleep(5)
        t = random.randint(1, 100)
        socketio.emit('server_response',
                      {'data': t},namespace='/test_conn')

if __name__ == '__main__':
    socketio.run(app, debug=True,host='127.0.0.1',port=11000)

前端和客户端

前端使用html,客户端使用js接受flask服务端推送来的消息,并动态更新html元素内容。

前端

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="../static/js/jquery.min.js"></script>
    <script type="text/javascript" src="../static/js/socket.io.js"></script>
</head>

<body onload="onload();">
<div id="random">
<p >1</p>
</div>

<script type="text/javascript" src="../static/js/flush.js"></script>
</body>

</html>

body标签内的内容一旦加载完,就去执行flush.js的onload()函数。

客户端

flush.js

function onload() {
    $(document).ready(function() {
        namespace='/test_conn'
        var socket = io.connect('ws://127.0.0.1:11000/test_conn');
        //或者使用 var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
        
        socket.on('server_response', function(res) {
            var msg = res.data;
            console.log(msg);
            document.getElementById("random").innerHTML = '<p>'+msg+'</p>';
        }); 
   	});
}

注意在客户端需要引入socket.io.js文件,然后就可以使用io.connect建立指定namespace的socket连接了,使用socket.on监听并捕捉服务端发来的消息,并操作前端界面进行相应改变。

运行结果

将以上几个文件按照Flask所要求的文件夹路径放好,运行app.py,我们可以在浏览器的console中看到输出,如下图所示:
在这里插入图片描述
本文参考:使用flask_socketio实现服务端向客户端定时推送

猜你喜欢

转载自blog.csdn.net/qq_41427568/article/details/102670035