【Flask】使⽤Websocket协议(Flask的socketio)进⾏服务端和客户端。。。
有个坑我觉得有必要填⼀下,那就是使⽤Flask作为服务端,使⽤while循环往客户端发送Websocket数据时,客户端接受不到消息的问题。
⽬录
Websocket协议
⾸先介绍⼀下WebSocket 协议,WebSocket 的最⼤特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。
WebSocket 是⼀个新协议,它和HTTP协议都属于应⽤层协议,它跟HTTP协议基本上没什么关系,但是为了兼容⽬前的主流浏览器,在握⼿阶段使⽤了HHTP。
HTTP 的⽣命周期通过 Request 来界定,也就是⼀个 Request ⼀个 Response ,那么在 HTTP1.0 中,这次 HTTP 请求就结束了。具体区别见下图:
图源:
在 HTTP1.1 中进⾏了改进,使得有⼀个 keep-alive,也就是说,在⼀个 HTTP 连接中,可以发送多个 Request,接收多个 Response。但是请记住 Request = Response, 在 HTTP 中永远是这样,也就是说⼀个 Request 只能有⼀个 Response。⽽且这个 Response 也是被动的,不能主动发起。
可以看到这两种⽅式都体现出了HTTP协议的被动性,也就是说服务端不能主动联系客户端,只能有客户端发起。⽽Websocket就解决了这个问题,当服务器完成协议升级后(HTTP->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__)
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()⽤于推送命名事件。实例代码:
@('connect',namespace='/test_conn')
def handle_message(message):
send(data=message, namespace='/test_conn')
@('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__)
socketio = SocketIO(app)
@('connect', namespace='/test_conn')
def test_connect():
{'data': ‘connect’},namespace='/test_conn')
if __name__ =='__main__':
socketio.run(app, debug=True,host='127.0.0.1',port=11000)
@(‘connect’, namespace=’/test_conn’)中的connect是socketio的内置事件。当客户端和服务端连接后,前端和后端都会收到⼀个名为connect的事件,服务端接受到这个事件就会执⾏test_connect()函数中的内容,然后服务端使⽤emit()函数向前端推送消息。
然后我们来看namesapce,namespace可以标志多个事件。官⽅⽂档的解释是:“当⼀个客户端连接服务器的不同命名域的时候,可以在同⼀个socket连接⾥完成”。结合⽹上的观点以及我个⼈的理解,⼀个namespace定义了⼀个后端的websocket连接接⼝,客户端和服务器通过三次握⼿建⽴socket连接后,连接不同的服务器接⼝,socket连接并不会断开。⽽⼀个后端接⼝可以接受多个客户端的socket连接,如果在后端的emit中定义‘broadcast=True’,那么所有连接到这个命名域的客户端都会收到这个消息。不同命名域之间可以通过发送消息指定命名域的⽅式来相互通信。
再看it,第⼀个参数’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__)
socketio = SocketIO(app)
@ute('/')
def index():
return render_template('index.html')
@('connect', namespace='/test_conn')
def test_connect():
#核⼼代码在while循环这
while True:
socketio.sleep(5)
t = random.randint(1,100)
{'data': t},namespace='/test_conn')
if __name__ =='__main__':
socketio.run(app, debug=True,host='127.0.0.1',port=11000)
这时候坑来了,事实证明这样是⾏不通的。服务端使⽤while来实现循环执⾏emit语句,所以理论上客户端应该可以定时收到服务端的随机数。但是结果是客户端根本接收不到,连js端的函数都没有触发运⾏。
原因应该是当服务端陷⼊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__)
socketio = SocketIO(app)
thread =None
thread_lock = Lock()
@ute('/')
def index():
return render_template('index.html')
@('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)
{'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);
<('server_response',function(res){
var msg = res.data;
console.log(msg);
});
});
}
注意在客户端需要引⼊socket.io.js⽂件,然后就可以使⽤io.connect建⽴指定namespace的socket连接了,使⽤监听并捕捉服务端发来的消息,并操作前端界⾯进⾏相应改变。
运⾏结果
将以上⼏个⽂件按照Flask所要求的⽂件夹路径放好,运⾏app.py,我们可以在浏览器的console中看到输出,如下图所⽰:
本⽂参考:
前端websocket怎么用