python channels 簡介

目錄

1.Channels概念

2.安裝

3.開始使用Channels

Groups

Running with Channels

Persisting Data

Authenticantion

Routing

Models

Enforcing Ording


1.Channels概念

channel是一個隊列,每個task最多一個consumer接收

用name字符串辨別channels,可由不同的機器向同一個channel通信

def my_consumer(message):
    pass

channel_routing = {
    "some-channel": "myapp.consumers.my_consumer",
}

       對channel的每個message,Django會用一個message對象調用consumer函數。message對象含一個content屬性,是數據的字典,一個channel屬性,是channel。

       區別於Django的傳統request-response模式,channel將Django改編爲worker模式。傳統Django只工作在與WSGI服務器關聯的單進程,而Channel中的Django在三個不同的層運行:

1.服務層:包括WSGI適配器和WebSocket服務器。

2.channel backend

3.workers

# 這部分是僞代碼
# Listens on http.request
def my_consumer(message):
    # Decode the request from message format to aRequest object
    django_request = AsgiRequest(message)
    # Run view
    django_response = view(django_request)
    # Encode the response into message format
    for chunk in AsgiHandler.encode_response(django_response):
        message.reply_channel.send(chunk)

    事實上channels有兩種作用。

  1. 將work給consumers。
  2. 用於responses,只有interface server在監聽它,每個response channel命名不同,並且客戶端終止時會被退回interface server。

兩種channel差別不大,不同在於message可以由任意channel傳遞,但response channel只會將message傳到它所監聽的channel server。所以將它們視爲兩種不同的channel,並用!標記response channel,normal channel沒有標記,但它只能包含字母和-_

因爲channels只分配給一個監聽器,所以他們不能廣播。若希望給一組client發送消息,需要記錄這些response channels。

redis_conn = redis.Redis("localhost", 6379)

@receiver(post_save, sender=BlogUpdate)
def send_update(sender, instance, **kwargs):
    # Loop through all response channels and sendthe update
    for reply_channel in redis_conn.smembers("readers"):
        Channel(reply_channel).send({
            "id": instance.id,
            "content": instance.content,
        })

# Connected towebsocket.connect
def ws_connect(message):
    # Add to reader set
    redis_conn.sadd("readers", message.reply_channel.name)

           可以通過監聽websocket.disconnect將不需要的從readers中移出,對於在調用該方法之前就終止的特殊情況,用Group。Group通常只用於response channel

@receiver(post_save, sender=BlogUpdate)
def send_update(sender, instance, **kwargs):
    Group("liveblog").send({
        "id": instance.id,
        "content": instance.content,
    })

# Connected towebsocket.connect
def ws_connect(message):
    # Add to reader group
    Group("liveblog").add(message.reply_channel)

# Connected towebsocket.disconnect
def ws_disconnect(message):
    # Remove from reader group on clean disconnect
    Group("liveblog").discard(message.reply_channel)

  但是channel不保證發送task。如果需要確保可以用專門的系統如celery。

2.安裝

pip install -U channels

# 然後把 channels 添加到 settings.py文件 的INSTALLED_APPS 裏

3.開始使用Channels

        下面的例子可能不常用,但是可以很好地說明channels如何在Django的下層發揮作用。

        創建過程,將下面代碼放入consumer.py

from django.http import HttpResponse
from channels.handler import AsgiHandler

def http_consumer(message):
    # Make standard HTTP response -access ASGI path attribute directly
    response = HttpResponse("Hello world! You asked for %s" %message.content['path'])
    # Encode that response into messageformat (ASGI)
    for chunk in AsgiHandler.encode_response(response):
        message.reply_channel.send(chunk)

        最重要的是,message都是可JSON序列化的,所以request和response都是鍵值對形式。對於ASGI,你只需要知道有一個AsgiRequest類完成從ASGI到Django的request對象翻譯, 並且AsgiHandler類負責HttpResponse到ASGI消息的翻譯. 通常傳統Django的內建代碼會幫你完成這些工作.

       還有一件事, 需要告訴Django, 這個consumer應該與http.request channel關聯. 需要在settings文件中默認的channel layer以及它的路由規則.

# In settings.py
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgiref.inmemory.ChannelLayer",
        "ROUTING": "myproject.routing.channel_routing",
    },
}

# In routing.py
from channels.routing import route
channel_routing = [
    route("http.request", "myapp.consumers.http_consumer"),
    ]

        特別注意, 此例及之後大部分樣例中都用"in memory"channel layer. 它適合初學者, 但是沒有跨進程的channel溝通, 也只能用於"runserver". 實際產品中需要選擇其他的backend. 建議將這個文件命名爲routing.py,並且放在urls.py的同一目錄下.

        再來一個簡單的聊天服務器,如果不爲指定http.request指定consumer,則它會由Django的views處理。

# In consumers.py

def ws_message(message):
    # ASGI WebSocket packet-received andsend-packet message types
    # both have a "text" key for their textual data.
   message.reply_channel.send({
        "text":message.content['text'],
    })

# In routing.py
from channels.routing import route
from myapp.consumers import ws_message

channel_routing= [
    route("websocket.receive",ws_message),
]

// Note that the path doesn't matter for routing; any WebSocket
// connection gets bumped over to WebSocket consumers
socket = new WebSocket("ws://" + window.location.host +"/chat/");
socket.onmessage = function(e) {
   alert(e.data);
}
socket.onopen = function() {
    socket.send("hello world");
}

Groups

        接下來是一個真正實現聊天的服務器。注意,channels 的設計默認允許有一小部分消息不會被正常發送,這樣保證當錯誤出現時不會影響整個系統。

# In consumers.py
from channels import Group

# Connected to websocket.connect
def ws_add(message):
   Group("chat").add(message.reply_channel)

# Connected to websocket.receive
def ws_message(message):
   Group("chat").send({
        "text": "[user]%s" % message.content['text'],
    })

# Connected to websocket.disconnect
def ws_disconnect(message):
   Group("chat").discard(message.reply_channel)



from channels.routing import route
from myapp.consumers import ws_add, ws_message, ws_disconnect

channel_routing= [
    route("websocket.connect",ws_add),
    route("websocket.receive",ws_message),
   route("websocket.disconnect", ws_disconnect),
]



// Note that the path doesn't matter right now; any WebSocket
// connection gets bumped over to WebSocket consumers
socket = new WebSocket("ws://" + window.location.host +"/chat/");
socket.onmessage = function(e) {
   alert(e.data);
}
socket.onopen = function() {
    socket.send("helloworld");
}

 

Running with Channels

        因爲Channels將Django置入多進程模型,所以不再依靠一個WSGI服務器工作在單進程。而是運行通過channel layer關聯起來的多個interface server和多個worker servers。

        有許多種interface server,每個都負責一種request。它們與worker server解耦,而由channellayer傳輸channel的內容。從產品的角度看,你通常會將worker server當作與interfaceservers不同的集羣運行,雖然你也可以將他們作爲不同的進程運行在同一臺機器上。

        Django默認沒有channel layer。上個例子我們用的in-memory channel layer,它將channel數據存儲在內存中的一個字典裏,所以不能跨進程。當部署時還是要換成Redis後端asgi_redis等。

        第二件事,當我們設置好了channel後端,要確保我們的interface layer可以用於WebSockets。Channels用daphne解決這個問題。它是個可以同時處理HTTP和WebSockets的interfaceserver,並且當你runserver時運行。(其實就是,runserver運行Daphne在一個線程,一個worker在另一個線程,但都在同一個進程)

        我們來試試Redis後端。先安裝asgi_redis包。然後設置channel layer:

# In settings.py
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "asgi_redis.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("localhost", 6379)],
        },
        "ROUTING": "myproject.routing.channel_routing",
    },
}

        運行runserver,它會和前面的一樣運行,你也可以試試跨進程類型。將下面兩個命令在兩個終端運行:

manage.py runserver --noworker
manage.py runworker

       你可能猜到了,它在runserver中禁用了worker並且在另一個進程中處理。如果你想看運行consumers的logging,也可以向runworker傳遞-v 2參數。

       如果Django運行在debug模式,runworker像runserver一樣會用於靜態文件。就像通常的Django設置,你需要設置Debug模式關掉後你自己的靜態文件。

 

Persisting Data

       我們考慮一個基本的聊天網站,基於初始連接。

       記住,Channels是網絡透明的,並且可以在多個worker運行,所以你不能僅把東西存儲在本地的全局變量。正如Django的session框架用cookie作爲key,Channels提供channel_session裝飾器。它提供message.channel_session屬性,就像Django session。

# In consumers.py
from channels import Group
from channels.sessions import channel_session

# Connected towebsocket.connect
@channel_session
def ws_connect(message):
    # Work out room name from path (ignore slashes)
    room = message.content['path'].strip("/")
    # Save room in session and add us to the group
    message.channel_session['room'] = room
    Group("chat-%s" % room).add(message.reply_channel)

# Connected towebsocket.receive
@channel_session
def ws_message(message):
    Group("chat-%s" % message.channel_session['room']).send({
        "text": message['text'],
    })

# Connected towebsocket.disconnect
@channel_session
def ws_disconnect(message):
    Group("chat-%s" %message.channel_session['room']).discard(message.reply_channel)



# in routing.py
from channels.routing import route
from myapp.consumers import ws_connect, ws_message,ws_disconnect

channel_routing = [
    route("websocket.connect", ws_connect),
    route("websocket.receive", ws_message),
    route("websocket.disconnect", ws_disconnect),
]

Authenticantion

       目前的WebSocket無法與網站的其餘成員通信。幸運的是,由於Channels在WebSocket和ASGI的底層,它自帶了認證和Django session的裝飾器。Channels可以通過cookies或session_key屬性得到Django sessions。你用http_session裝飾器獲得Django session(message.http_session屬性,就像request.session)。你也可以用http_session_user裝飾器獲得message.user屬性。

       注意這些只是WebSocket的HTTP消息的詳細信息,並沒有消耗多餘的帶寬。

       也意味着我們需要從connection handler中獲取user並將它保存在session中,。Channels自帶了channel_session_user裝飾器,就像http_session_user裝飾器,不同的是前者從channel session獲取user。還有一個函數transfer_user從一個session複製user到另一個。更棒的是,它把上面兩個功能都組合進channnel_session_user_from_http裝飾器。

     我們實現一個服務器,只能與名字第一個字母相同的人對話。

# In consumers.py
from channels import Channel, Group
from channels.sessions import channel_session
from channels.auth import http_session_user,channel_session_user, channel_session_user_from_http

# Connected towebsocket.connect
@channel_session_user_from_http
def ws_add(message):
    # Add them to the right group
    Group("chat-%s" %message.user.username[0]).add(message.reply_channel)

# Connected towebsocket.receive
@channel_session_user
def ws_message(message):
    Group("chat-%s" % message.user.username[0]).send({
        "text": message['text'],
    })

# Connected towebsocket.disconnect
@channel_session_user
def ws_disconnect(message):
    Group("chat-%s" %message.user.username[0]).discard(message.reply_channel)

       如果你只是runserver(Daphne),你就可以正常連接並且你的cookies也應該傳遞你的auth。如果你將WebSockets運行在不同的端口,你必須在URL中提供Django session ID。

socket = newWebSocket("ws://127.0.0.1:9000/?session_key=abcdefg");
你可以在模板中得到session key{{request.session.session_key }}。注意它對於signed cookie sessions無效。

 

Routing

        routing.py文件很像Django的urls.py。也可以用正則。

http_routing = [
    route("http.request",poll_consumer, path=r"^/poll/$", method=r"^POST$"),
]

chat_routing = [
    route("websocket.connect",chat_connect, path=r"^/(?P<room>[a-zA-Z0-9_]+)/$),
   route("websocket.disconnect", chat_disconnect),
]

routing = [
    # You can use a string import path asthe first argument as well.
    include(chat_routing,path=r"^/chat"),
    include(http_routing),
]

routing按順序執行,有短路的可能。也可以不以^起始,而用python的re.match函數,但這需要經驗。

 

Models

      可以用Django的ORM處理信息的持久化。考慮性能:我們可以爲聊天消息定製channel,並且加入保存和發送的步驟,這樣發送進程和consumer就可以快速結束而不用等待。

# In consumers.py
from channels import Channel
from channels.sessions import channel_session
from .models import ChatMessage

# Connected tochat-messages
def msg_consumer(message):
    # Save to model
    room = message.content['room']
    ChatMessage.objects.create(
        room=room,
        message=message.content['message'],
    )
    # Broadcast to listening sockets
    Group("chat-%s" % room).send({
        "text": message.content['message'],
    })

# Connected towebsocket.connect
@channel_session
def ws_connect(message):
    # Work out room name from path (ignore slashes)
    room = message.content['path'].strip("/")
    # Save room in session and add us to the group
    message.channel_session['room'] = room
    Group("chat-%s" % room).add(message.reply_channel)

# Connected towebsocket.receive
@channel_session
def ws_message(message):
    # Stick the message onto the processing queue
    Channel("chat-messages").send({
        "room": channel_session['room'],
        "message": message['text'],
    })

# Connected towebsocket.disconnect
@channel_session
def ws_disconnect(message):
    Group("chat-%s" %message.channel_session['room']).discard(message.reply_channel)

        注意我們可以從任何地方將message加入chat-messages channel。

 

Enforcing Ording

        因爲Channles是分佈式系統,默認它按workers從隊列中獲得的順序處理消息。很可能interface server發出非常近的connect和receive消息,connect還未被處理完,receive就被另一個worker處理。

        Channels的解決方法是enforce_ordering裝飾器。所有websocket消息都包含一個order鍵,這個裝飾器用這個鍵確保message按順序處理。有兩個模式:

        Slightordering:connect先處理,其他無序。

        Strictordering:所有都按序。

# In consumers.py
from channels import Channel, Group
from channels.sessions import channel_session, enforce_ordering
from channels.auth import http_session_user,channel_session_user, channel_session_user_from_http

# Connected towebsocket.connect
@enforce_ordering(slight=True)
@channel_session_user_from_http
def ws_add(message):
    # Add them to the right group
    Group("chat-%s" %message.user.username[0]).add(message.reply_channel)

# Connected towebsocket.receive
@enforce_ordering(slight=True)
@channel_session_user
def ws_message(message):
    Group("chat-%s" % message.user.username[0]).send({
        "text": message['text'],
    })

# Connected towebsocket.disconnect
@enforce_ordering(slight=True)
@channel_session_user
def ws_disconnect(message):
    Group("chat-%s" %message.user.username[0]).discard(message.reply_channel)python channels筆記-

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章