flask-socketio 實現

Flask-SocketIO使Flask應用程序能夠訪問客戶端和服務器之間的低延遲雙向通訊。 客戶端應用程序可使用Javascript,C ++,Java和Swift中的任何SocketIO官方客戶端庫,或任何兼容的客戶端來創建與服務器的永久鏈接。javascript

1,安裝html

pip install flask-socketio

2,依賴
Flask-SocketIO兼容Python 2.7和Python 3.3+。這個軟件包所依賴的異步服務能夠從如下三種選擇中選擇:前端

eventlet是最好的高性能選項,支持長輪詢和WebSocket傳輸。
gevent支持多種不一樣的配置。 long-polling傳輸徹底由gevent包支持,但與eventlet不一樣,gevent沒有原生的WebSocket支持。爲了添加對WebSocket的支持,目前有兩種選擇。安裝gevent-websocket軟件包會將WebSocket支持添加到gevent中,或者可使用隨WebSocket功能一塊兒提供的uWSGI Web服務器。 gevent的使用也是一個性能選項,但比eventlet略低。
也可使用基於Werkzeug的Flask開發服務器,但缺乏其餘兩個選項的性能,所以只能用於簡化開發流程。該選項僅支持長輪詢傳輸。
擴展程序根據安裝的內容自動檢測使用哪一個異步框架。優先考慮eventlet,接着是gevent。對於gevent中的WebSocket支持,首選uWSGI,而後是gevent-websocket。若是既沒有安裝eventlet也沒有安裝gevent,則使用Flask開發服務器。java

若是使用多個進程,則進程使用消息隊列服務來協調諸如廣播的操做。受支持的隊列是Redis,RabbitMQ和Kombu軟件包支持的任何其餘消息隊列。node

在客戶端,官方的Socket.IO Javascript客戶端庫能夠用來創建到服務器的鏈接。還有用Swift,Java和C ++編寫的官方客戶端。只要他們實現Socket.IO協議,非官方客戶端也能夠工做。nginx

3,初始化
簡單的代碼例子:web

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開發服務器啓動。 當應用程序處於調試模式時,Werkzeug開發服務器仍在socketio.run()中使用和正確配置。 在生產模式下首選使用eventlet Web服務器,不然使用gevent Web服務器。 若是沒有安裝eventlet和gevent,則使用Werkzeug開發Web服務器。ajax

還支持基於Flask 0.11中引入的點擊命令行界面。 該擴展提供了適用於啓動Socket.IO服務器的flask run命令的新版本。 用法示例:redis

$ FLASK_APP=my_app.py flask run

應用程序必須向加載Socket.IO庫的客戶端提供一個頁面,並創建一個鏈接:算法

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

4,接收信息:
1)當使用SocketIO時,消息被雙方做爲事件接收。 在客戶端使用Javascript回調。 使用Flask-SocketIO,服務器須要爲這些事件註冊處理程序,相似於視圖函數處理路由的方式。

如下示例爲未命名的事件建立一個服務器端事件處理程序:

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

2)上面的例子使用字符串消息。 另外一種未命名事件使用JSON數據:

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

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

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

4)自定義命名事件也能夠支持多個參數:

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

5)命名事件是最靈活的,由於它們不須要包含額外的元數據來描述消息類型。

Flask-SocketIO還支持SocketIO命名空間,它容許客戶端在同一個物理套接字上覆用幾個獨立的鏈接:

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

6)當沒有指定名稱空間時,將使用名稱爲「/」的默認全局名稱空間。

對於裝飾器語法不方便的狀況,可使用on_event方法:

def my_function_handler(data):
      pass

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

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

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

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

5,發送信息
如上一節中所示定義的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客戶端接收到一個回調函數來接收消息。 客戶端應用程序調用回調函數後,調用相應的服務器端回調。 若是用參數調用客戶端回調,則這些回調也做爲參數提供給服務器端回調。

6,廣播
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,不須要指定。

8,聊天室
對於許多應用程序來講,有必要將用戶分紅能夠一塊兒處理的子集。 最好的例子是有多個房間的聊天應用程序,用戶從房間或房間接收消息,而不是從其餘房間的其餘房間接收消息。 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能夠被用做房間參數。


9,鏈接事件
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來拒絕鏈接。 這樣就能夠在這個時候驗證客戶端。

請注意,鏈接和斷開鏈接事件在每一個使用的名稱空間上單獨發送。

10,基於類的命名空間
做爲上述基於裝飾器的事件處理程序的替代方法,屬於名稱空間的事件處理程序能夠把類的方法名映射到命名空間。 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類中的幾個方法的版本,當沒有給出命名空間參數時,默認爲適當的命名空間。

若是事件在基於類的名稱空間中有一個處理程序,而且還有基於裝飾器的函數處理程序,則只調用裝飾的函數處理程序。

11,處理錯誤
處理錯誤的例子:

@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

錯誤處理函數將異常對象做爲參數。

當前請求的消息和數據參數也可使用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,)

12,訪問flask全局內容
SocketIO事件的處理程序與路由處理程序的處理程序不一樣,這引發了有關在一個SocketIO處理程序中能夠和不能夠完成什麼的困惑。主要區別在於,爲客戶端生成的全部SocketIO事件都發生在單個長時間運行請求的上下文中。

儘管有所不一樣,但Flask-SocketIO試圖經過使環境相似於普通的HTTP請求,來使得SocketIO事件處理程序更容易工做。如下列表描述了什麼可行,什麼不可行:

在調用事件處理程序以前推送應用程序上下文,使得處理程序可使用current_app和g。
在調用處理程序以前,還會推送請求上下文,同時也使請求和會話可用。可是請注意,WebSocket事件沒有與它們關聯的單個請求,因此啓動鏈接的請求上下文被推送到鏈接生命期間分派的全部事件。
全局請求上下文使用sid成員進行了加強,該成員被設置爲鏈接的惟一會話ID。此值用做添加客戶端的初始空間。
經過包含當前處理的名稱空間和事件參數的名稱空間和事件成員來加強請求上下文全局。事件成員是一個帶有消息和參數鍵的字典。
會話上下文全局的行爲方式與常規請求不一樣。在創建SocketIO鏈接時用戶會話的副本可用於該鏈接上下文中調用的處理程序。若是一個SocketIO處理程序修改會話,修改的會話將被保留以供未來的SocketIO處理程序使用,但常規的HTTP路由處理程序將不會看到這些更改。實際上,當SocketIO處理程序修改會話時,將爲這些處理程序專門建立會話的「分支」。這種限制的技術緣由是爲了保存用戶會話,須要將cookie發送到客戶端,而且須要HTTP請求和響應,這在SocketIO鏈接中不存在。在使用服務器端會話(如Flask-Session或Flask-KVSession擴展提供的會話)時,只要會話在SocketIO處理程序中未修改,則能夠經過SocketIO處理程序查看HTTP路由處理程序中對會話所作的更改。
對於SocketIO事件處理程序,不會調用before_request和after_request掛鉤。
SocketIO處理程序可使用自定義裝飾器,可是大多數Flask裝飾器將不適合用於SocketIO處理程序,由於在SocketIO鏈接期間沒有關於Response對象的概念。
13 受權
應用程序的共同需求是驗證用戶的身份。 基於Web表單和HTTP請求的傳統機制不能用於SocketIO鏈接,由於沒有地方發送HTTP請求和響應。 若是須要,應用程序能夠實現一個定製的登陸表單,當用戶按下提交按鈕時,該表單將證書做爲SocketIO消息發送到服務器。

可是,在大多數狀況下,在SocketIO鏈接創建以前執行傳統的認證過程會更方便。 而後能夠將用戶的標識記錄在用戶會話中或cookie中,稍後在創建SocketIO鏈接時,能夠經過SocketIO事件處理程序訪問該信息。

14 ,用flask-login 做爲例子講解flask-socketIO
Flask-SocketIO能夠訪問由Flask-Login維護的登陸信息。 在執行常規Flask-Login身份驗證並調用login_user()函數以在用戶會話中記錄用戶以後,任何SocketIO鏈接都將有權訪問current_user上下文變量:

@socketio.on('connect')
def connect_handler():
    if current_user.is_authenticated:
        emit('my response',
             {'message': '{0} has joined'.format(current_user.name)},
             broadcast=True)
    else:
        return False  # not allowed here

請注意,login_required裝飾器不能與SocketIO事件處理程序一塊兒使用,但能夠建立斷開未經認證用戶的自定義裝飾器,以下所示:

import functools
from flask import request
from flask_login import current_user
from flask_socketio import disconnect

def authenticated_only(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        if not current_user.is_authenticated:
            disconnect()
        else:
            return f(*args, **kwargs)
    return wrapped

@socketio.on('my event')
@authenticated_only
def handle_my_custom_event(data):
    emit('my response', {'message': '{0} has joined'.format(current_user.name)},
         broadcast=True)

15 發佈
部署Flask-SocketIO服務器有多種選擇,從簡單到複雜。 在本節中,將介紹最經常使用的選項。
1)嵌入式服務
最簡單的部署策略是安裝eventlet或gevent,並經過調用socketio.run(app)來啓動Web服務器,如上例所示。 這將在eventlet或gevent web服務器上運行應用程序,以安裝的爲準。

請注意,當安裝eventlet或gevent時,socketio.run(app)會運行一個生產準備服務器。 若是這兩個都沒有安裝,那麼應用程序將在Flask的開發Web服務器上運行,這不適合於生產使用。

不幸的是,在uWSGI中使用gevent時,這個選項是不可用的。 有關此選項的信息,請參閱下面的uWSGI部分。
2)gunicorn web 服務
socketio.run(app)的替代方法是使用gunicorn做爲web服務器,使用eventlet或gevent工做。 對於這個選項,除了gunicorn以外,還須要安裝eventlet或gevent。 經過gunicorn啓動eventlet服務器的命令行是:
gunicorn --worker-class eventlet -w 1 module:app
若是您更喜歡使用gevent,則啓動服務器的命令是:

gunicorn -k gevent -w 1 module:app

當gevent worker使用gunicorn和gevent-websocket提供的WebSocket支持時,必須更改啓動服務器的命令以選擇支持WebSocket協議的自定義gevent Web服務器。 修改後的命令是:

```gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module:app```
在全部這些命令中,module是定義應用程序實例的Python模塊或包,app是應用程序實例自己。

Gunicorn版本18.0是Flask-SocketIO推薦的版本。 已知19.x版本在包含WebSocket的某些部署方案中具備不兼容性。

因爲gunicorn使用有限的負載均衡算法,所以使用此Web服務器時不可能使用多個工做進程。 出於這個緣由,上面的全部例子都包含-w 1選項。

3) uWSGI web服務
將uWSGI服務器與gevent結合使用時,Socket.IO服務器能夠利用uWSGI的原生WebSocket支持。

有關uWSGI服務器的配置和使用的完整說明超出了本文檔的範圍。 uWSGI服務器是一個至關複雜的軟件包,提供了大量全面的選項。 必須使用WebSocket和SSL支持來編譯WebSocket傳輸才能使用。 做爲介紹的一種方式,如下命令在端口5000上爲示例應用程序app.py啓動一個uWSGI服務器:
```$ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file app.py --callable app```

16 ngnix 作反向代理
可使用nginx做爲將請求傳遞給應用程序的前端反向代理。 可是,只有nginx 1.4和更新版本支持WebSocket協議的代理。 如下是代理HTTP和WebSocket請求的基本nginx配置:

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;
    }
}

 

下一個示例添加了對多個Socket.IO服務器的負載平衡的支持:

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支持。

17,使用多進程
Flask-SocketIO支持從2.0版本開始的負載均衡器以後的多個工做者。 部署多個工做者使得使用Flask-SocketIO的應用程序可以在多個進程和主機之間傳播客戶端鏈接,並以這種方式進行擴展以支持大量的併發客戶端。
有兩個要求使用多個Flask-SocketIO工做者:

負載均衡器必須配置爲未來自給定客戶端的全部HTTP請求始終轉發給同一個工做者。這有時被稱爲「粘性會話」。對於nginx,使用ip_hash指令來實現這一點。 Gunicorn不能用於多個工做者,由於它的負載平衡器算法不支持粘性會話。
因爲每一個服務器只擁有客戶端鏈接的一部分,所以服務器使用Redis或RabbitMQ等消息隊列來協調諸如廣播和房間之類的複雜操做。
使用消息隊列時,還須要安裝其餘依賴項:

對於Redis,必須安裝軟件包redis(pip install redis)。
對於RabbitMQ,必須安裝包kombu(pip install kombu)。
對於Kombu支持的其餘消息隊列,請參閱Kombu文檔以瞭解須要什麼依賴關係。
若是使用eventlet或gevent,那麼一般須要修補Python標準庫來強制消息隊列包使用協程友好的函數和類。
要啓動多個Flask-SocketIO服務器,必須首先確保您有消息隊列服務正在運行。要啓動Socket.IO服務器並將其鏈接到消息隊列,請將message_queue參數添加到SocketIO構造函數中:
```socketio = SocketIO(app, message_queue='redis://')```
message_queue參數的值是使用的隊列服務的鏈接URL。 對於在與服務器相同的主機上運行的redis隊列,可使用「redis://」URL。 一樣,對於默認的RabbitMQ隊列,可使用「amqp://」URL。 Kombu軟件包有一個文檔部分,描述全部支持的隊列的URL的格式。

對於許多類型的應用程序,有必要從不是SocketIO服務器的進程發出事件,例如一個Celery進程。 若是將SocketIO服務器或服務器配置爲按照上一節所述在消息隊列中進行偵聽,則其餘任何進程均可以建立本身的SocketIO實例,並使用該實例以與服務器相同的方式發出事件。

例如,對於在eventlet Web服務器上運行並使用Redis消息隊列的應用程序,如下Python腳本向全部客戶端廣播一個事件:

socketio = SocketIO(message_queue='redis://')
socketio.emit('my event', {'data': 'foo'}, namespace='/test')

以這種方式使用SocketIO實例時,Flask應用程序實例不會傳遞給構造函數。

SocketIO的通道參數可用於經過消息隊列選擇特定的通訊通道。當有多個獨立的SocketIO服務共享相同的隊列時,使用自定義通道名是必要的。

當使用eventlet或gevent時,Flask-SocketIO不適用猴子補丁。可是,當使用消息隊列時,若是Python標準庫沒有被修補,與消息隊列服務對話的Python包極可能會掛起。

須要注意的是,要鏈接到SocketIO服務器的外部進程不須要像主服務器那樣使用eventlet或gevent。讓服務器使用協程框架,而外部進程不是問題。例如,芹菜工做人員不須要配置爲僅僅由於主服務器而使用eventlet或gevent。可是若是你的外部進程無論用什麼理由使用協程框架,那麼可能須要猴子補丁,這樣消息隊列就能夠訪問協程友好的函數和類。

 

地址:http://www.javashuo.com/article/p-kfxcfwrn-h.html

相關文章
相關標籤/搜索