WebSocket
简介:WebSocket是一种全新的协议,随着HTML5的不断完善,越来越多的现代浏览器开始全面支持WebSocket技术了,它将TCP的Socket(套接字)应用在了webpage上,从而使通信双方建立起一个保持在活动连接通道。
运行流程:浏览器通过Javascript向服务器发起WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务器就可以通过TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet(长连接)技术小很多。
原理:WebSocket协议是借用HTTP协议的1.01 switch protocol(服务器根据客户端的指定,将协议转换成为Upgrade首部所列的协议)来达到协议转换的,从HTTP协议切换成WebSocket通信协议。
具体连接方式:通过在请求头中添加upgrade:websocket及通信秘钥(Sec-WebSocket-Key),使双方握手成功,建立全双工通信。
WebSocket客户端连接报文:
WebSocket服务端响应报文:
1. Flask-SocketIO
要对应安装不然会出现版本问题
pipinstallgevent
pipinstalleventlet
pipinstallFlask-SocketIO==4.3.1
pipinstallpython-engineio==3.13.2
pipinstallpython-socketio==4.6.0
1.1 初始化
fromflaskimportFlask, render_template
fromflask_socketioimportSocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
if__name__ == '__main__':
socketio.run(app)
1.2 客户端向服务器发送数据
客户端和服务端使用SocketIO时,消息都被当作事件进行接收。在客户端,Javascript 通过回调函数处理事件;在Flask-SocketIO服务端,每个事件都有对应的事件函数,类似原生Flask中,路由都有对应的视图函数。
前端代码:
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Test</title>
<scripttype="text/javascript"src="{{ url_for('static', filename = 'js/jquery.min.js') }}"></script>
<scripttype="text/javascript"src="{{ url_for('static', filename = 'js/socket.io.min.js') }}"></script>
</head>
<body>
<h1>Hello World!</h1>
<pid="t"></p>
<script>
varname_space='/dcenter'// 这里的命名空间必须与后端代码的命名空间一样,要不然无法建立连接
varsocket=io.connect(location.protocol+'//'+document.domain+':'+location.port+name_space) //初始化建立连接
{# 向服务器发送信息 #}
socket.on('connect',function () {
// 这里的事件命名必须与后端绑定的事件命名一样,不然接收不到消息
socket.emit('my event', {data: 'I\'m connected!'})
})
</script>
</body>
</html>
后端代码:
fromflaskimportFlask, render_template
fromflask_socketioimportSocketIO, emit
fromthreadingimportThread
importtime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
#socketio = SocketIO()
socketio = SocketIO(logger=True, engineio_logger=True) # 将日志输出到终端
socketio.init_app(app, cors_allowed_origins='*') # 解决跨域问题
name_space = '/dcenter' # 命名空间必须与前端一致
# 初始化连接函数
@socketio.on('connect', namespace=name_space)
defconnected_msg():
print('client connected.')
# 初始化断开函数
@socketio.on('disconnect', namespace=name_space)
defdisconnect_msg():
print('client disconnected.')
# 路由路径
@app.route('/')
defindex():
returnrender_template('index.html')
# 接收客户端消息(事件命名必须与前端一致)
@socketio.on('my event') # 匿名事件
defhandle_my_custom_event(json):
print('received json: '+str(json))
# 接收客户端消息(事件命名必须与前端一致)
@socketio.on('my event', namespace=name_space) # 绑定命名空间
defhandle_my_custom_event(json):
print('received json: '+str(json))
if__name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
服务器响应数据:
1.3 服务器实时接收数据
前端代码:
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Test</title>
<scripttype="text/javascript"src="{{ url_for('static', filename = 'js/jquery.min.js') }}"></script>
<scripttype="text/javascript"src="{{ url_for('static', filename = 'js/socket.io.min.js') }}"></script>
{# <scripttype="text/javascript"src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.6/socket.io.min.js"></script>#}
</head>
<body>
<h1>Hello World!</h1>
<pid="t"></p>
<ahref="javascript:a()">发送</a>
<script>
// 向服务器发送信息,如果要服务器实时接收数据的话,需建立连接与发送数据要同步进行(例如封装在一个函数里)
functiona(){
varsocket=io.connect('http://'+document.domain+':'+location.port);
socket.on('connect', function() {
socket.emit('message', 'I\'m connected!');
{#socket.emit('message', {data: 'I\'m connected!'});#}
});
console.log('kkk')
}
</script>
</body>
</html>
后端代码:
fromflaskimportFlask, render_template
fromflask_socketioimportSocketIO, emit
fromthreadingimportThread
importmultiprocessing
importtime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
# socketio = SocketIO()
socketio = SocketIO(logger=True, engineio_logger=True) # 将日志输出到终端
socketio.init_app(app, cors_allowed_origins='*') # 解决跨域问题
name_space = '/dcenter'
# 初始化断开函数
@socketio.on('connect', namespace=name_space)
defconnected_msg():
print('client connected.')
# 初始化断开连接函数
@socketio.on('disconnect', namespace=name_space)
defdisconnect_msg():
print('client disconnected.')
@app.route('/')
defindex():
returnrender_template('index.html')
# 匿名函数
@socketio.on('message')
defhandle_message(message):
print('received message: '+message)
if__name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
先访问,点击发送:
服务器实时接收结果:
1.4 服务器向客户端发送数据
在启用广播选项的情况下发送消息时,连接到命名空间的所有客户端都会收到它,包括发送者。当不使用命名空间时,连接到全局命名空间的客户端会收到消息。请注意,不会为广播消息调用回调。
设置参数broadcast=True就会启动广播功能。
前端代码:
<!DOCTYPE html>
<htmllang="en">
<head>
<metacharset="UTF-8">
<title>Test</title>
<scripttype="text/javascript"src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
<scripttype="text/javascript"src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
</head>
<body>
<h1>Hello World!</h1>
<pid="t"></p>
<script>
varname_space='/dcenter'// 这里的命名空间必须与后端代码的命名空间一样,要不然无法建立连接
varsocket=io.connect(location.protocol+'//'+document.domain+':'+location.port+name_space)
// 这里的事件命名必须与后端绑定的事件命名一样,不然接收不到消息
socket.on('dcenter', function (res) {
console.log(res)
lett=res.data
if (t) {
$("#t").append(t).append('<br/>');
}
})
</script>
</body>
</html>
后端代码:
fromflaskimportFlask, render_template
fromflask_socketioimportSocketIO, emit
fromthreadingimportThread
importtime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
#socketio = SocketIO()
socketio = SocketIO(logger=True, engineio_logger=True) # 将日志输出到终端
socketio.init_app(app, cors_allowed_origins='*') # 解决跨域问题
name_space = '/dcenter' # 命名空间
# 初始化连接函数
@socketio.on('connect', namespace=name_space)
defconnected_msg():
print('client connected.')
# 回调函数
defcb():
print('vvv==bbb')
# 初始化断开连接函数
@socketio.on('disconnect', namespace=name_space)
defdisconnect_msg():
print('client disconnected.')
# 测试函数向客户端发送数据
deftest():
event_name = 'dcenter'
foriinrange(11):
broadcasted_data = {'data': f"test message!,{i}"}
# 消息广播
socketio.emit(event_name, broadcasted_data, broadcast=False, namespace=name_space, callback=cb())
time.sleep(1)
@app.route('/')
defindex():
# 开启多线程进行消息发送
td = Thread(target=test)
td.start()
returnrender_template('index.html')
if__name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
访问http://127.0.0.1:5000/后,服务器会在11秒内依次向客户端发送数据
1.5 总结
1、命名空间和事件命名必须前后端保持一致。
2、服务器的信息发送与广播只支持线程广播但不支持进程广播。
2. 房间
实际应用场景中,可能需要给用户分组。比如,聊天室,不同用户只能收到他们所在房间的消息。通过join_room() 和 leave_room() 可以实现上述功能:
from flask_socketio import join_room, leave_room
@socketio.on('join')
def on_join(data):
username = data['username']
room = data['room']
join_room(room)
send(username + ' has entered the room.', room=room)
@socketio.on('leave')
def on_leave(data):
username = data['username']
room = data['room']
leave_room(room)
send(username + ' has left the room.', room=room)
send() 和emit() 函数接受room 参数。
所有客户端连接时,会被分配一个房间。默认房间名称为连接的session ID,Flask中通过request.sid获取该ID。客户端能加入所有存在的房间。客户端断开时,所有它加入的房间都会移除它。上下文外的socketio.send() 和 socketio.emit()也可以接收room参数,来给房间中所有客户端广播。
因为所有客户端在加入时,都被指定了一个私人的房间,所以,如果想要发送消息给指定客户端,也可以通过指定消息的room参数为该客户端session ID来实现。
参考文献:https://zhuanlan.zhihu.com/p/376370796
https://blog.csdn.net/weixin_46020624/article/details/123619230?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167513282616800225584650%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167513282616800225584650&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-123619230-null-null.142^v71^wechat,201^v4^add_ask&utm_term=Flask-SocketIO&spm=1018.2226.3001.4187