Flask-SocketIO
what's the Flask-SocketIO
Flask 是一个同步的轻量级框架,主要提供最基本的 api 接口功能,表现形式主要是服务端被动接收客户端的请求后做出响应,然后客户端根据响应结果做出业务操作。但是业务领域繁杂,会遇到需要服务端主动发送消息,客户端被动接收的情形,这个时候第一反应会想到的是使用 WebSocket 。若是出现服务端既要主动发送又要被动接收的情形,Flask-SocketIO 就是个不错的选择。
Flask-SocketIO 和 WebSocket 都是长连接,也就是说服务端和客户端握手成功后就可以持续通讯。WebSocket 是 HTML5 中实现了服务端和客户端进行双向文本或二进制数据通信的一种新协议,其实已经低于 HTTP 协议本身,和 HTTP 本质上没有什么关系。不过形式上两者略有相似。WebSocket 的连接地址基本格式:ws://localhost:8080。
WebSocket 在连接建立阶段通过 HTTP 的握手方式进行,这可以看做是为了兼容浏览器或者使用一些现成的功能来实现。当连接建立之后,客户端和服务端之间就不再进行 HTTP 通信,所有信息交互都由 WebSocket 接管。
从资源占用的角度上来说,其实 WebSocket 比 ajax 占用的资源更多,但它真正实现了全双工通信这一点还是很理想的,意味着无论是前端还是后端的信息交互程序编写都会变得更加方便。
Flask-SocketIO 使 Flask 应用程序可以让客户端和服务器之间实现低延迟双向通信。 客户端应用程序可以使用任何 SocketIO 官方客户端库,或任何兼容的客户端来建立与服务器的永久连接。
安装及依赖
pip install flask-socketio
Flask-SocketIO兼容Python 2.7和Python 3.3+。这个软件包所依赖的异步服务可以从以下三种选择中选择:
- eventlet 是最好的高性能选项,支持长轮询和 WebSocket 传输。
- gevent 支持多种不同的配置。 long-polling 传输完全由 gevent 包支持。与 eventlet 不同的是 gevent 没有原生的 WebSocket 支持。为了添加对 WebSocket 的支持,目前有两种选择:
- 安装 gevent-websocket 软件包
- 使用随 WebSocket 功能一起提供的uWSGI Web 服务器。 gevent 的使用也是一个性能选项,但比eventlet略低。
- 使用基于 Werkzeug 的 Flask 开发服务器,相比上述两项而言会降低一些性能,因此只能用于简化开发流程。该选项仅支持长轮询传输。
优先考虑 eventlet,接着是 gevent。对于 gevent 中的 WebSocket 支持,首选 uWSGI,然后是 gevent-websocket。如果既没有安装 eventlet 也没有安装 gevent,则使用Flask开发服务器。
注意:Flask-SocketIO 不会调用 before_request 和 after_request,也就是说如果 Flask-SocketIO 和 Flask 结合使用,那么 before_request 和 after_request 只能约束普通的 Flask 接口
简单的例子
from flask import Flask, render_template from flask_socketio import SocketIO app = Flask(__name__) app.config['SECRET_KEY'] = 'secret!' socketio = SocketIO(app) if __name__ == '__main__': socketio.run(app) # socketio.run()函数封装了Web服务器的启动,代替了app.run()标准的Flask开发服务器启动。 当应用程序处于调试模式时,Werkzeug开发服务器仍在socketio.run()中使用和正确配置。 在生产模式下首选使用eventlet Web服务器,否则使用gevent Web服务器。 如果没有安装eventlet和gevent,则使用Werkzeug开发Web服务器。
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.6/socket.io.min.js"></script> <script type="text/javascript" charset="utf-8"> var socket = io.connect('http://' + document.domain + ':' + location.port); socket.on('connect', function() { socket.emit('my event', {data: 'I\'m connected!'}); }); </script>
Flask-SocketIO 服务端
Flask-SocketIO 的功能实现形式和 Flask 相似,主要是路由装饰器和视图函数的结合
服务端接收信息
# 未命名事件 @socketio.on('message') def handle_message(message): print('received message: ' + message) # 自定义命名事件 @socketio.on('my_event') def handle_message(p1, p2): # 形参 print('received message: ', p1, p2) # 命名空间namespace,它允许客户端在同一个物理套接字上复用几个独立的连接 @socketio.on('my_event', namespace='/test') def handle_my_custom_namespace_event(p): print('received: ' + str(p)) # 返回值给客户端 def handle_message(p): # 形参 print(p) return 123 # 客户端将收到这个返回值 ######################################################### # on_event方法,效果等同于装饰器 def my_function_handler(data): pass socketio.on_event('my event', my_function_handler, namespace='/test')
服务端发送信息
SocketIO 事件处理程序可以使用 send() 和emit() 函数向连接的客户端发送回复消息。区别在于 send() 和 emit() 分别用于未命名事件和已命名事件。
from flask_socketio import send, emit @socketio.on('event1') def handle_event1(p): send('hello world') @socketio.on('event2') def handle_event2(p): emit('event2 response', 'hi world') # event2 response为该事件的命名 # namespace @socketio.on('event3') def handle_event3(): emit('event3 response', '333', namespace='/chat') # 多个值用元祖的形式 @socketio.on('event4') def handle_event4(): emit('event4 response', ('4', '44', '444'), namespace='/chat') # 回调函数 def ack(): print ('message was received!') @socketio.on('event5') def handle_event5(): emit('event5 response', '555', callback=ack) # 当使用回调函数时,客户端接收到一个回调函数来接收消息。 客户端应用程序调用回调函数后,调用相应的服务器端回调。 如果用参数调用客户端回调,则这些回调也作为参数提供给服务器端回调。
广播
SocketIO 的另一个非常有用的功能是消息的广播。 Flask-SocketIO 支持使用 broadcast = True 和 optional(可选参数)来 send() 和emit()
- 在启用广播选项的情况下发送消息时,连接到命名空间的所有客户端都会收到它,包括发件人。
- 当不使用名称空间时,连接到全局名称空间的客户端将收到该消息。
- 广播消息不会调用回调。
# 服务端被动广播 @socketio.on('broadcast_event') def broadcast_event1(data): emit('broadcast response', data, broadcast=True) # 服务端主动广播 def broadcast_event2(): socketio.emit('broadcast event', {'data': 'hello everyone'})
聊天室
对于许多应用程序来说,有必要将用户分成可以一起处理的子集。 最好的例子是有多个房间的聊天应用程序,用户从房间或房间接收消息,而不是从其他房间接收消息。 Flask-SocketIO 通过 join_room() 和 leave_room() 函数来支持这个房间的概念。
所有的客户端在连接时都被分配一个空间,用连接的会话ID命名,可以从 request.sid 中获得。 一个给定的客户可以加入任何房间,可以给任何名字。 当一个客户端断开连接时,它将从它所在的所有房间中移除。
由于所有的客户端都被分配了一个个人房间,所以为了向一个客户端发送消息,客户端的会话ID可以被用作房间参数。
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()函数接受一个可选的房间参数,使得消息被发送到给定房间中的所有客户端。(其实就是指定命名空间的客户端)
连接事件(授权)
连接事件处理程序可以选择返回 False 来拒绝连接。 这样就可以在这个时候验证客户端。
请注意,连接和断开连接事件在每个使用的名称空间上单独发送。
@socketio.on('connect', namespace='/chat') def test_connect(): emit('my response', {'data': 'Connected'}) @socketio.on('disconnect', namespace='/chat') def test_disconnect(): print('Client disconnected')
错误捕捉
错误处理函数将异常对象作为参数。
当前请求的消息和数据参数也可以使用 request.event 变量进行检查,这对于事件处理程序之外的错误日志记录和调试很有用
@socketio.on_error() # Handles the default namespace def error_handler(e): pass @socketio.on_error('/chat') # handles the '/chat' namespace def error_handler_chat(e): pass @socketio.on_error_default # handles all namespaces without an explicit error handler def default_error_handler(e): pass from flask import request @socketio.on("my error event") def on_my_event(data): raise RuntimeError() @socketio.on_error_default def default_error_handler(e): print(request.event["message"]) # "my error event" print(request.event["args"]) # (data,)
用类的形式实现
作为上述基于装饰器的事件处理程序的替代方法,属于名称空间的事件处理程序可以把类的方法名映射到命名空间。 flask_socketio.Namespace 作为基类提供,以创建基于类的命名空间。
当使用基于类的命名空间时,服务器收到的任何事件都会被分配到一个名为带有 on_ 前缀的事件名称的方法。 例如,事件 my_event 将由名为 on_my_event 的方法处理。 如果收到一个没有在命名空间类中定义的相应方法的事件,则该事件被忽略。 在基于类的命名空间中使用的所有事件名称必须使用方法名称中合法的字符。
为了方便在基于类的命名空间中定义的方法,命名空间实例包含了 flask_socketio.SocketIO 类中的几个方法的版本,当没有给出命名空间参数时,默认为适当的命名空间。
如果事件在基于类的名称空间中有一个处理程序,并且还有基于装饰器的函数处理程序,则只调用装饰的函数处理程序。
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'))
示例
<!DOCTYPE HTML> <html> <head> <title>Flask-SocketIO Test</title> <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script> <script type="text/javascript" charset="utf-8"> $(document).ready(function() { // Use a "/test" namespace. // An application can open a connection on multiple namespaces, and // Socket.IO will multiplex all those connections on a single // physical channel. If you don't care about multiple channels, you // can set the namespace to an empty string. namespace = '/test'; // Connect to the Socket.IO server. // The connection URL has the following format, relative to the current page: // http[s]://<domain>:<port>[/<namespace>] var socket = io(namespace); // Event handler for new connections. // The callback function is invoked when a connection with the // server is established. socket.on('connect', function() { socket.emit('my_event', {data: 'I\'m connected!'}); }); // Event handler for server sent data. // The callback function is invoked whenever the server emits data // to the client. The data is then displayed in the "Received" // section of the page. socket.on('my_response', function(msg, cb) { $('#log').append('<br>' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html()); if (cb) cb(); }); // Interval function that tests message latency by sending a "ping" // message. The server then responds with a "pong" message and the // round trip time is measured. var ping_pong_times = []; var start_time; window.setInterval(function() { start_time = (new Date).getTime(); socket.emit('my_ping'); }, 10000); // Handler for the "pong" message. When the pong is received, the // time from the ping is stored, and the average of the last 30 // samples is average and displayed. socket.on('my_pong', function() { var latency = (new Date).getTime() - start_time; ping_pong_times.push(latency); ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples var sum = 0; for (var i = 0; i < ping_pong_times.length; i++) sum += ping_pong_times[i]; $('#ping-pong').text(Math.round(10 * sum / ping_pong_times.length) / 10); }); // Handlers for the different forms in the page. // These accept data from the user and send it to the server in a // variety of ways $('form#emit').submit(function(event) { socket.emit('my_event', {data: $('#emit_data').val()}); return false; }); $('form#broadcast').submit(function(event) { socket.emit('my_broadcast_event', {data: $('#broadcast_data').val()}); return false; }); $('form#join').submit(function(event) { socket.emit('join', {room: $('#join_room').val()}); return false; }); $('form#leave').submit(function(event) { socket.emit('leave', {room: $('#leave_room').val()}); return false; }); $('form#send_room').submit(function(event) { socket.emit('my_room_event', {room: $('#room_name').val(), data: $('#room_data').val()}); return false; }); $('form#close').submit(function(event) { socket.emit('close_room', {room: $('#close_room').val()}); return false; }); $('form#disconnect').submit(function(event) { socket.emit('disconnect_request'); return false; }); }); </script> </head> <body> <h1>Flask-SocketIO Test</h1> <p>Async mode is: <b>{{ async_mode }}</b></p> <p>Average ping/pong latency: <b><span id="ping-pong"></span>ms</b></p> <h2>Send:</h2> <form id="emit" method="POST" action='#'> <input type="text" name="emit_data" id="emit_data" placeholder="Message"> <input type="submit" value="Echo"> </form> <form id="broadcast" method="POST" action='#'> <input type="text" name="broadcast_data" id="broadcast_data" placeholder="Message"> <input type="submit" value="Broadcast"> </form> <form id="join" method="POST" action='#'> <input type="text" name="join_room" id="join_room" placeholder="Room Name"> <input type="submit" value="Join Room"> </form> <form id="leave" method="POST" action='#'> <input type="text" name="leave_room" id="leave_room" placeholder="Room Name"> <input type="submit" value="Leave Room"> </form> <form id="send_room" method="POST" action='#'> <input type="text" name="room_name" id="room_name" placeholder="Room Name"> <input type="text" name="room_data" id="room_data" placeholder="Message"> <input type="submit" value="Send to Room"> </form> <form id="close" method="POST" action="#"> <input type="text" name="close_room" id="close_room" placeholder="Room Name"> <input type="submit" value="Close Room"> </form> <form id="disconnect" method="POST" action="#"> <input type="submit" value="Disconnect"> </form> <h2>Receive:</h2> <div id="log"></div> </body> </html>
参考:
https://blog.csdn.net/qq_37193537/article/details/90901171
https://www.cnblogs.com/minsons/p/8251780.html