momo's Blog.

Python Channels库使用

字数统计: 2.1k阅读时长: 10 min
2020/09/04 Share

前言

记录一下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
<!-- chatroom/templates/chatroom/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>

创建一个view视图 chatroom/views.py

1
2
3
4
5
6
7
from django.shortcuts import render

# Create your views here.


def 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
# Channels
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 render

# Create your views here.


def 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_path

from . import consumers

websocket_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.routing

application = 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

# 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
}))

新的代码我们需要进行一步说明

  • 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 0x000001D8CAA88E80>, 'user': <channels.auth.UserLazyObject object at 0x000001D
      8CAA88EF0>, '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)(...)

    • 离开一个group
  • 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 json
from channels.generic.websocket import AsyncWebsocketConsumer


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
}))
CATALOG
  1. 1. 前言
  2. 2. 基本设置
    1. 2.1. 创建聊天室应用
    2. 2.2. 添加索引视图
    3. 2.3. 使用 Channels 库
  3. 3. 实现聊天服务器
    1. 3.1. 创建消费者
    2. 3.2. 使用 channels layer
  4. 4. 将consumer改写为异步方法