前言 记录一下django channels 库的基本使用 本文档记录 官方文档教程,可直接参考官方文档: 教程第一部分
基本设置 创建聊天室应用 代码结构如下图
添加我们的chatroom应用. 编辑djangoProject/settings.py文件, 在列表中加入我们的APP名称
1 2 3 4 5 6 7 8 9 10 11 # Application definition INSTALLED_APPS = [ 'django .contrib.admin', 'django .contrib.auth', 'django .contrib.contenttypes', 'django .contrib.sessions', 'django .contrib.messages', 'django .contrib.staticfiles', 'chatroom' ]
添加索引视图 在应用下下建立一个templates/chatroom/index.html 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!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 ) { 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 >
创建一个view视图 chatroom/views.py
1 2 3 4 5 6 7 from django.shortcuts import renderdef index (request) : return render(request, 'chatroom/index.html' )
配置url路径,在项目目录下创建urls.py
1 2 3 4 5 6 7 from django.urls import path from . import views urlpatterns = [ path ('' , views.index, name ='index' ), ]
将根URLconf指向我们的 urls.py
1 2 3 4 5 6 7 from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/' , admin.site .urls ), path('chat/' , include('chatroom.urls' )) ]
验证配置是否有效
1 python3 manage.py runserver
使用 Channels 库 安装channels
1 python -m pip install -U channels
Channels 也要创建一个路由配置, 配置类似于django的url_conf,它告诉channels当channels服务器接收到HTTP请求时要运行什么代码、
首先在django url跟路由目录下同级,创建一个channels的根路由 djangoProject/routing.py
1 2 3 4 5 from channels.routing import ProtocolTypeRouter application = ProtocolTypeRouter({ # (http -> django views is added by default) })
将channels添加到 INSTALLED_APPS 配置
1 2 3 4 5 6 7 8 9 10 INSTALLED_APPS = [ 'django .contrib.admin', 'django .contrib.auth', 'django .contrib.contenttypes', 'django .contrib.sessions', 'django .contrib.messages', 'django .contrib.staticfiles', 'chatroom' , 'channels' ]
还需要继续修改settings 将channels指向根路由配置,在配置中添加
1 2 ASGI_APPLICATION = 'djangoProject.routing.application'
此时,重新运行下方命令,已确认channels正常工作
1 python3 manage.py runserver
可以看到如下输出
1 2 3 4 5 6 You have 18 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. September 04 , 2020 - 14 :11 :57 Django version 3.1 .1 , using settings 'djangoProject.settings' Starting ASGI/Channels version 2.4 .0 development server at http ://127.0 .0 .1 :8000 / Quit the server with CTRL-BREAK.
注意此行,表示channels开发服务器已接管django的开发服务器
Starting ASGI/Channels version 2.4.0 development server at http://127.0.0.1:8000/
实现聊天服务器 新建房间的前端页面 chatroom/templates/room.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <!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" > {{ room_name|json_script:"room-name" }} <script > const roomName = JSON.parse(document.getElementById('room-name').textContent); const chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/' + roomName + '/' ); chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); document.querySelector('#chat-log').value += (data.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) { const messageInputDom = document.querySelector('#chat-message-input'); const message = messageInputDom.value; chatSocket.send(JSON.stringify({ 'message': message })); messageInputDom.value = ''; }; </script > </body > </html >
创建对应的视图views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 from django.shortcuts import renderdef index (request) : return render(request, 'chatroom/index.html' ) def room (request, room_name) : return render(request, 'chatroom/room.html' , { 'room_name' : room_name })
创建url规则
1 2 3 4 5 6 7 8 from django.urls import path from . import views urlpatterns = [ path ('' , views.index, name ='index' ), path ('<str:room_name>/' , views.room, name ='room' ) ]
重新运行页面,并键入房间名,F12打开console发现报错
1 WebSocket connection to 'ws://127.0.0.1:8000/ws/chat/3232/' failed: Error during WebSocket handshake: net: :ERR_CONNECTION_RESET
这是因为我们还未创建接受WebSocket连接的使用者。
创建消费者 在app目录下创建 chatroom/consumers.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import json from channels.generic.websocket import WebsocketConsumer 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 }))
创建URL匹配规则,chatroom/routing.py
为了规范,一般匹配 ws 路径前缀
1 2 3 4 5 6 7 from django.urls import re_pathfrom . import consumerswebsocket_urlpatterns = [ re_path(r'ws/chat/(?P<room_name>\w+)/$' , consumers.ChatConsumer), ]
将匹配规则导入主路由匹配 djangoProject/routing.py
1 2 3 4 5 6 7 8 9 10 11 12 from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import chatroom.routingapplication = ProtocolTypeRouter({ # (http -> django views is added by default) 'websocket' : AuthMiddlewareStack( URLRouter( chatroom.routing.websocket_urlpatterns ) ), })
此配置指定在与Channels服务器建立连接时,ProtocolTypeRouter 将会检查连接的类型。如果是WebSocket连接(ws:// 或者 wss://), 则将连接给配给AuthMiddlewareStack.
经过鉴权以后,连接到URL路径
现在打开聊天室,便可以在窗口中看到自己的消息,但是无法看到另一个相同窗口所发送的聊天内容。
所以,我们需要相同的consumer能够互相通信才行, channels提供了一个 channel layer 来管理
使用 channels layer 官方推荐使用redis作为后端存储
安装 channels_redis
1 python3 -m pip install channels_redis
在使用layer前,需要在配置文件中添加Redis设置
1 2 3 4 5 6 7 8 9 10 # Channels ASGI_APPLICATION = 'djangoProject.routing.application' CHANNEL_LAYERS = { 'default' : { 'BACKEND' : 'channels_redis.core.RedisChannelLayer' , 'CONFIG' : { "hosts" : [('192.168.1.70' , 6379 )], }, }, }
为了检测可用,我们需要打开django shell 并执行如下命令
1 2 3 4 5 6 7 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' }
使用新的代码来替换 consumers.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import json from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer 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 async_to_sync(self .channel_layer.group_add)( self .room_group_name, self .channel_name ) self .accept() def disconnect (self , close_code) : async_to_sync(self .channel_layer.group_discard)( self .room_group_name, self .channel_name ) def receive (self , text_data) : text_data_json = json.loads(text_data) message = text_data_json['message' ] async_to_sync(self .channel_layer.group_send)( self .room_group_name, { 'type' : 'chat_message' , 'message' : message } ) def chat_message (self , event) : message = event['message' ] self .send(text_data=json.dumps({ 'message' : message }))
新的代码我们需要进行一步说明
self.scope['url_route']['kwargs']['room_name']
room_name是从 chatroom/routing.py 的URL路由中获取的参数.scope对象完整内容如下1 2 3 4 {'type': 'websocket', 'path': '/ws/chat/3232 /', 'raw_path': b'/ws/chat/3232 /', 'headers': [(b'host', b'127.0.0.1:8000 '), (b'connection', b'Upgrade'), (b'pragma', b'no-cache'), (b'cache-control', b'no-cache'), (b'user-agent', b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36'), (b'upgrade', b'websocket'), (b'origin', b'http://127.0.0.1:8000 '), (b'sec-websocket-version', b'13'), (b'accept-encoding', b'gzip, deflate, br'), (b'accept-language', b'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7'), (b'cookie', b'csrftoken=NVTSIc1SysiRUeOjW2IYuAQo9QMbyEPLofdqJA4jdLBkraImmCPP3jdScaNyXzsZ'), (b'sec-websocket-key', b'fOBE+s7uCP0NQdycxwRNvQ=='), (b'sec-websocket-extensions', b'permessage -deflate; client_max_window_bits')], 'query_string': b'', 'client': ['127.0.0.1', 57962 ], 'server': ['127.0.0.1', 8000 ], 'subprotocols': [], 'cookies': {'csrftoken': 'NVTSIc1SysiRUeOjW2 IYuAQo9QMbyEPLofdqJA4jdLBkraImmCPP3jdScaNyXzsZ'}, 'session': <django.utils.functional.LazyObject object at 0 x000001D8CAA88E80>, 'user': <channels.auth.UserLazyObject object at 0 x000001D 8 CAA88EF0>, 'path_remaining': '', 'url_route': {'args': (), 'kwargs': {'room_name': '3232 '}}}
self.room_group_name = 'chat_%s' % self.room_name
直接从用户指定的房间名称构造通道组名称,而无需引号或转义。
组名只能包含字母,数字,连字符和句点。因此,此示例代码将在包含其他字符的房间名称上失败。
async_to_sync(self.channel_layer.group_add)(...)
加入一个group
此方法需要用async_to_sync装饰器, 因为ChatConsumer是一个同步的方法,但是它正在调用异步的layer方法. (所有的layer 方法都是异步的)
self.accept()
接收Websocket连接
如果未在 connect() 方法中调用 accept() 方法,则连接将被拒绝关闭。例如,你想拒绝一个没有权限的用户连接
async_to_sync(self.channel_layer.group_discard)(...)
async_to_sync(self.channel_layer.group_send)
将事件发送到group
事件具有一个特殊的type 键,改键对应于接收事件使用者中,调用方法的名称
将consumer改写为异步方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import jsonfrom channels.generic.websocket import AsyncWebsocketConsumerclass 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 await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect (self, close_code) : await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) async def receive (self, text_data) : text_data_json = json.loads(text_data) message = text_data_json['message' ] await self.channel_layer.group_send( self.room_group_name, { 'type' : 'chat_message' , 'message' : message } ) async def chat_message (self, event) : message = event['message' ] await self.send(text_data=json.dumps({ 'message' : message }))