【websocket】小白快速上手flask-socketio

2023-07-11 21:00:27

大家好,我是一個初級的Python開發工程師。本文是結合官方教學和程式碼案例,簡單說下我對flask-socketio的使用理解。

 

一、websocket簡介

websocket 說白一點就是,建立使用者端和伺服器端雙向通訊通道, 伺服器可以主動向使用者端發訊息。

 

二、flask-socketio理解與使用

1. 環境準備:Python3.7

pip install eventlet==0.33.3
pip install flask-socketio==5.8.0
pip install flask==1.1.4

 

2. 程式碼來自官方教學

下面的程式碼親測可用,請放心食用。

(1)專案結構

(2)app.py程式碼

from threading import Lock
from flask import Flask, render_template, session, request, copy_current_request_context
from flask_socketio import SocketIO, emit, join_room, leave_room, close_room, rooms, disconnect

# Set this variable to "threading", "eventlet" or "gevent" to test the
# different async modes, or leave it set to None for the application to choose
# the best option based on installed packages.
async_mode = None

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, async_mode=async_mode)
thread = None
thread_lock = Lock()


def background_thread():
    """Example of how to send server generated events to clients."""
    count = 0
    while True:
        socketio.sleep(10)
        count += 1
        socketio.emit('my_response',
                      {'data': 'Server generated event', 'count': count})


@app.route('/')
def index():
    return render_template('index.html', async_mode=socketio.async_mode)


@socketio.event
def my_event(message):
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response',
         {'data': message['data'], 'count': session['receive_count']})


@socketio.event
def my_broadcast_event(message):
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response',
         {'data': message['data'], 'count': session['receive_count']},
         broadcast=True)


@socketio.event
def join(message):
    join_room(message['room'])
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response',
         {'data': 'In rooms: ' + ', '.join(rooms()),
          'count': session['receive_count']})


@socketio.event
def leave(message):
    leave_room(message['room'])
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response',
         {'data': 'In rooms: ' + ', '.join(rooms()),
          'count': session['receive_count']})


@socketio.on('close_room')
def on_close_room(message):
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response', {'data': 'Room ' + message['room'] + ' is closing.',
                         'count': session['receive_count']},
         to=message['room'])
    close_room(message['room'])


@socketio.event
def my_room_event(message):
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response',
         {'data': message['data'], 'count': session['receive_count']},
         to=message['room'])


@socketio.event
def disconnect_request():
    @copy_current_request_context
    def can_disconnect():
        disconnect()
    session['receive_count'] = session.get('receive_count', 0) + 1
    # for this emit we use a callback function
    # when the callback function is invoked we know that the message has been
    # received and it is safe to disconnect
    emit('my_response',
         {'data': 'Disconnected!', 'count': session['receive_count']},
         callback=can_disconnect)


@socketio.event
def my_ping():
    emit('my_pong')


@socketio.event
def connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(background_thread)
    emit('my_response', {'data': 'Connected', 'count': 0})


@socketio.on('disconnect')
def test_disconnect():
    print('Client disconnected', request.sid)


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

(3)index.html程式碼

<!DOCTYPE HTML>
<html>
<head>
    <title>Flask-SocketIO Test</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.4/socket.io.js" integrity="sha512-aMGMvNYu8Ue4G+fHa359jcPb1u+ytAF+P2SCb+PxrjCdO3n3ZTxJ30zuH39rimUggmTwmh2u7wvQsDTHESnmfQ==" crossorigin="anonymous"></script>
    <script type="text/javascript" charset="utf-8">
        $(document).ready(function() {
            // 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.connect('http://' + document.domain + ':' + location.port);

            // 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();
                $('#transport').text(socket.io.engine.transport.name);
                socket.emit('my_ping');
            }, 1000);

            // 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><br>
      Current transport is: <b><span id="transport"></span></b><br>
      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>

(4)執行app.py程式碼,瀏覽器存取5000埠,如下:

 

(5)程式碼理解(最重要的部分!!!)

  flask-socketio包的常用方法理解:

  1. socketio.on和socketio.event是等價的,都是用來定義事件處理器(event handlers)的。區別是.on的第一個引數是事件名稱(event name),.event沒有這個引數,而是使用被裝飾的函數名作為事件名稱。其他引數是一樣的。事件名稱 connect / disconnect / message / json 都是SocketIO生成的特殊事件名,任何其他的事件名都被視為自定義事件。其他引數還有namespace(名稱空間)。

  2. send和emit都被伺服器用來向用戶端傳送訊息。send直接傳送訊息,emit需要指定事件和訊息。一般情況下,都是使用emit指定事件名傳送訊息。emit的其他引數有:

    A. namespace(名稱空間),和事件名配合使用。預設為"/"。

    B. broadcast(廣播模式True/False),是否向所有使用者端Client傳送訊息。

    C. to,通常為room_id,傳送給指定房間的所有使用者。

    D. callback(回撥函數),指定回撥函數,傳送到另一端執行。

  

啟動後的執行流程理解:

  1. 啟動時的初始執行流程。使用者端存取http://host:5000後,觸發index.html裡面的js程式碼,使用者端執行了後,

  var socket = io.connect('http://' + document.domain + ':' + location.port);

  使用者端和後臺伺服器建立了連線,注意,此時先觸發伺服器端的程式碼:  

@socketio.event
def connect():
    global thread
    with thread_lock:
        if thread is None:
            thread = socketio.start_background_task(background_thread)
    emit('my_response', {'data': 'Connected', 'count': 0})

  然後緊接著觸發了使用者端的程式碼:

socket.on('connect', function() {
                socket.emit('my_event', {data: 'I\'m connected!'});
            });

  所以,瀏覽器請求的web頁面 Receive部分:先是 Received #0: Connected,再是 Received #1: I'm connected! 

 

  2. 接下來,看看 瀏覽器請求的web頁面 Send部分:

  (1)echo:輸入123,瀏覽器會向伺服器端的my_event事件處理器傳送資料{"data": 123}

socket.emit('my_event', {data: $('#emit_data').val()});

  伺服器端的my_event事件處理器為:

@socketio.event
def my_event(message):
    session['receive_count'] = session.get('receive_count', 0) + 1
    emit('my_response',
         {'data': message['data'], 'count': session['receive_count']})

  可以看到,伺服器端在接收到資料後,又向用戶端的my_response事件處理器傳送資料。在看看index.html裡的my_response事件處理器是如何定義的:

socket.on('my_response', function(msg, cb) {
                $('#log').append('<br>' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html());
                if (cb)
                    cb();
            });

  最終,瀏覽器的web頁面顯示為 Received #2: 123。通過這個例子,也充分展示了websocket的功能,伺服器端和使用者端都主動可以向另一端傳送資料。這是有別於http的。http協定只能使用者端發起請求,伺服器端響應請求。伺服器端無法主動向使用者端傳送資料。

  (2)broadcast暫時不說了。

  (3)Join Room:這個和Leave Room是成對使用的。就像一個聊天室一樣,加入指定聊天室後,當執行Send to Room,就可以接收這個房間內的所有訊息。

  (4)Close Room:關閉房間

  (5)Disconnect:使用者端主動斷開連線,使用者端觸發伺服器端的disconnect_request事件處理器,

# 使用者端
socket.emit('disconnect_request');

# 伺服器端
@socketio.event
def disconnect_request():
    @copy_current_request_context
    def can_disconnect():
        disconnect()
    session['receive_count'] = session.get('receive_count', 0) + 1
    # for this emit we use a callback function
    # when the callback function is invoked we know that the message has been
    # received and it is safe to disconnect
    emit('my_response',
         {'data': 'Disconnected!', 'count': session['receive_count']},
         callback=can_disconnect)

  伺服器端收到請求後,會向用戶端的my_response事件處理器傳送資料,同時傳送一個callback回撥函數can_disconnect,讓使用者端執行該函數。

  最終瀏覽器的頁面顯示:Received #2: Disconnected!

 

三、寫在最後

至此,你應該已經對使用flask-socketio庫有了基本的認識了。如果還有不瞭解的,可以留言交流。

生產環境中,還需要新增例外處理,比如socketio.on_error()和socketio.on_error_default()。

本文只是入門使用教學,感興趣的話請大家自行查檔案深入理解。

 

附上官方教學連結

1. https://blog.miguelgrinberg.com/post/easy-websockets-with-flask-and-gevent

2. https://flask-socketio.readthedocs.io/en/latest/index.html