官方文檔鏈接: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了