[翻譯] flask-SocketIO

最近開發工做須要用到websocket去替代老辦法輪詢,由於咱們的web系統使用flask搭建,因此使用flask-SocketIO做爲咱們的websocket方案,所以順手翻譯官方文檔
***
Flask-SocketIO 賦予了Flask應用在客戶端和服務器之間使用低延遲的雙向通信的能力。客戶端可以使用任意的SocketIO官方提供的clients庫(包括 Javascript, C++, Java及Swift)或者全部兼容的第三方client庫與server端創建一個長久鏈接。javascript

安裝

使用pip安裝:
pip install flask-socketiohtml

必備

Flask-SocketIO兼容ython 2.7 和 Python 3.3+. 這個包中異步services所依賴的可從如下三種中選擇:
1.eventlet 是最佳選擇, 支持長輪詢和WebSocket傳輸.
2.gevent 在一系列不一樣的配置中被支持. 長輪詢被gevent徹底支持,但不一樣於eventlet, gevent 沒有原生的WebSocket支持. 想要增長對WebSocket的支持有兩種辦法. 安裝gevent-websocket 增長gevent對WebSocket的支持或是使用帶有WebSocket功能的uWSGI web server.使用gevent也是一個高效的選項,但不如eventlet.
3.基於Werkzeug的Flask 開發環境server也是能夠的,不過注意它的性能比不上上面兩項,因此它應該只在簡單的開發工做流中使用,它只支持長輪詢.前端

這一擴展(即flask-SocketIO)自動檢查哪個異步框架被安裝了,從而決定使用哪個做爲它的異步框架。最好是使用eventlet,其次gevent。對於gevent中的WebSocket支持,首選uWSGI,而後是gevent-websocket。若是既沒有安裝eventlet也沒有安裝gevent,則使用Flask開發服務器。java

若是使用了多進程,一個消息隊列將被進程用來協調操做(例如廣播),支持的隊列有redis,rabbitMQ和其它被 Kombu支持的消息隊列。
在客戶端一側,官方Socket.IO Javascript client 庫可建立鏈接到server的鏈接. 一樣也有Swift, Java and C++的官方客戶端. 非官方的客戶端也沒有問題, 只要他們實現了Socket.IO協議.node

初始化

下面的栗子展現了怎樣把Flask-SocketIO加到Flask應用中nginx

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)

init_app()風格的初始化一樣支持。注意Web服務器的啓動方式。 socketio.run()函數封裝了Web服務器的啓動,代替了app.run()標準的Flask開發服務器啓動。當應用處於debug模式時,Werkzeug開發服務器仍在socketio.run()中使用並正確配置。在生產模式下使用eventlet Web服務器(若是可用),不然使用gevent Web服務器。若是沒有安裝eventlet和gevent,則使用Werkzeug development Web服務器。
一樣也支持基於click的命令行接口(Flask 0.11引入)。該擴展提供了適用於啓動Socket.IO服務器的flask run命令的新版本。用法舉例:
$ FLASK_APP=my_app.py flask run
應用必須向加載Socket.IO庫的客戶端提供一個頁面,並創建一個鏈接: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>

接收消息

當使用SocketIO時,消息被雙方做爲事件接收。在客戶端使用Javascript回調。使用Flask-SocketIO,服務器須要爲這些事件註冊處理程序,相似於視圖函數處理route的方式。
下面的栗子是一個處理未命名事件的服務端事件處理程序ajax

@socketio.on('message')
def handle_message(message):
    print('received message: ' + message)

上例使用了字符串消息,其它類型的未命名事件使用jsonredis

@socketio.on('json')
def handle_json(json):
    print('received json: ' + str(json))

最靈活的事件類型使用自定義事件名稱。這些事件的消息數據能夠是字符串,字節,整數或JSON:算法

@socketio.on('my event')
def handle_my_custom_event(json):
    print('received json: ' + str(json))

自定義的命名事件也支持多參數

@socketio.on('my event')
def handle_my_custom_event(arg1, arg2, arg3):
    print('received args: ' + arg1 + arg2 + arg3)

命名事件是最靈活的,由於它們不須要包含額外的元數據來描述消息類型。
Flask-SocketIO一樣支持SocketIO命名空間,它容許客戶端在同一個物理套接字上覆用幾個獨立的鏈接:

@socketio.on('my event', namespace='/test')
def handle_my_custom_namespace_event(json):
    print('received json: ' + str(json))

當沒有指定命名空間時,將使用名稱爲「/」的默認全局命名空間。 對於裝飾器語法不方便的狀況,可使用on_event方法:

def my_function_handler(data):
    pass

socketio.on_event('my event', my_function_handler, namespace='/test')

客戶能夠要求確認回覆,確認收到他們發送的消息。從處理函數返回的任何值將做爲回調函數中的參數傳遞給客戶端

@socketio.on('my event')
def handle_my_custom_event(json):
    print('received json: ' + str(json))
    return 'one', 2

在上面的例子中,客戶端回調函數將被兩個參數「1」和2調用。若是一個處理函數沒有返回任何值,客戶端回調函數將被調用而不帶參數。

發送消息

如上一節中所定義的SocketIO事件處理程序可使用send()和emit()函數向鏈接的客戶端發送回覆消息。
如下示例將收到的事件反饋回發送給它們的客戶端:

from flask_socketio import send, emit

@socketio.on('message')
def handle_message(message):
    send(message)

@socketio.on('json')
def handle_json(json):
    send(json, json=True)

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json)

請注意send()和emit()分別用於未命名事件和已命名事件。

使用命名空間時,默認狀況下,send()和emit()使用傳入消息的名稱空間。可使用可選的命名空間參數來指定不一樣的命名空間:

@socketio.on('message')
def handle_message(message):
    send(message, namespace='/chat')

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json, namespace='/chat')

要發送具備多個參數的事件,請發送一個元組:

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', ('foo', 'bar', json), namespace='/chat')

SocketIO支持確認消息被客戶端接收的確認回調:

def ack():
    print 'message was received!'

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json, callback=ack)

當使用回調函數時,Javascript客戶端接收到一個回調函數去發送一個已收到消息的回執。客戶端應用程序調用回調函數後,服務端一側也調用相應的服務器端回調。若是執行客戶端回調時帶有參數,則這些回調也做爲參數提供給服務器端回調(比較繞口,這一段仍是建議看原文)。

廣播

SocketIO的另外一個很是有用的功能是消息的廣播。 Flask-SocketIO支持使用broadcast = True和optional(可選參數)來send()和emit():

@socketio.on('my event')
def handle_my_custom_event(data):
    emit('my response', data, broadcast=True)

在啓用廣播選項的狀況下發送消息時,鏈接到命名空間的全部客戶端都會收到它,包括髮件人。當不使用命名空間時,鏈接到全局命名空間的客戶端將收到該消息。請注意,廣播消息不會調用回調。
在以前全部示例中,直到這一點,服務器都響應客戶端發送的事件。但對於某些應用程序,服務器須要成爲消息的發起者。將通知發送到服務器中發生的事件的客戶端可能會頗有用,例如在後臺線程中。 socketio.send()和socketio.emit()方法可用於向全部鏈接的客戶端廣播:

def some_function():
    socketio.emit('some event', {'data': 42})

請注意,socketio.send()和socketio.emit()與上文提到的send()和emit()不一樣。還要注意,在上面的用法中沒有客戶端上下文,因此已經假定broadcast=True,不須要特別指定。

聊天室

對於許多應用程序來講,有必要將用戶分紅能夠一塊兒處理的子集。最好的例子是有多個房間的聊天應用程序,用戶從房間或房間接收消息,而不是從其餘人的其餘房間接收消息。 Flask-SocketIO經過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()函數接受一個可選的房間參數,使得消息被髮送到給定房間中的全部客戶端。

全部的客戶端在鏈接時被分配一個房間,用鏈接的會話ID命名,能夠從request.sid中得到。一個給定的客戶能夠加入任何房間,能夠給任何名字。當一個客戶端斷開鏈接時,它將從它所在的全部房間中移除。上下文無關的socketio.send()和socketio.emit()函數也接受一個房間參數來廣播給房間中的全部客戶端。
因爲全部的客戶端都被分配了一個私人房間,因此爲了向一個客戶端發送消息,客戶端的會話ID能夠被用做房間參數。

鏈接事件

Flask-SocketIO也調度鏈接和斷開事件。如下示例顯示如何爲其註冊處理程序:

@socketio.on('connect', namespace='/chat')
def test_connect():
    emit('my response', {'data': 'Connected'})

@socketio.on('disconnect', namespace='/chat')
def test_disconnect():
    print('Client disconnected')

鏈接事件處理程序能夠選擇返回False來拒絕鏈接。這樣就能夠在這個時候驗證客戶端。 請注意,鏈接和斷開鏈接事件是在每一個使用的命名空間上單獨發送的。

基於類的命名空間

做爲上述基於裝飾器的事件處理程序的替代方法,屬於命名空間的事件處理程序能夠建立爲類的方法。 flask_socketio.Namespace做爲基類提供,以建立基於類的命名空間:

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'))

當使用基於類的命名空間時,服務器接收到的任何事件都會被分配到一個名爲帶有on_前綴的事件名稱的方法。例如,事件my_event將由名爲on_my_event的方法處理。若是收到的事件沒有在名稱空間類中定義的相應方法,則該事件將被忽略。在基於類的命名空間中使用的全部事件名稱必須使用方法名稱中合法的字符。
爲了方便在基於類的命名空間中定義的方法,命名空間實例包含了flask_socketio.SocketIO類中幾個方法的版本,當沒有給出命名空間參數時,默認爲適當的命名空間。
若是事件在基於類的名稱空間中有一個處理程序,而且還有基於裝飾器的函數處理程序,則只調用裝飾的函數處理程序。

錯誤處理

Flask-SocketIO也處理異常

@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

錯誤處理程序以Exception對象做爲參數
當前請求的消息和數據參數也可使用request.event變量進行檢查,這對於事件處理程序以外的錯誤日誌記錄和調試頗有用:

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,)

用nginx 作WebSocket的反向代理

你可能會使用nginx作前端反向代理,可是隻有nginx 1.4及以上版本支持websocket的代理,下面是代理http和websocket請求的基本配置:

server {
    listen 80;
    server_name _;

    location / {
        include proxy_params;
        proxy_pass http://127.0.0.1:5000;
    }

    location /socket.io {
        include proxy_params;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://127.0.0.1:5000/socket.io;
    }
}

下面的栗子增長了對多worker servers的負載均衡支持

upstream socketio_nodes {
    ip_hash;

    server 127.0.0.1:5000;
    server 127.0.0.1:5001;
    server 127.0.0.1:5002;
    # to scale the app, just add more nodes here!
}

server {
    listen 80;
    server_name _;

    location / {
        include proxy_params;
        proxy_pass http://127.0.0.1:5000;
    }

    location /socket.io {
        include proxy_params;
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass http://socketio_nodes/socket.io;
    }
}

雖然上述示例能夠做爲初始配置,但請注意,nginx做爲生產環境須要更完整的配置,涵蓋其餘部署方面,如提供靜態文件資源和SSL支持。

使用多worker

Flask-SocketIO從release 2.0版本開始支持在負載均衡器下的多worker模式。部署多個worker使得使用Flask-SocketIO的應用程序可以在多個進程和主機之間傳播客戶端鏈接,並以這種方式進行擴展以支持大量的併發客戶端。
使用多workers須要知足如下兩個要求:
1.負載均衡器必須配置爲能夠把來自給定客戶端的全部HTTP請求始終轉發給同一個worker。這有時被稱爲「粘性會話」(sticky sessions)。對於nginx,使用ip_hash指令來實現這一點。 Gunicorn不能在多worker模式下使用,由於其負載平衡器算法不支持粘性會話
2.因爲每一個server只管理一部分的客戶端鏈接,因此必需要有一個消息隊列用來協調處理複雜的操做,例如廣播和聊天室功能
使用消息隊列須要安裝一些依賴:
1.對於Redis,redis包須要安裝(pip install redis)
2.對於RabbitMQ,kombu包須要安裝(pip install kombu)
3.對於其餘kombu支持的消息隊列,查看Kombu documentation去尋找須要哪些依賴
4.若是使用了gevent或eventlet,那麼一般須要修補Python標準庫來強制消息隊列包使用協程友好的函數和類
要啓動多個Flask-SocketIO服務器,必須首先確保你有消息隊列服務正在運行。要啓動Socket.IO服務器並將其鏈接到消息隊列,請將message_queue參數添加到SocketIO構造函數中:
socketio = SocketIO(app, message_queue='redis://')
message_queue參數的值是使用的隊列服務的鏈接URL。對於在與server相同的主機上運行的redis隊列,可使用'redis://'URL。一樣,對於默認的RabbitMQ隊列,可使用'amqp://'URL。 Kombu軟件包有一個documentation section,描述了全部受支持隊列的URL格式

從外部進程發佈

對於許多類型的應用程序,有必要從不是SocketIO server的進程發出事件,例如Celery worker。若是將SocketIO server配置爲按照上一節所述在消息隊列中進行偵聽,則其餘任何進程均可以建立本身的SocketIO實例並使用它來以與server相同的方式發出事件。
例如,對於在eventlet Web server上運行並使用Redis消息隊列的應用程序,如下Python腳本向全部客戶端廣播一個事件:
socketio = SocketIO(message_queue='redis://') socketio.emit('my event', {'data': 'foo'}, namespace='/test') 以這種方式使用SocketIO實例時,Flask應用程序實例不會傳遞給構造函數。 SocketIO的通道參數可用於經過消息隊列選擇特定的通訊通道。當有多個獨立的SocketIO服務共享相同的隊列時,須要使用自定義通道名稱。 當使用eventlet或gevent時,Flask-SocketIO不適用monkey patching,可是在處理消息隊列時,若是Python標準庫沒有作monkey patching,那麼與消息隊列服務對話的Python包極可能會掛起。 須要注意的是,想要鏈接到SocketIO服務器的外部進程不須要像主服務器那樣使用eventlet或gevent。服務器使用協程框架,但外部進程不須要,例如Celery workers不須要被配置成使用gevent或eventlet,可是若是你的外部進程出於任何緣由確實使用了協程框架,那麼可能須要monkey patching,以便消息隊列能訪問協程友好的函數和類。

相關文章
相關標籤/搜索