Django Channels 官方文檔
https://channels.readthedocs.io/en/latest/index.html
前言:
最近課程設計需要用到 WebSocket,而原生的 Django 又不支持 WebSocket,僅有 Django Channels 庫支持 WebSocket,但是 Django Channels 的資料,特別是中文資料異常稀缺,因此我在自己理解的基礎上,整理翻譯了這一篇 官方入門教程,僅作參考,如有疑問可以在下方留言。感謝大家的查看!
Tutorial
教程
Channels allows you to use WebSockets and other non-HTTP protocols in your Django site. For example you might want to use WebSockets to allow a page on your site to immediately receive updates from your Django server without using HTTP long-polling or other expensive techniques.
Channels 允許您在 Django 站點中使用 Websockets 和其他非 HTTP 協議。例如, 您可能希望 Websockets 允許網站上的頁面立即從 Django 服務器接收更新, 而無需使用 HTTP 長輪詢或其他昂貴的技術。
In this tutorial we will build a simple chat server, where you can join an online room, post messages to the room, and have others in the same room see those messages immediately.
在本教程中, 我們將建立一個簡單的聊天服務器, 在那里你可以加入一個在線房間, 張貼消息到房間, 並讓其他人在同一房間看到這些消息立即。
- Tutorial Part 1: Basic Setup
- 教程1部分: 基本設置
- Tutorial Part 2: Implement a Chat Server
- 教程2部分: 實現聊天服務器
- Tutorial Part 3: Rewrite Chat Server as Asynchronous
- 教程3部分: 將聊天服務器重寫為異步
- Tutorial Part 4: Automated Testing
- 教程4部分: 自動化測試
Tutorial Part 1: Basic Setup
教程1部分: 基本設置
In this tutorial we will build a simple chat server. It will have two pages:
在本教程中, 我們將構建一個簡單的聊天服務器。它將有兩個頁面:
- An index view that lets you type the name of a chat room to join.
- 一個 index 視圖, 用於輸入要加入的聊天室的名稱。
- A room view that lets you see messages posted in a particular chat room.
- 一個可以查看在特定聊天室中發送的消息的房間視圖。
The room view will use a WebSocket to communicate with the Django server and listen for any messages that are posted.
房間視圖將使用 WebSocket 與 Django 服務器進行通信, 並監聽任何發送出來的消息。
We assume that you are familar with basic concepts for building a Django site. If not we recommend you complete the Django tutorial first and then come back to this tutorial.
我們假設您熟悉構建 Django 站點的基本概念。如果不是, 我們建議您先完成 Django 教程, 然后再回到本教程。
We assume that you have Django installed already. You can tell Django is installed and which version by running the following command in a shell prompt (indicated by the $ prefix):
我們假設你已經安裝了 Django。您可以通過在 shell 提示符下運行以下命令 (用 $ 前綴表示) 來查看 您安裝的 Django 版本:
$ python3 -m django --version
We also assume that you have Channels installed already. You can tell Channels is installed by running the following command:
我們還假設您已經安裝了 Channels。您可以通過運行以下命令來查看 Channels 安裝與否:
$ python3 -c 'import channels; print(channels.__version__)'
This tutorial is written for Channels 2.0, which supports Python 3.5+ and Django 1.11+. If the Channels version does not match, you can refer to the tutorial for your version of Channels by using the version switcher at the bottom left corner of this page, or update Channels to the newest version.
本教程是為 Channels 2.0 編寫的, 它支持 Python 3.5 + 和 Django 1.11 +。如果 Channels 版本不匹配, 你可以使用本頁左下角的版本切換器, 或將 Channels 更新到最新版本, 以參考您的 Channels 版本的教程。
This tutorial also uses Docker to install and run Redis. We use Redis as the backing store for the channel layer, which is an optional component of the Channels library that we use in the tutorial. Install Docker from its official website - there are official runtimes for Mac OS and Windows that make it easy to use, and packages for many Linux distributions where it can run natively.
本教程還使用 Docker 安裝和運行 Redis。我們使用 Redis 作為 Channels 層的后備存儲, 它是我們在教程中使用的 Channels 庫的可選組件。從其官方網站安裝 Docker --有用於 Mac OS 和 Windows 的易於使用的正式運行版, 並為許多 Linux 發行版提供了可本地運行的軟件包。
Note
提醒
While you can run the standard Django runserver without the need for Docker, the channels features we’ll be using in later parts of the tutorial will need Redis to run, and we recommend Docker as the easiest way to do this.
雖然您可以運行標准的 Django runserver 不需要 Docker , 我們將使用的 channels 功能在后面的教程將需要 Redis 運行, 我們建議使用 Docker 這一最簡單的方式來做到這一點。
Creating a project
新建一個項目
If you don’t already have a Django project, you will need to create one.
如果您還沒有 Django 項目, 您將需要創建一個。
From the command line, cd into a directory where you’d like to store your code, then run the following command:
從命令行, 將 cd 放入要存儲代碼的目錄中, 然后運行以下命令:
$ django-admin startproject mysite
This will create a mysite directory in your current directory with the following contents:
這將在當前目錄中創建一個 mysite 目錄, 其中有以下內容:
mysite/ manage.py mysite/ __init__.py settings.py urls.py wsgi.py
Creating the Chat app
創建聊天應用程序
We will put the code for the chat server in its own app.
我們會將聊天服務器的代碼放在它自己的應用程序中。
Make sure you’re in the same directory as manage.py and type this command:
請確保您位於與 manage.py 相同的目錄中. 然后輸入以下命令:
$ python3 manage.py startapp chat
That’ll create a directory chat, which is laid out like this:
這將創建一個 chat 文件夾, 它是像這樣的:
chat/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py
For the purposes of this tutorial, we will only be working with chat/views.py and chat/__init__.py. So remove all other files from the chat directory.
為了達到本教程的目的, 我們將只使用 chat/views.py 和 chat/__init__.py。因此, 從 chat 目錄中刪除所有其他文件。
After removing unnecessary files, the chat directory should look like:
刪除不必要的文件后, chat 目錄應如下所示:
chat/ __init__.py views.py
We need to tell our project that the chat app is installed. Edit the mysite/settings.py file and add 'chat' to the INSTALLED_APPS setting. It’ll look like this:
我們需要告訴我們的項目 chat app 已經安裝。編輯 mysite/settings.py 文件並將 'chat' 添加到 INSTALLED_APPS 設置中。它看起來像這樣:
# mysite/settings.py INSTALLED_APPS = [ 'chat', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
Add the index view
添加 index 視圖
We will now create the first view, an index view that lets you type the name of a chat room to join.
現在, 我們將創建第一個視圖, 這個 index 視圖允許您輸入要加入的聊天室的名稱。
Create a templates directory in your chat directory. Within the templates directory you have just created, create another directory called chat, and within that create a file called index.html to hold the template for the index view.
在 chat 目錄中創建 templates 目錄。在剛剛創建的 templates 目錄中, 創建另一個名為 chat 的目錄, 並在其中創建一個名為 index.html 的文件。
Your chat directory should now look like:
您的 chat 目錄現在應該看起來像:
chat/ __init__.py templates/ chat/ index.html views.py
Put the following code in chat/templates/chat/index.html:
將下面的代碼寫進 chat/templates/chat/index.html 文件中:
<!-- 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"/> </body> <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> </html>
Create the view function for the room view. Put the following code in chat/views.py:
為 room 視圖創建視圖函數。將下面的代碼寫進 chat/views.py 文件中:
# chat/views.py from django.shortcuts import render def index(request): return render(request, 'chat/index.html', {})
To call the view, we need to map it to a URL - and for this we need a URLconf.
為了調用這個視圖,我們需要把它映射到一個 URL -- 因此我們需要一個 URL 配置文件。
To create a URLconf in the chat directory, create a file called urls.py. Your app directory should now look like:
為了在 chat 目錄下創建一個 URL 配置文件,我們需要新建一個名為 urls.py 的文件。你的 app 目錄應該像現在這樣子:
chat/ __init__.py templates/ chat/ index.html urls.py views.py
In the chat/urls.py file include the following code:
在 chat/urls.py 文件中包含以下代碼:
# chat/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name='index'), ]
The next step is to point the root URLconf at the chat.urls module. In mysite/urls.py, add an import for django.conf.urls.include and insert an include() in the urlpatterns list, so you have:
下一步是將根目錄下的 URLconf 文件指向 chat.urls 模塊。在 mysite/urls.py 中, 導入 django.conf.urls.include模塊,並在 urlpatterns 列表中插入一個 include() 函數, 因此您需要寫入以下代碼:
# 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), ]
Let’s verify that the index view works. Run the following command:
讓我們驗證 index 視圖是否有效。運行以下命令:
$ python3 manage.py runserver
You’ll see the following output on the command line:
您將在命令行中看到以下輸出:
Performing system checks... System check identified no issues (0 silenced). You have 13 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. February 18, 2018 - 22:08:39 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.
Note
提醒
Ignore the warning about unapplied database migrations. We won’t be using a database in this tutorial.
忽略有關未應用數據庫遷移的警告。我們將不會在本教程中使用數據庫。
Go to http://127.0.0.1:8000/chat/ in your browser and you should see the text “What chat room would you like to enter?” along with a text input to provide a room name.
在瀏覽器中轉到 http://127.0.0.1:8000/chat/, 您應該看到文本 "What chat room would you like to enter?" 以及一個用於輸入房間名字的文本輸入框。
Type in “lobby” as the room name and press enter. You should be redirected to the room view at http://127.0.0.1:8000/chat/lobby/ but we haven’t written the room view yet, so you’ll get a “Page not found” error page.
輸入 "lobby" 作為房間名稱, 然后按 enter 鍵。你應該被重定向到 http://127.0.0.1:8000/chat/lobby/的房間視圖, 但我們還沒有寫的房間視圖, 所以你會得到一個 "頁面找不到" 錯誤頁面。
Go to the terminal where you ran the runserver command and press Control-C to stop the server.
轉到運行 runserver 命令的終端, 然后按下 Control+C 以停止服務器。
Integrate the Channels library
集成 Channels 庫
So far we’ve just created a regular Django app; we haven’t used the Channels library at all. Now it’s time to integrate Channels.
到目前為止, 我們剛剛創建了一個常規的 Django 應用程序;我們根本就沒有使用 Channels 庫。現在是時候集成 Channels 庫了。
Let’s start by creating a root routing configuration for Channels. A Channels routing configuration is similar to a Django URLconf in that it tells Channels what code to run when an HTTP request is received by the Channels server.
讓我們從創建 Channels 的根路由配置文件開始。Channels 路由配置類似於 Django URLconf,它會告訴 Channels 當收到由 Channels 服務器發過來的 HTTP 請求時,應該執行什么代碼。
We’ll start with an empty routing configuration. Create a file mysite/routing.py and include the following code:
我們將從一個空的路由配置文件開始。創建文件 mysite/routing.py, 並寫入以下代碼:
# mysite/routing.py from channels.routing import ProtocolTypeRouter application = ProtocolTypeRouter({ # (http->django views is added by default) })
Now add the Channels library to the list of installed apps. Edit the mysite/settings.py file and add 'channels' to the INSTALLED_APPS setting. It’ll look like this:
現在, 將 Channels 庫添加到已安裝的應用程序列表中。編輯 mysite/settings.py 文件並將 'channels' 添加到 INSTALLED_APPS 設置。它看起來像這樣:
# mysite/settings.py INSTALLED_APPS = [ 'channels', 'chat', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ]
You’ll also need to point Channels at the root routing configuration. Edit the mysite/settings.py file again and add the following to the bottom of it:
您同樣需要在根路由配置中指向 Channels。再次編輯 mysite/settings.py 文件, 並將以下內容添加到底部:
# mysite/settings.py # Channels ASGI_APPLICATION = 'mysite.routing.application'
With Channels now in the installed apps, it will take control of the runserver command, replacing the standard Django development server with the Channels development server.
現在已安裝的應用程序中有 Channels, 它將控制 runserver 命令, 用 Channels 開發服務器替換標准的 Django 開發服務器。
Note
提醒
The Channels development server will conflict with any other third-party apps that require an overloaded or replacement runserver command. An example of such a conflict is with whitenoise.runserver_nostatic from whitenoise. In order to solve such issues, try moving channels to the top of your INSTALLED_APPS or remove the offending app altogether.
Channels 開發服務器將與需要重載或替換 runserver 命令的任何其他第三方應用程序沖突。whitenoise 中的 whitenoise.runserver_nostatic是一個沖突的例子。為了解決這些問題, 請嘗試將 Channels 移動到您的 INSTALLED_APPS 的頂部, 或者完全刪除與其發生沖突的應用程序。
Let’s ensure that the Channels development server is working correctly. Run the following command:
讓我們確保 Channels 開發服務器工作正常。運行以下命令:
$ python3 manage.py runserver
You’ll see the following output on the command line:
您將在命令行中看到以下輸出:
Performing system checks... System check identified no issues (0 silenced). You have 13 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions. Run 'python manage.py migrate' to apply them. February 18, 2018 - 22:16:23 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
Note
提醒
Ignore the warning about unapplied database migrations. We won’t be using a database in this tutorial.
忽略有關未應用數據庫遷移的警告。我們將不會在本教程中使用數據庫。
Notice the line beginning with Starting ASGI/Channels development server at http://127.0.0.1:8000/. This indicates that the Channels development server has taken over from the Django development server.
留意從 Starting ASGI/Channels development server at http://127.0.0.1:8000/ 開始的內容。這表明 Channels 開發服務器已接管了 Django 開發服務器。
Go to http://127.0.0.1:8000/chat/ in your browser and you should still see the index page that we created before.
在瀏覽器中轉到 http://127.0.0.1:8000/chat/, 您仍然應該看到我們以前創建的 index 頁面。
Go to the terminal where you ran the runserver command and press Control-C to stop the server.
轉到運行 runserver 命令的終端, 然后按下 Control+C 以停止服務器。
Tutorial Part 2: Implement a Chat Server
教程2部分: 實現聊天服務器
This tutorial begins where Tutorial 1 left off. We’ll get the room page working so that you can chat with yourself and others in the same room.
本教程在教程1的基礎上開始。我們會讓房間頁面工作, 這樣你可以和你自己或者其他人在同一個房間里聊天。
Add the room view
添加房間視圖
We will now create the second view, a room view that lets you see messages posted in a particular chat room.
現在, 我們將創建第二個視圖, 即一個允許您查看在特定聊天室中發布消息的房間視圖。
Create a new file chat/templates/chat/room.html. Your app directory should now look like:
創建新的文件 chat/templates/chat/room.html。您的應用程序目錄現在應該看起來像:
chat/ __init__.py templates/ chat/ index.html room.html urls.py views.py
Create the view template for the room view in chat/templates/chat/room.html:
在 chat/templates/chat/room.html 中填入一下代碼:
<!-- 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>
Create the view function for the room view in chat/views.py. Add the imports of mark_safe and json and add the room view function:
在 chat/views.py 中為房間視圖創建視圖函數。添加導入 mark_safe 和 json 模塊, 並添加 房間視圖的視圖函數:
# chat/views.py from django.shortcuts import render from django.utils.safestring import mark_safe import json def index(request): return render(request, 'chat/index.html', {}) def room(request, room_name): return render(request, 'chat/room.html', { 'room_name_json': mark_safe(json.dumps(room_name)) })
Create the route for the room view in chat/urls.py:
在 chat/urls.py 中創建房間視圖的路由:
# chat/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'), ]
Start the Channels development server:
啟動 Channels 開發服務器:
$ python3 manage.py runserver
Go to http://127.0.0.1:8000/chat/ in your browser and to see the index page.
在瀏覽器中轉到 http://127.0.0.1:8000/chat/並查看 index 頁面。
Type in “lobby” as the room name and press enter. You should be redirected to the room page at http://127.0.0.1:8000/chat/lobby/ which now displays an empty chat log.
輸入 “lobby” 作為房間名稱, 然后按 enter 鍵。您將會重定向到 http://127.0.0.1:8000/chat/lobby/, 該頁面現在顯示一個空的聊天日志。
Type the message “hello” and press enter. Nothing happens. In particular the message does not appear in the chat log. Why?
鍵入消息 "hello", 然后按 enter 鍵。什么也沒有發生。尤其是,消息並不會出現在聊天日志中。為什么?
The room view is trying to open a WebSocket to the URL ws://127.0.0.1:8000/ws/chat/lobby/ but we haven’t created a consumer that accepts WebSocket connections yet. If you open your browser’s JavaScript console, you should see an error that looks like:
房間視圖試圖打開一個 WebSocket 連接到 URL ws://127.0.0.1:8000/ws/chat/lobby/,但我們還沒有創建一個接受 WebSocket 連接的 consumer。如果打開瀏覽器的 JavaScript 控制台, 您應該會看到如下所示的錯誤:
WebSocket connection to 'ws://127.0.0.1:8000/ws/chat/lobby/' failed: Unexpected response code: 500
Write your first consumer
編寫您的第一個用戶
When Django accepts an HTTP request, it consults the root URLconf to lookup a view function, and then calls the view function to handle the request. Similarly, when Channels accepts a WebSocket connection, it consults the root routing configuration to lookup a consumer, and then calls various functions on the consumer to handle events from the connection.
當 Django 接受 HTTP 請求時, 它會根據根 URLconf 以查找視圖函數, 然后調用視圖函數來處理請求。同樣, 當 Channels 接受 WebSocket 連接時, 它會根據根路由配置以查找對應的 consumer, 然后調用 consumer 上的各種函數來處理來自這個連接的事件。
We will write a basic consumer that accepts WebSocket connections on the path /ws/chat/ROOM_NAME/ that takes any message it receives on the WebSocket and echos it back to the same WebSocket.
我們將編寫一個簡單的 consumer, 它會在路徑 /ws/chat/ROOM_NAME/ 接收 WebSocket 連接,然后把接收任意的消息, 回送給同一個 WebSocket 連接。
Note
提醒
It is good practice to use a common path prefix like /ws/ to distinguish WebSocket connections from ordinary HTTP connections because it will make deploying Channels to a production environment in certain configurations easier.
使用常見的路徑前綴 (如/ws) 來區分 WebSocket 連接與普通 HTTP 連接是很好的做法, 因為它將使在某些配置中部署 Channels 更容易。
In particular for large sites it will be possible to configure a production-grade HTTP server like nginx to route requests based on path to either a production-grade WSGI server like Gunicorn+Django for ordinary HTTP requests or a production-grade ASGI server like Daphne+Channels for WebSocket requests.
特別是大型網站, 它們很有可能配置像 nginx 這樣的生產級別 HTTP 服務器,根據路徑將請求發送到生產級別的 WSGI 服務器,例如用於處理普通 HTTP 請求的 Gunicorn + Django, 或生產級別的 ASGI 服務器,例如用於處理 WebSocket 請求的 Daphne + Channels。
Note that for smaller sites you can use a simpler deployment strategy where Daphne serves all requests - HTTP and WebSocket - rather than having a separate WSGI server. In this deployment configuration no common path prefix like is /ws/ is necessary.
請注意, 對於較小的站點, 您可以使用更簡單的部署策略, 其中 Daphne 服務器處理所有的請求--HTTP 和 WebSocket--而不是單獨的 WSGI 服務器。在這種部署配置策略中, 不需要使用 /ws/ 這樣的通用路徑前綴。
Create a new file chat/consumers.py. Your app directory should now look like:
創建新文件 chat/consumers.py。您的應用程序目錄現在應該看起來像:
chat/ __init__.py consumers.py templates/ chat/ index.html room.html urls.py views.py
Put the following code in chat/consumers.py:
在 chat/consumers.py 中寫入以下代碼:
# 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 }))
This is a synchronous WebSocket consumer that accepts all connections, receives messages from its client, and echos those messages back to the same client. For now it does not broadcast messages to other clients in the same room.
這是一個同步 WebSocket consumer, 它接受所有連接, 接收來自其客戶端的消息, 並將這些消息回送到同一客戶端。現在, 它不向同一個房間的其他客戶端廣播消息。
Note
提醒
Channels also supports writing asynchronous consumers for greater performance. However any asynchronous consumer must be careful to avoid directly performing blocking operations, such as accessing a Django model. See the Consumers reference for more information about writing asynchronous consumers.
Channels 還支持編寫異步 consumers 以提高性能。但是, 任何異步 consumers 都必須小心, 避免直接執行阻塞操作, 例如訪問 Django 的 model。有關編寫異步 consumers 的詳細信息, 請參閱 Consumers。
We need to create a routing configuration for the chat app that has a route to the consumer. Create a new file chat/routing.py. Your app directory should now look like:
我們需要為 chat app 創建一個路由配置, 它有一個通往 consumer 的路由。創建新文件 chat/routing.py。您的應用程序目錄現在應該看起來像:
chat/ __init__.py consumers.py routing.py templates/ chat/ index.html room.html urls.py views.py
Put the following code in chat/routing.py:
在 chat/routing.py 中輸入以下代碼:
# chat/routing.py from django.conf.urls import url from . import consumers websocket_urlpatterns = [ url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer), ]
The next step is to point the root routing configuration at the chat.routing module. In mysite/routing.py, import AuthMiddlewareStack, URLRouter, and chat.routing; and insert a 'websocket' key in the ProtocolTypeRouter list in the following format:
下一步是將根路由指向 chat.routing 模塊。在 mysite/routing.py 中, 導入 AuthMiddlewareStack、URLRouter 和 chat.routing ;並在 ProtocolTypeRouter 列表中插入一個 "websocket" 鍵, 格式如下:
# mysite/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import chat.routing application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
This root routing configuration specifies that when a connection is made to the Channels development server, the ProtocolTypeRouter will first inspect the type of connection. If it is a WebSocket connection (ws:// or wss://), the connection will be given to the AuthMiddlewareStack.
這個根路由配置指定,當與 Channels 開發服務器建立連接的時候, ProtocolTypeRouter 將首先檢查連接的類型。如果是 WebSocket 連接 (ws://或 wss://), 則連接會交給 AuthMiddlewareStack。
The AuthMiddlewareStack will populate the connection’s scope with a reference to the currently authenticated user, similar to how Django’s AuthenticationMiddleware populates the request object of a view function with the currently authenticated user. (Scopes will be discussed later in this tutorial.) Then the connection will be given to the URLRouter.
AuthMiddlewareStack 將使用對當前經過身份驗證的用戶的引用來填充連接的 scope, 類似於 Django 的 AuthenticationMiddleware 用當前經過身份驗證的用戶填充視圖函數的請求對象。(Scopes 將在本教程后面討論。)然后連接將被給到 URLRouter。
The URLRouter will examine the HTTP path of the connection to route it to a particular consumer, based on the provided url patterns.
根據提供的 url 模式, URLRouter 將檢查連接的 HTTP 路徑, 以將其路由指定到到特定的 consumer。
Let’s verify that the consumer for the /ws/chat/ROOM_NAME/ path works. Start the Channels development server:
讓我們驗證 consumer 的 /ws/chat/ROOM_NAME/ 路徑是否工作。啟動 Channels 開發服務器:
$ python3 manage.py runserver
Go to the room page at http://127.0.0.1:8000/chat/lobby/ which now displays an empty chat log.
轉到 http://127.0.0.1:8000/chat/lobby/ 中的 房間頁面, 該頁現在顯示一個空的聊天日志。
Type the message “hello” and press enter. You should now see “hello” echoed in the chat log.
輸入消息 "hello", 然后按 enter 鍵。您現在應該看到 "hello" 在聊天日志中顯示。
However if you open a second browser tab to the same room page at http://127.0.0.1:8000/chat/lobby/ and type in a message, the message will not appear in the first tab. For that to work, we need to have multiple instances of the same ChatConsumer be able to talk to each other. Channels provides a channel layer abstraction that enables this kind of communication between consumers.
但是, 如果您打開第二個瀏覽器選項卡輸入 http://127.0.0.1:8000/chat/lobby/進入同一房間頁面上並輸入消息, 則消息並不會出現在第一個選項卡中。為了做到這一點, 我們需要有多個相同 ChatConsumer 實例才能互相交談。Channels 提供了一種 channel layer 抽象, 使 consumers 之間能夠進行這種通信。
Go to the terminal where you ran the runserver command and press Control-C to stop the server.
轉到運行 runserver 命令的終端, 然后按下 Control+C 以停止服務器。
Enable a channel layer
啟用 channel layer
A channel layer is a kind of communication system. It allows multiple consumer instances to talk with each other, and with other parts of Django.
channel layer 是一種通信系統。它允許多個 consumer 實例互相交談, 以及與 Django 的其他部分進行通信。
A channel layer provides the following abstractions:
channel layer 提供以下抽象:
- A channel is a mailbox where messages can be sent to. Each channel has a name. Anyone who has the name of a channel can send a message to the channel.
- channel 是可以發送消息的郵箱。每個 channel 都有一個名稱。任何有名稱的 channel 都可以向 channel 發送消息。
- A group is a group of related channels. A group has a name. Anyone who has the name of a group can add/remove a channel to the group by name and send a message to all channels in the group. It is not possible to enumerate what channels are in a particular group.
- group 是一組相關的 channels。group 具有名稱。任何具有名字的 group 都可以按名稱向 group 中添加/刪除 channel, 也可以向 group 中的所有 channel 發送消息。無法列舉特定 group 中的 channel。
Every consumer instance has an automatically generated unique channel name, and so can be communicated with via a channel layer.
每個 consumer 實例都有一個自動生成的唯一的 channel 名稱, 因此可以通過 channel layer 進行通信。
In our chat application we want to have multiple instances of ChatConsumer in the same room communicate with each other. To do that we will have each ChatConsumer add its channel to a group whose name is based on the room name. That will allow ChatConsumers to transmit messages to all other ChatConsumers in the same room.
在我們的聊天應用程序中, 我們希望在同一房間中有多個 ChatConsumer 的實例相互通信。要做到這一點, 我們將有每個 ChatConsumer 添加它的 channel 到一個 group, 其名稱是基於房間的名稱。這將允許 ChatConsumers 將消息傳輸到同一個房間中的所有其他 ChatConsumers。
We will use a channel layer that uses Redis as its backing store. To start a Redis server on port 6379, run the following command:
我們將使用一個 channel layer, 使用 Redis 作為其后備存儲。要在端口6379上啟動 Redis 服務器, 請運行以下命令:
$ docker run -p 6379:6379 -d redis:2.8
We need to install channels_redis so that Channels knows how to interface with Redis. Run the following command:
我們需要安裝 channels_redis, 以便 Channels 知道如何調用 redis。運行以下命令:
$ pip3 install channels_redis
Before we can use a channel layer, we must configure it. Edit the mysite/settings.py file and add a CHANNEL_LAYERS setting to the bottom. It should look like:
在使用 channel layer 之前, 必須對其進行配置。編輯 mysite/settings.py 文件並將 CHANNEL_LAYERS 設置添加到底部。它應該看起來像:
# mysite/settings.py # Channels ASGI_APPLICATION = 'mysite.routing.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }
Note
提醒
It is possible to have multiple channel layers configured. However most projects will just use a single 'default' channel layer.
可以配置多個 channel layer。然而, 大多數項目只使用一個 "默認" 的 channel layer。
Let’s make sure that the channel layer can communicate with Redis. Open a Django shell and run the following commands:
讓我們確保 channel layer 可以與 Redis 通信。打開 Django shell 並運行以下命令:
$ python3 manage.py shell >>> import channels.layers >>> channel_layer = channels.layers.get_channel_layer() >>> from asgiref.sync import async_to_sync >>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'}) >>> async_to_sync(channel_layer.receive)('test_channel') {'type': 'hello'}
Type Control-D to exit the Django shell.
輸入 Control+D 退出 Django shell。
Now that we have a channel layer, let’s use it in ChatConsumer. Put the following code in chat/consumers.py, replacing the old code:
現在我們有了一個 channel layer, 讓我們在 ChatConsumer 中使用它。將以下代碼放在 chat/consumers.py 中, 替換舊代碼:
# 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 }))
When a user posts a message, a JavaScript function will transmit the message over WebSocket to a ChatConsumer. The ChatConsumer will receive that message and forward it to the group corresponding to the room name. Every ChatConsumer in the same group (and thus in the same room) will then receive the message from the group and forward it over WebSocket back to JavaScript, where it will be appended to the chat log.
當用戶發布消息時, JavaScript 函數將通過 WebSocket 將消息傳輸到 ChatConsumer。ChatConsumer 將接收該消息並將其轉發到與房間名稱對應的 group。在同一 group 中的每個 ChatConsumer (並因此在同一個房間中) 將接收來自該 group 的消息, 通過 WebSocket 將其轉發並返回到 JavaScript, 它將會追加到聊天日志中。
Several parts of the new ChatConsumer code deserve further explanation:
新的 ChatConsumer 代碼中有幾個部分需要進一步解釋:
- self.scope[‘url_route’][‘kwargs’][‘room_name’]
- Obtains the 'room_name' parameter from the URL route in chat/routes.py that opened the WebSocket connection to the consumer.
- 從給 consumer 打開 WebSocket 連接的 chat/routes.py 中的 URL 路由中獲取 "room_name" 參數。
- Every consumer has a scope that contains information about its connection, including in particular any positional or keyword arguments from the URL route and the currently authenticated user if any.
- 每個 consumer 都有一個 scope, 其中包含有關其連接的信息, 特別是來自 URL 路由和當前經過身份驗證的用戶 (如果有的話) 中的任何位置或關鍵字參數。
- self.room_group_name = ‘chat_%s’ % self.room_name
- Constructs a Channels group name directly from the user-specified room name, without any quoting or escaping.
- 直接從用戶指定的房間名稱構造一個 Channels group 名稱, 無需任何引用或轉義。
- Group names may only contain letters, digits, hyphens, and periods. Therefore this example code will fail on room names that have other characters.
- 組名可能只包含字母、數字、連字符和句點。因此, 此示例代碼將在具有其他字符的房間名稱上發生失敗。
- async_to_sync(self.channel_layer.group_add)(…)
- Joins a group.
- 加入一個 group。
- The async_to_sync(…) wrapper is required because ChatConsumer is a synchronous WebsocketConsumer but it is calling an asynchronous channel layer method. (All channel layer methods are asynchronous.)
- async_to_sync(…) wrapper 是必需的, 因為 ChatConsumer 是同步 WebsocketConsumer, 但它調用的是異步 channel layer 方法。(所有 channel layer 方法都是異步的)
- Group names are restricted to ASCII alphanumerics, hyphens, and periods only. Since this code constructs a group name directly from the room name, it will fail if the room name contains any characters that aren’t valid in a group name.
- group 名稱僅限於 ASCII 字母、連字符和句點。由於此代碼直接從房間名稱構造 group 名稱, 因此如果房間名稱中包含的其他無效的字符, 代碼運行則會失敗。
- self.accept()
- Accepts the WebSocket connection.
- 接收 WebSocket 連接。
- If you do not call accept() within the connect() method then the connection will be rejected and closed. You might want to reject a connection for example because the requesting user is not authorized to perform the requested action.
- 如果你在 connect() 方法中不調用 accept(), 則連接將被拒絕並關閉。例如,您可能希望拒絕連接, 因為請求的用戶未被授權執行請求的操作。
- It is recommended that accept() be called as the last action in connect() if you choose to accept the connection.
- 如果你選擇接收連接, 建議 accept() 作為在 connect() 方法中的最后一個操作。
- async_to_sync(self.channel_layer.group_discard)(…)
- Leaves a group.
- 離開一個 group。
- async_to_sync(self.channel_layer.group_send)
- Sends an event to a group.
- 將 event 發送到一個 group。
- An event has a special 'type' key corresponding to the name of the method that should be invoked on consumers that receive the event.
- event 具有一個特殊的鍵 'type' 對應接收 event 的 consumers 調用的方法的名稱。
Let’s verify that the new consumer for the /ws/chat/ROOM_NAME/ path works. To start the Channels development server, run the following command:
讓我們驗證新 consumer 的 /ws/chat/ROOM_NAME/ 路徑是否工作。要啟動 Channels 開發服務器, 請運行以下命令:
$ python3 manage.py runserver
Open a browser tab to the room page at http://127.0.0.1:8000/chat/lobby/. Open a second browser tab to the same room page.
打開瀏覽器選項卡到 http://127.0.0.1:8000/chat/lobby/的房間頁面。打開另一個瀏覽器選項卡到同一個房間頁面。
In the second browser tab, type the message “hello” and press enter. You should now see “hello” echoed in the chat log in both the second browser tab and in the first browser tab.
在第二個瀏覽器選項卡中, 輸入消息 "hello", 然后按 enter 鍵。在第二個瀏覽器選項卡和第一個瀏覽器選項卡中, 您現在應該看到 "hello" 在聊天日志中顯示。
You now have a basic fully-functional chat server!
您現在有一個基本的功能齊全的聊天服務器!
Tutorial Part 3: Rewrite Chat Server as Asynchronous
教程3部分: 將聊天服務器重寫為異步方式
This tutorial begins where Tutorial 2 left off. We’ll rewrite the consumer code to be asynchronous rather than synchronous to improve its performance.
本教程在教程2的基礎上開始。我們將重寫 consumer 代碼使其變成是異步的而不是同步的, 以提高其性能。
Rewrite the consumer to be asynchronous
將 consumer 改寫為異步
The ChatConsumer that we have written is currently synchronous. Synchronous consumers are convenient because they can call regular synchronous I/O functions such as those that access Django models without writing special code. However asynchronous consumers can provide a higher level of performance since they don’t need create additional threads when handling requests.
我們編寫的 ChatConsumer 當前是同步的。同步的 consumers 很方便, 因為它們可以調用常規的同步 I/O 函數, 例如訪問 Django models 而不用編寫特殊的代碼。但是, 異步的 consumers 可以提供更高級別的性能, 因為它們在處理請求時不需要創建其他線程。
ChatConsumer only uses async-native libraries (Channels and the channel layer) and in particular it does not access synchronous Django models. Therefore it can be rewritten to be asynchronous without complications.
ChatConsumer 只使用 async-native 庫 (Channels 和 channel layer), 特別是它不訪問同步的 Django models。因此, 它可以被改寫為異步的而不會變得復雜化。
Note
提醒
Even if ChatConsumer did access Django models or other synchronous code it would still be possible to rewrite it as asynchronous. Utilities like asgiref.sync.sync_to_async and channels.db.database_sync_to_async can be used to call synchronous code from an asynchronous consumer. The performance gains however would be less than if it only used async-native libraries.
即使 ChatConsumer 訪問 Django models 或其他同步的代碼, 它仍然可以將其重寫為異步的。像 asgiref.sync.sync_to_async 和 channels.db.database_sync_to_async 這樣的實用工具可以用來從異步 consumer 那里調用同步的代碼。但是, 性能增益將小於僅使用 async-native 庫的方式。
Let’s rewrite ChatConsumer to be asynchronous. Put the following code in chat/consumers.py:
讓我們重寫 ChatConsumer 使其變為異步的。在 chat/consumers.py 中輸入以下代碼:
# 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 }))
This new code is for ChatConsumer is very similar to the original code, with the following differences:
這些用於 ChatConsumer 的新代碼與原始代碼非常相似, 它們具有以下差異:
- ChatConsumer now inherits from AsyncWebsocketConsumer rather than WebsocketConsumer.
- 現在 ChatConsumer 繼承自 AsyncWebsocketConsumer 而不是 WebsocketConsumer。
- All methods are async def rather than just def.
- 所有方法都是 async def, 而不僅僅是 def。
- await is used to call asynchronous functions that perform I/O.
- await 被用於調用執行 I/O 的異步函數。
- async_to_sync is no longer needed when calling methods on the channel layer.
- 在 channel layer 上調用方法時, 不再需要 async_to_sync。
Let’s verify that the consumer for the /ws/chat/ROOM_NAME/ path still works. To start the Channels development server, run the following command:
讓我們驗證 consumer 的 /ws/chat/ROOM_NAME/ 路徑是否仍然有效。啟動 Channels 開發服務器, 運行以下命令:
$ python3 manage.py runserver
Open a browser tab to the room page at http://127.0.0.1:8000/chat/lobby/. Open a second browser tab to the same room page.
打開瀏覽器選項卡到 http://127.0.0.1:8000/chat/lobby/的房間頁面。打開另一個瀏覽器選項卡到同一個房間頁面。
In the second browser tab, type the message “hello” and press enter. You should now see “hello” echoed in the chat log in both the second browser tab and in the first browser tab.
在第二個瀏覽器選項卡中, 輸入消息 "hello", 然后按 enter 鍵。在第二個瀏覽器選項卡和第一個瀏覽器選項卡中, 您現在應該看到 "hello" 在聊天日志中顯示。
Now your chat server is fully asynchronous!
現在, 您的聊天服務器是完全異步的了!
Tutorial Part 4: Automated Testing
Tutorial 4 部分: 自動化測試
This tutorial begins where Tutorial 3 left off. We’ve built a simple chat server and now we’ll create some automated tests for it.
本教程在教程3的基礎上開始。我們已經建立了一個簡單的聊天服務器, 現在我們將為它創建一些自動化測試。
Testing the views
測試視圖
To ensure that the chat server keeps working, we will write some tests.
為了確保聊天服務器能夠繼續工作, 我們將編寫一些測試。
We will write a suite of end-to-end tests using Selenium to control a Chrome web browser. These tests will ensure that:
我們將編寫一套端到端的測試, 使用 Selenium 來控制 Chrome web 瀏覽器。這些測試將確保:
- when a chat message is posted then it is seen by everyone in the same room
- 當一個聊天信息被發布, 然后它能被大家在同一房間看到
- when a chat message is posted then it is not seen by anyone in a different room
- 當一個聊天信息被發布, 那么它在不同的房間是不會被別人看到的
Install the Chrome web browser, if you do not already have it.
如果您尚未擁有 Chrome web 瀏覽器, 請安裝它。
Install chromedriver.
安裝 chromedriver。
Install Selenium. Run the following command:
安裝 Selenium。運行以下命令:
$ pip3 install selenium
Create a new file chat/tests.py. Your app directory should now look like:
創建新的文件 chat/tests.py。您的應用程序目錄現在應該看起來像:
chat/ __init__.py consumers.py routing.py templates/ chat/ index.html room.html tests.py urls.py views.py
Put the following code in chat/tests.py:
在 chat/tests.py 中輸入以下代碼:
# chat/tests.py from channels.testing import ChannelsLiveServerTestCase from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.wait import WebDriverWait class ChatTests(ChannelsLiveServerTestCase): serve_static = True # emulate StaticLiveServerTestCase @classmethod def setUpClass(cls): super().setUpClass() try: # NOTE: Requires "chromedriver" binary to be installed in $PATH cls.driver = webdriver.Chrome() except: super().tearDownClass() raise @classmethod def tearDownClass(cls): cls.driver.quit() super().tearDownClass() def test_when_chat_message_posted_then_seen_by_everyone_in_same_room(self): try: self._enter_chat_room('room_1') self._open_new_window() self._enter_chat_room('room_1') self._switch_to_window(0) self._post_message('hello') WebDriverWait(self.driver, 2).until(lambda _: 'hello' in self._chat_log_value, 'Message was not received by window 1 from window 1') self._switch_to_window(1) WebDriverWait(self.driver, 2).until(lambda _: 'hello' in self._chat_log_value, 'Message was not received by window 2 from window 1') finally: self._close_all_new_windows() def test_when_chat_message_posted_then_not_seen_by_anyone_in_different_room(self): try: self._enter_chat_room('room_1') self._open_new_window() self._enter_chat_room('room_2') self._switch_to_window(0) self._post_message('hello') WebDriverWait(self.driver, 2).until(lambda _: 'hello' in self._chat_log_value, 'Message was not received by window 1 from window 1') self._switch_to_window(1) self._post_message('world') WebDriverWait(self.driver, 2).until(lambda _: 'world' in self._chat_log_value, 'Message was not received by window 2 from window 2') self.assertTrue('hello' not in self._chat_log_value, 'Message was improperly received by window 2 from window 1') finally: self._close_all_new_windows() # === Utility === def _enter_chat_room(self, room_name): self.driver.get(self.live_server_url + '/chat/') ActionChains(self.driver).send_keys(room_name + '\n').perform() WebDriverWait(self.driver, 2).until(lambda _: room_name in self.driver.current_url) def _open_new_window(self): self.driver.execute_script('window.open("about:blank", "_blank");') self.driver.switch_to_window(self.driver.window_handles[-1]) def _close_all_new_windows(self): while len(self.driver.window_handles) > 1: self.driver.switch_to_window(self.driver.window_handles[-1]) self.driver.execute_script('window.close();') if len(self.driver.window_handles) == 1: self.driver.switch_to_window(self.driver.window_handles[0]) def _switch_to_window(self, window_index): self.driver.switch_to_window(self.driver.window_handles[window_index]) def _post_message(self, message): ActionChains(self.driver).send_keys(message + '\n').perform() @property def _chat_log_value(self): return self.driver.find_element_by_css_selector('#chat-log').get_property('value')
Our test suite extends ChannelsLiveServerTestCase rather than Django’s usual suites for end-to-end tests (StaticLiveServerTestCase or LiveServerTestCase) so that URLs inside the Channels routing configuration like /ws/room/ROOM_NAME/ will work inside the suite.
我們的測試套件擴展了 ChannelsLiveServerTestCase, 而不是 Django 常用來進行端到端測試的套件 (StaticLiveServerTestCase 或 LiveServerTestCase), 這樣, Channels 路由配置里面的 URLs(如 /ws/room/ROOM_NAME/ ) 將會在套件里面工作。
To run the tests, run the following command:
要運行測試, 請運行以下命令:
$ python3 manage.py test chat.tests
You should see output that looks like:
您應該看到如下所示的輸出:
Creating test database for alias 'default'... System check identified no issues (0 silenced). .. ---------------------------------------------------------------------- Ran 2 tests in 5.014s OK Destroying test database for alias 'default'...
You now have a tested chat server!
你現在有一個經過測試的聊天服務器了!
What’s next?
接下來應該做什么呢?
Congratulations! You’ve fully implemented a chat server, made it performant by writing it in asynchronous style, and written automated tests to ensure it won’t break.
祝賀!您已經完全實現了一個聊天服務器, 通過在異步樣式中編寫它來高性能, 並編寫了自動測試以確保它不會中斷。
This is the end of the tutorial. At this point you should know enough to start an app of your own that uses Channels and start fooling around. As you need to learn new tricks, come back to rest of the documentation.
這是教程的結尾。現在,你應該清楚地知道如何啟動一個使用了 Channels 的你自己的應用程序和做其他的操作。當您需要學習新的技巧時, 請回到文檔的其余部分。
作者: 守護窗明守護愛
出處: https://www.cnblogs.com/chuangming/p/9222794.html
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出。如有問題,可郵件(1269619593@qq.com)咨詢.