flask-socketio使用遇到的坑,版本相容問題和while True堵塞訊息問題

1
2
  特別說明,下面解決方法是在按網上最容易查到的一個範例參考使用時,遇到了問題,反覆使用仍沒有解決情況下,找到了下面文章,發現是後台的while true問題,特別編輯理髮一下,同時感謝原作者,引用地址如下,避免大家遇到同樣問題。
  [https://www.cnblogs.com/luozx207/p/9714487.html](https://www.cnblogs.com/luozx207/p/9714487.html)

特別請大家注意,版本相容問題:

websocket連接是客戶端與伺服器之間永久的雙向通訊通道,直到某方斷開連接。
  雙向通道意味著在連接時,伺服端隨時可以發送訊息給客戶端,反之亦然,這在一些需要即時通訊的場景例如多人聊天室非常重要。
  flask_socketio實現了對websocket的封裝,它可以讓執行flask應用的伺服端和客戶端建立全雙工通道。
  flask_socketio是一個python庫,是flask框架的擴展。

一、安裝

1
pip install flask-socketio

二、實現對flask的封裝

1
2
3
4
5
6
7
8
from flask import Flask, render_template
from flask_socketio import SocketIO,emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

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

socketio.run()函式封裝了flask的web伺服器的啟動

三、伺服端向客戶端推送

socketio的兩個函式send()和emit()都可以實現訊息發送,前者用於無名事件,後者用於命名的事件。
  事件是訊息的名稱。如果把訊息比做信件,事件就是貼在信封上的標識,這個標識規定了信件送往客戶端或伺服端的某個函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, render_template
from flask_socketio import SocketIO,emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

@socketio.on('connect', namespace='/test_conn')
def test_connect():
        socketio.emit('server_response',
                      {<!-- -->'data': 『connected』},namespace='/test_conn')

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

例如上面socketio.on(『connect』,namespace=』/test_conn』)中的connect就是soketio的內建事件,當客戶端與伺服端連接之後,前端和後端都會收到一個名為『connect』的事件,伺服端接到這個事件就會執行test_connect函式中的內容了。
  再說namespace,namespace可以標誌多個事件,在官方檔案的解釋是「Namespaces allow a client to open multiple connections to the server that are multiplexed on a single socket.」。當一個客戶端連接伺服器的不同命名域的時候,可以在同一個socket連接里完成。我的理解是一個namespace就定義了一個後端websocket連接的介面,客戶端與伺服器透過三次握手建立socket連接後,連接不同的伺服器介面,socket的連接並不會斷開。這可以類比於http的路由(但是完全不同哦,因為傳輸協定完全不一樣),在http連接範疇,當使用者登入後,訪問伺服器不同的路由並不會改變它的登入狀態。一個後端介面可以接受多個客戶端的socket連接,如果在後端的emit中定義『broadcast=True』,那麼所有連接到這個命名域的客戶端都會收到這個訊息,命名域之間也可以透過發送訊息指定命名域的方式來相互通訊。
  再看soketio.emit,第一個引數』server_response』是伺服端發送這個訊息的事件名,在客戶端要建立一個接受這個事件的函式處理,後面的字典就是訊息內容,namespace=』/test_conn』表示這個訊息還是發送到同一個通道(test_conn)中。emit發送訊息只能從前端發到後端或者從後端發向前端,如果在在前端emit(『event』,{data})再寫socket.on(『event』, {data})是收不到的。

四、定時推送

實驗的目的是伺服端定時發送一個亂數到客戶端,並且客戶端可以及時顯示。
  一開始,我在socketio裝飾的函式中寫了一個while迴圈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, render_template
from flask_socketio import SocketIO,emit
import random
async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('connect', namespace='/test_conn')
def test_connect():
    while True:
        socketio.sleep(5)
        t = random.randint(1, 100)
        socketio.emit('server_response',
                      {<!-- -->'data': t},namespace='/test_conn')

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

事實證明這樣是行不通的,雖然看上去,雖然伺服端陷入while的無窮迴圈中,但是emit函式每次都會執行,所以理論上客戶端應該可以定時收到伺服端的亂數。但是結果是客戶端根本接收不到,連soketio.on函式都沒有觸發執行。
  原因應該是當伺服端陷入無窮迴圈,會影響與客戶端之間的websocket連接,總之寫while true需謹慎
  在flask_socketio的范常式式中,我找到了用後台執行緒進行while迴圈以解決這個問題的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from flask import Flask, render_template
from flask_socketio import SocketIO,emit
from threading import Lock
import random
async_mode = None
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
thread = None
thread_lock = Lock()

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('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)
        socketio.emit('server_response',
                      {<!-- -->'data': t},namespace='/test_conn')

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

五、客戶端

index.html的內容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" src="//code.jquery.com/jquery-1.4.2.min.js"></script>
    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io.min.js"></script>
</head>
<body>
<h1 id="t"></h1>
<script type="text/javascript">
    $(document).ready(function() {<!-- -->
        namespace = '/test_conn';
        var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace, {<!-- -->transports: ['websocket']});
        socket.on('server_response', function(res) {<!-- -->
            console.log(res.data);
            $('#t').text(res.data);
        });
    });
</script>
</body>
</html>

注意客戶端也要匯入socketio的庫,然後用io.connect建立命名域的socket連接。
  如果不加{transports: [『websocket』]},實際上建立的是長輪詢。長輪詢或websocket都是由客戶端發起的

最後在瀏覽器輸入http://127.0.0.1:5000就可以了