网上有很多channel的教学文章,我看的时候,总感觉有些概念模糊不清,看得有点头痛(比如推送消息时的type字段究竟时什么意思)。结合多篇文章,很不容易理解了一些,于是在下面写出channel的基本使用和对一些概念的解析,希望对大家有一些帮助。
关于channel的更深一层理解和使用,我也正在学习,后面可能会写的更详细点。
参考:
1.Django-channels2.0笔记–2、Channel Layers
2.django中channel模块之websocket
1.为什么使用channel
django 本身不支持websocket协议,可以通过使用channel实现对django的支持
2.websocket 搭建
2.1 setting.py 配置
INSTALLED_APPS 中添加 channel,
// 'Chat.wsgi.application' 是websock路由配置的地方,根据实际修改
WSGI_APPLICATION = 'Chat.wsgi.application',
// 配置redis作为通道层,可选
// 如果要使用channel layer 则必须配置通道层,否则无法获取channel_name
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('localhost', 6379)],
},
},
}
2.2 约定俗成地新建一个routing.py(路径与WSGI_APPLICATION 中一致)
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chartroom.routing
application = ProtocolTypeRouter({
// 这里只对websocket通信进行配置,还可以对其他通信进行处理
'websocket': AuthMiddlewareStack(
URLRouter(
// 实际的websock请求会路由到这里
chartroom.routing.websocket_urlpatterns
)
),
})
chartroom.routing.websocket_urlpatterns 如下:
from django.urls import path
from chartroom.consumers import ChatConsumer
websocket_urlpatterns = [
// 按需要自己进行配置,与django的类视图一致
// Chatconsumer 是自定义的消费者类
path('ws/chat/', ChatConsumer),
]
class ChatConsumer(WebsocketConsumer):
# 连接时触发
def connect(self):
// 将channel_name进行分组,后面可以通过组名来分发消息给同一个组的所有channel
// 这里特别说明一下,组名是自定义的,自行对channel进行分组
async_to_sync(self.channel_layer.group_add)(
"lkm",
self.channel_name
)
print(self.channel_name)
// 将连接的websock的channel_name 进行持久化,通常与用户名进行绑定
// 只有定义了通道层(如redis),才能获取到channel_name,否则会报错
c = Clients(channel_name=self.channel_name)
c.save()
self.accept()
# 关闭连接时触发
def disconnect(self, close_code):
pass
# 收到消息后触发
def receive(self, text_data):
// 自动将消息发送到当前连接的websocket客户端
self.send(text_data=text_data)
// event 相当于request
def chat_message(self, event):
print("chat_message")
self.send(text_data=event["text"])
系统会根据连接状态先后执行connect,disconnect等方法,这些方法名是固定的
chat_message 方法名是自定义的
4.在消费者类外进行消息推送
在消费者类外进行消息推送,推送消息给多个客户端需要利用channel layer
Channel引入了一个layer的概念,channel layer是一种通信系统,允许多个consumer实例之间互相通信,以及与外部Django程序实现互通。
Channel layer主要实现了两种概念抽象:
channel name:
channel实际上是一个发送消息的通道,每个channel都有一个名称,每一个拥有这个名称的人都可以往channel里面发送消息
group:
多个channel可以组成一个group,每个group都有一个名称。
每个拥有这个名称的人都可以往这个group里添加/删除channel,也可以往group里发送消息。
group内的所有channel都可以收到,但是不能给group内的具体某个channel发送消息。
================================================================================
from channels.layers import get_channel_layer
from .models import Clients
def chat1(request):
// 取出保存的channel_name
c = Clients.objects.all()[0]
// 获取当前的通道层对象
channel_layer = get_channel_layer()
// 发送到指定的channel,并将消息转发到type中的方法进行处理
async_to_sync(channel_layer.send)(c.channel_name, {
"type": "chat.message",
"text": "Hello there!",
})
// 发送到组里所有的channel
async_to_sync(channel_layer.group_send)("lkm",{
"type": "chat_message",
"text": "xxx!",
})
return HttpResponse("ok")
注意:
1.因为layer中所有的方法都是异步的,要从同步环境发送事件,需要使用async_to_sync 来包装,否 则消息无法发送。
2.type含义:将该消息转发到对应消费者类中的对应方法进行处理,chat.message 会被解析为chat_mesage, 然后将消息转发到chat_message中进行下一步处理。所以要注意方法的准确命名。至于怎么准确找到对应消费者,我认为应该是channel_name 里应该就包含了相关信息。
3.一个连接(channel)创建时,通过group_add将channel添加到Group中。
连接关闭时,通过group_discard将channel从Group中删除。
收到消息时,调用group_send发送消息到Group中,group中的所有channel都可以收到。
直接在消费者类为同步
from channels.generic.websocket import AsyncWebsocketConsumer,WebsocketConsumer
import json
from asgiref.sync import async_to_sync
from testchannel.tasks import *
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
# 连接时触发
self.group_name = "chat_group"
await self.channel_layer.group_add(
self.group_name, self.channel_name
)
await self.accept()
async def disconnect(self, code):
# 关闭连接时触发
await self.channel_layer.group_discard(
self.group_name, self.channel_name
)
pass
async def receive(self, text_data=None, bytes_data=None):
# 收到消息后触发
# 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
text_data_json = json.loads(text_data)
message = text_data_json['message']
print("wwwwwwwwwwwww", message)# yyyyyyyyyyyyy
await self.channel_layer.group_send(
self.group_name,
{
'type': 'chat_message',
'message':message
}
)
async def chat_message(self,event):
print("enventooooooooooooooooo",event) # {'type': 'chat_message', 'message': 'yyyyyyyyyyyyy'}
message = "聊天:" + event["message"]
await self.send(text_data=json.dumps({'message':message}))
class TailfConsumer(WebsocketConsumer):
def connect(self):
self.file_id = self.scope["url_route"]["kwargs"]["id"]
self.result = tailf.delay(self.file_id,self.channel_name)
# self.result = add.delay(1,8)
self.accept()
def disconnect(self, code):
self.result.revoke(terminate=True)
def send_message(self,event):
self.send(text_data=json.dumps({"message":event["message"]}))
有几个小区别:
ChatConsumer由WebsocketConsumer修改为了AsyncWebsocketConsumer
所有的方法都修改为了异步defasync def
用await来实现异步I/O的调用
channel layer也不再需要使用async_to_sync了
追加
问题:从上面可以知道,以下代码会把消息发送到consumer的chat_message方法再次进行处理,那么,当存在两个个consumer,并且consumer都存在相同方法时,会把改消息转发到哪个consumer的方法上呢?
async_to_sync(channel_layer.group_send)("lkm",{
"type": "chat_message",
"text": "xxx!",
})
我尝试在源码中找到答案,但是水平不够,没找出来,但是通过实验能大概得出以下结论。在建立websocket连接时,会将consumer类与channel name 进行绑定.当发送信息时,会自动转发到绑定的consumer上。
注意:如果每个consumer中都使用以下代码
async_to_sync(self.channel_layer.group_add)(
"lkm",
self.channel_name
)
那么,消息会同时转发到两个consumer上的chat_message方法上,因为现在“lkm”这个gruop有两个channel,每个channel绑定一个不同的consumer。但是如果你这样干了,那么你就需要保证每个consumer都有对应chat_message方法,否则会因为某个consumer找不到处理方法而报错。