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+。這個軟件包所依賴的異步服務可以從以下三種選擇中選擇:

  1. eventlet 是最好的高性能選項,支持長輪詢和 WebSocket 傳輸。
  2. gevent 支持多種不同的配置。 long-polling 傳輸完全由 gevent 包支持。與 eventlet 不同的是 gevent 沒有原生的 WebSocket 支持。為了添加對 WebSocket 的支持,目前有兩種選擇:
    • 安裝 gevent-websocket 軟件包
    • 使用隨 WebSocket 功能一起提供的uWSGI Web 服務器。 gevent 的使用也是一個性能選項,但比eventlet略低。
  3. 使用基於 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服務器。
Python服務端
<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'))

 

示例

Python服務端
<!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

 

 

 

 

                        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM