Django-channels2.0筆記--1憎夷、搭建一個(gè)簡(jiǎn)單的聊天服務(wù)器

官方文檔鏈接:https://channels.readthedocs.io/en/latest/

聊天服務(wù)器包括兩個(gè)網(wǎng)頁(yè):提供索引的index頁(yè),用來(lái)選擇要加入的聊天室的名稱(chēng);進(jìn)行聊天的room頁(yè)蠢箩,用來(lái)查看特定聊天室中發(fā)布的信息

通過(guò)以下查看當(dāng)前Django的安裝版本

python3 -m django --version

通過(guò)以下命令查看安裝的channels版本

python3 -c 'import channels;print(channels.__version__)'

Django2.0適配python3.5+和Django1.11+

創(chuàng)建一個(gè)mysite的項(xiàng)目并添加一個(gè)chat的app

django-admin startproject mysite
python3 manage.py startapp chat

將除了view之外的文件刪除,通過(guò)tree命令可以得到如下樹(shù)狀圖

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py
  chat/
        __init__.py
        views.py

在setting文件中添加chat和channels模塊

# mysite/settings.py
INSTALLED_APPS = [
    'chat',
    'channels',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

在template文件夾中創(chuàng)建一個(gè)index.html文件事甜,并添加以下內(nèi)容:

<!-- chat/templates/chat/index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Rooms</title>
</head>
<body>
    What chat room would you like to enter?<br/>
    <input id="room-name-input" type="text" size="100"/><br/>
    <input id="room-name-submit" type="button" value="Enter"/>

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#room-name-submit').click();
            }
        };

        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>
</body>
</html>

修改chat模塊的view谬泌,添加響應(yīng)的視圖函數(shù)

# chat/views.py
from django.shortcuts import render

def index(request):
    return render(request, 'chat/index.html', {})

在chat模塊中新建一個(gè)urls文件并添加URL路徑

# chat/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
]

修改根路徑下的urls文件,添加url配置信息

# mysite/urls.py
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^chat/', include('chat.urls')),
    url(r'^admin/', admin.site.urls),
]

如果你使用的是Django2.0+的版本而不是1.x的版本需要手動(dòng)從django.conf.url中導(dǎo)入url和include逻谦,因?yàn)閺腄jango2.0開(kāi)始模塊使用的是path而不是url

配置完成之后執(zhí)行運(yùn)行命令

python3 manage.py runserver

可以在命令行界面看到以下內(nèi)容

Django version 1.11.10, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

至此基本的項(xiàng)目構(gòu)建完畢掌实,接下來(lái)完成channels的構(gòu)建

在channels中有兩個(gè)不同于常規(guī)Django項(xiàng)目的文件,分別是routing和consumers邦马,routing文件用來(lái)識(shí)別websocket連接贱鼻,consumers用來(lái)處理websocket,簡(jiǎn)單來(lái)說(shuō)前者相當(dāng)于Django中的url滋将,而后者相當(dāng)于Django中的view

首先需要在setting文件中添加channels的根routing配置位置

# mysite/settings.py
# Channels
ASGI_APPLICATION = 'mysite.routing.application'

接著創(chuàng)建在根路徑下創(chuàng)建一個(gè)routing文件邻悬,添加以下內(nèi)容

# mysite/routing.py
from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
    # (http->django views is added by default)
})

現(xiàn)在再次重啟服務(wù),可以命令行信息改動(dòng)

Django version 1.11.10, using settings 'mysite.settings'
Starting ASGI/Channels development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
2018-02-18 22:16:23,729 - INFO - server - HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2018-02-18 22:16:23,730 - INFO - server - Configuring endpoint tcp:port=8000:interface=127.0.0.1
2018-02-18 22:16:23,731 - INFO - server - Listening on TCP address 127.0.0.1:8000

現(xiàn)在的啟動(dòng)方式由默認(rèn)啟動(dòng)方式變成了ASGI/channels啟動(dòng)耕渴,同時(shí)還有對(duì)應(yīng)的監(jiān)聽(tīng)信息

接著創(chuàng)建聊天的room頁(yè)拘悦,并添加以下內(nèi)容

<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room</title>
</head>
<body>
    <textarea id="chat-log" cols="100" rows="20"></textarea><br/>
    <input id="chat-message-input" type="text" size="100"/><br/>
    <input id="chat-message-submit" type="button" value="Send"/>
</body>
<script>
    var roomName = {{ room_name_json }};

    var chatSocket = new WebSocket(
        'ws://' + window.location.host +
        '/ws/chat/' + roomName + '/');

    chatSocket.onmessage = function(e) {
        var data = JSON.parse(e.data);
        var message = data['message'];
        document.querySelector('#chat-log').value += (message + '\n');
    };

    chatSocket.onclose = function(e) {
        console.error('Chat socket closed unexpectedly');
    };

    document.querySelector('#chat-message-input').focus();
    document.querySelector('#chat-message-input').onkeyup = function(e) {
        if (e.keyCode === 13) {  // enter, return
            document.querySelector('#chat-message-submit').click();
        }
    };

    document.querySelector('#chat-message-submit').onclick = function(e) {
        var messageInputDom = document.querySelector('#chat-message-input');
        var message = messageInputDom.value;
        chatSocket.send(JSON.stringify({
            'message': message
        }));

        messageInputDom.value = '';
    };
</script>
</html>

并在chat的url中添加配置信息

    url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'),

重啟服務(wù)發(fā)現(xiàn)可以通知先前的index頁(yè)面可以進(jìn)入當(dāng)前頁(yè)面,使用F12打開(kāi)審查頁(yè)面橱脸,選擇console頁(yè)础米,會(huì)發(fā)現(xiàn)提示websocket連接異常,這是因?yàn)殡m然在setting中添加了routing配置信息但是實(shí)際文件中尚未完善添诉,接下來(lái)進(jìn)行consumers和routing的完善

在chat模塊下創(chuàng)建consumers文件屁桑,并添加以下內(nèi)容

# chat/consumers.py
from channels.generic.websocket import WebsocketConsumer
import json

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        self.send(text_data=json.dumps({
            'message': message
        }))

現(xiàn)在就已經(jīng)完成一個(gè)簡(jiǎn)單的websocket連接配置,一般來(lái)說(shuō)栏赴,默認(rèn)使用的是同步websocket連接蘑斧,這個(gè)連接會(huì)接收所有的連接,從客戶(hù)端接收信息须眷,同時(shí)將信息同步發(fā)送回客戶(hù)端

需要注意的是竖瘾,對(duì)于channels,也是支持異步功能的花颗,通過(guò)異步也可以獲得更高的性能捕传,但是相應(yīng)的,異步的操作就可能出現(xiàn)阻塞操作扩劝,比如異步訪(fǎng)問(wèn)Django的model的時(shí)候

完成了consumers的編寫(xiě)庸论,最后進(jìn)行routing

的配置职辅,在chat模塊下創(chuàng)建一個(gè)routing文件,添加以下內(nèi)容

# chat/routing.py
from django.conf.urls import url

from . import consumers

websocket_urlpatterns = [
    url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
]

以及修改原本尚未添加配置信息的根路徑的routing文件

 # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),

將上述內(nèi)容添加到application=ProtocolTypeRouter()的括號(hào)中

這樣配置之后聂示,每次發(fā)起連接域携,ProtocolTypeRouter都會(huì)進(jìn)行檢查,如果有websocket連接(ws://或者wss://)鱼喉,就回自動(dòng)調(diào)用AuthMiddlewareStack秀鞭,而AuthMiddlwareStack則會(huì)連接到對(duì)應(yīng)的URLRouter上,此處為跳轉(zhuǎn)進(jìn)入chat.routing.websocket_urlpatterns蒲凶,也就是我們?cè)赾hat模塊中配置的routing中

重啟服務(wù)气筋,開(kāi)啟兩個(gè)頁(yè)面,我們?cè)谄渲幸粋€(gè)頁(yè)面中發(fā)送hello旋圆,可以在另外一個(gè)頁(yè)面的對(duì)話(huà)框中看到之前的頁(yè)面發(fā)送的信息

下面介紹channel中通道層(channel layer),channel layer 屬于通信系統(tǒng)的一種麸恍,他允許多實(shí)例(也是多個(gè)consumers)進(jìn)行交談灵巧,或者與Django的其他部分進(jìn)行交互

channel layer提供以下兩個(gè)概念

  • channel:位于單個(gè)channel中的所有用戶(hù)發(fā)送的信息會(huì)廣播給當(dāng)前channel下的所有用戶(hù)
  • group:多個(gè)channel構(gòu)成的組,擁有組名稱(chēng)的用戶(hù)可以添加/刪除組抹沪,同時(shí)發(fā)送的信息廣播給當(dāng)前組的所有頻道的所有用戶(hù)刻肄,需要注意的是組內(nèi)只能廣播無(wú)法指定某一channel進(jìn)行單播

每一個(gè)consumers都有一個(gè)唯一的channel,并且可以通過(guò)這個(gè)channel進(jìn)行與channel layer的通訊融欧,在這個(gè)聊天服務(wù)器上希望能夠多個(gè)實(shí)例之間互相通訊敏弃,所以我們將其添加到基于房間名的組中,通過(guò)組進(jìn)行多個(gè)實(shí)例互相交互

在修改consumers文件之間我們先完成Redis的配置噪馏,一般來(lái)說(shuō)channel使用Redis作為存儲(chǔ)層

在安裝完畢Redis并成功開(kāi)啟服務(wù)之后向setting文件添加以下內(nèi)容

# mysite/settings.py
# Channels
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

配置完成之后修改chat模塊下的consumers文件麦到,添加以下內(nèi)容:

# chat/consumers.py
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import json

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))

這樣,當(dāng)用戶(hù)發(fā)送消息時(shí)欠肾,js通過(guò)websocket將消息傳輸?shù)紺hatconsumer瓶颠,chatconsumer將消息轉(zhuǎn)發(fā)到對(duì)應(yīng)的組,組內(nèi)的每一個(gè)chatconsumer接收消息并通過(guò)websocket轉(zhuǎn)發(fā)回客戶(hù)端刺桃,同步添加到聊天日志中

//部分代碼解釋
self.scope['url']['kwargs']['room_name']:通過(guò)解包url獲取傳入的關(guān)鍵詞的信息粹淋,對(duì)于每一個(gè)consumer實(shí)例而言,都擁有一個(gè)scope瑟慈,在這個(gè)scope中包含了此次連接的信息桃移,包含url的位置以及關(guān)鍵詞
self.room_group_name = 'chat_%s'%self.room_name:使用用戶(hù)的房間名稱(chēng)直接命名組名稱(chēng),不進(jìn)行轉(zhuǎn)義
async_to_sync(self.channel_layer.group_add)(...):1葛碧、加入一個(gè)組中借杰;2、async_to_sync 這個(gè)裝飾器是必須的因?yàn)閏hatconsumer是一個(gè)同步websocket連接吹埠,但是對(duì)于channel layer需要調(diào)用異步方法第步,因?yàn)樗械腸hannel layer都需要異步實(shí)現(xiàn)疮装;3、組名字僅限于A(yíng)SCII字母數(shù)字和字符句點(diǎn)
self.accept():接受websocket連接粘都,如果該方法并非在connect()中被調(diào)用廓推,則會(huì)發(fā)生拒絕并關(guān)閉連接
async_to_sync(self.channel_layer.group_discard)(...):關(guān)閉組
async_to_sync(self.channel_layer.group_send):1、發(fā)送一個(gè)event到組翩隧;2踪栋、在這個(gè)event中一般會(huì)定義的第一個(gè)key:value組為type,value鎖指向的就是調(diào)用的方法

接下來(lái)將consumer由同步改寫(xiě)成異步刨疼,提高性能

之前在chatconsumer中所使用的是同步方式狰挡,這個(gè)方式可以調(diào)用常規(guī)的IO函數(shù),但是對(duì)于異步而言淑仆,可以具有更高的性能涝婉,并且不需要?jiǎng)?chuàng)建新的線(xiàn)程;對(duì)于在這個(gè)例子中的chatconsumer而言蔗怠,并不訪(fǎng)問(wèn)同步Django model墩弯,僅僅使用了 async-native 庫(kù),不會(huì)因?yàn)橹貙?xiě)為異步出現(xiàn)復(fù)雜的情況寞射,但是渔工,就算是訪(fǎng)問(wèn)了Django model或者其他同步代碼,也可以重寫(xiě)為異步桥温,然后調(diào)用sync_to_async(與async_to_sync位于同一個(gè)模塊下)以同步狀態(tài)調(diào)用異步代碼引矩,但是性能提升沒(méi)有這個(gè)例子的高

重寫(xiě)consumer,添加以下內(nèi)容

# chat/consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )

        await self.accept()

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )

    # Receive message from room group
    async def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message
        }))
對(duì)比之前的原始代碼可以發(fā)現(xiàn)
  • 現(xiàn)在chatconsumer繼承自AsyncWebsocketConsumer
    而不是WebsocketConsumer
  • 所有的方法都是async def而不是def了
  • await用來(lái)調(diào)用異步的IO函數(shù)
  • 在channel layer上調(diào)用方法時(shí)不再需要async_to_sync了
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侵浸,一起剝皮案震驚了整個(gè)濱河市旺韭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌通惫,老刑警劉巖茂翔,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異履腋,居然都是意外死亡珊燎,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)遵湖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)悔政,“玉大人,你說(shuō)我怎么就攤上這事延旧∧惫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵迁沫,是天一觀(guān)的道長(zhǎng)芦瘾。 經(jīng)常有香客問(wèn)我捌蚊,道長(zhǎng),這世上最難降的妖魔是什么近弟? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任缅糟,我火速辦了婚禮,結(jié)果婚禮上祷愉,老公的妹妹穿的比我還像新娘窗宦。我一直安慰自己,他們只是感情好二鳄,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布赴涵。 她就那樣靜靜地躺著,像睡著了一般订讼。 火紅的嫁衣襯著肌膚如雪髓窜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天欺殿,我揣著相機(jī)與錄音纱烘,去河邊找鬼。 笑死祈餐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哄陶。 我是一名探鬼主播帆阳,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼屋吨!你這毒婦竟也來(lái)了蜒谤?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤至扰,失蹤者是張志新(化名)和其女友劉穎鳍徽,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體敢课,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阶祭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了直秆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片濒募。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖圾结,靈堂內(nèi)的尸體忽然破棺而出瑰剃,到底是詐尸還是另有隱情,我是刑警寧澤筝野,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布晌姚,位于F島的核電站粤剧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏挥唠。R本人自食惡果不足惜抵恋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猛遍。 院中可真熱鬧馋记,春花似錦、人聲如沸懊烤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腌紧。三九已至茸习,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壁肋,已是汗流浹背号胚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浸遗,地道東北人猫胁。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像跛锌,于是被迫代替她去往敵國(guó)和親弃秆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345