官文:
https://channels.readthedocs.io/en/latest/tutorial/part_3.html
安装:
pip install channels, channels-redis
uvicorn==0.22.0
channels==3.0.5
channels-redis==4.1.0
配置
# settings.py中添加
INSTALLED_APPS = [
...,
'channels',
]
ASGI_APPLICATION = '项目名.asgi.application'
注:推荐使用版本大于django3.0(配置)
asgi.py中添加
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '项目名.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
# Just HTTP for now. (We can add other protocols later.)
})
django2.2配置(因为没asgi文件,自建一个asgi.py文件)
import os
import django
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '项目名.settings')
django.setup()
application = ProtocolTypeRouter({
"http": AsgiHandler(),
# Just HTTP for now. (We can add other protocols later.)
})
启动服务,展示下面日志便启动成功(ASGI)
此时已经配置成功
单向通讯(实操)
单向通讯不用redis即可实现
在自己项目下新建consumers.py
# front_end/consumer.py
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
}))
在自己项目下新建routing.py
from django.urls import path
from front_end import consumers
websocket_urlpatterns = [
path(r'ws/chat/', consumers.ChatConsumer.as_asgi()),
]
asgi.py中注册url
2.2版本
import os
import django
from channels.http import AsgiHandler
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from front_end import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '项目名.settings')
django.setup()
application = ProtocolTypeRouter({
"http": AsgiHandler(),
# Just HTTP for now. (We can add other protocols later.)
"websocket": AllowedHostsOriginValidator( # 新增部分
AuthMiddlewareStack(
URLRouter(
routing.websocket_urlpatterns
)
)
),
})
3.0及以上
# 项目名/asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "项目名.settings")
django_asgi_app = get_asgi_application()
import chat.routing
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
routing.websocket_urlpatterns
)
)
),
})
websocket在线测试
http://www.websocket-test.com/
http://www.jsons.cn/websocket/【简洁】
ws://127.0.0.1:8000/ws/chat/
发送: {"message": "测试发送一下"}
双向通道(多人聊天配置)
settings.py中配置redis
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)], # 不带密码
注: 不带密码在未关掉redis安全保护的情况不能访问公网redis
(redis.conf注释掉bind 127.0.0.1, 找到 protected-mode yes 把 yes 改为 no)
# "hosts": ["redis://:密码@IP:端口/1"], 带密码配置
},
},
}
配置自定义时间后主动断开连接
在Django Channels中实现超时断开连接可以通过使用timeout
参数和threading.Timer
来实现。下面是一个示例:
from channels.generic.websocket import WebsocketConsumer
import threading
class MyConsumer(WebsocketConsumer):
def connect(self):
# 连接建立时设置超时
self.timeout = 60 # 设置超时时间(以秒为单位)
self.timer = threading.Timer(self.timeout, self.disconnect)
self.timer.start()
# 连接逻辑...
def receive(self, text_data):
# 收到消息时重置超时计时器
self.timer.cancel()
self.timer = threading.Timer(self.timeout, self.disconnect)
self.timer.start()
# 处理消息...
def disconnect(self, code=None):
# 断开连接逻辑...
self.close(code)
在这个示例中,我们定义了一个MyConsumer
类,它继承自WebsocketConsumer
。在connect
方法中,我们使用threading.Timer
设置一个计时器,当超过指定时间后调用disconnect
方法断开连接。
然后,在receive
方法中,每当收到消息时,我们重置计时器,以保持连接的活跃状态。
最后,在disconnect
方法中,我们断开连接的逻辑可以自行定义,比如通过调用self.close(code)
来主动关闭连接。
请注意,这只是一个简单的示例,你可以根据你的具体需求和业务逻辑进行修改和扩展。
在多线程编程中,cancel 方法用于请求取消一个线程的执行。它是 Thread 类的一个方法,可以用来中断正在运行的线程,
或者阻止一个已经被请求中断的线程继续执行。
当调用 cancel 方法时,它会将线程的中断状态设置为 true。这通常会让线程在适当的时候退出自己的执行。
然而,需要注意的是 cancel 方法仅仅是请求线程取消,具体是否取消还取决于线程的具体实现。线程可以通过检查是否发生
了中断来自行决定如何处理中断请求。一些常见的线程实现,如使用循环或阻塞方法的线程,通常会检查中断状态并相应地停止执行。
在使用 cancel 方法时需要注意以下几点:
1. 调用 cancel 方法并不一定会导致线程立即停止执行,具体取决于线程的实现代码。
2. 在需要取消线程的情况下,通常需要在适当的地方检查线程的中断状态,并编写相应的代码逻辑来处理中断请求。
3. 对于一些长时间运行的任务,可以在任务内部的循环中通过调用 Thread.currentThread().isInterrupted()
来检查中断状态,并在需要时通过 return 或抛出 InterruptedException 来停止任务。
总之,cancel 方法是用来请求取消线程执行的,但具体的取消逻辑需要线程的具体实现来决定。
重复连接问题
在Django Channels中,你可以使用一个signal来处理用户连接和断开连接的事件。通过拦截连接事件,你可以检查用户是否已经连接过,并避免多次连接。
首先,在你的consumers.py
文件中,定义一个signal处理函数。该函数会在用户连接和断开连接时被调用:
from channels.db import database_sync_to_async
connected_users = set()
@database_sync_to_async
def update_connected_users(user_id, add=True):
if add:
connected_users.add(user_id)
else:
connected_users.remove(user_id)
async def handle_connect(user_id):
await update_connected_users(user_id, add=True)
async def handle_disconnect(user_id):
await update_connected_users(user_id, add=False)
接下来,在你的consumer中,可以使用上述定义的signal处理函数来检查用户是否已连接,并避免多次连接和加入组:
from channels.generic.websocket import AsyncWebsocketConsumer
from django.contrib.auth.models import User
from django.dispatch import receiver
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
@receiver(user_logged_in)
def on_user_login(sender, request, user, **kwargs):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"connected_users", # 你可以定义一个组名,用于管理连接的用户
{
'type': 'handle_connect',
'user_id': user.id,
}
)
@receiver(user_logged_out)
def on_user_logout(sender, request, user, **kwargs):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"connected_users",
{
'type': 'handle_disconnect',
'user_id': user.id,
}
)
class MyConsumer(AsyncWebsocketConsumer):
async def connect(self):
user_id = self.scope['user'].id
if user_id not in connected_users:
await self.channel_layer.group_add(
"connected_users",
self.channel_name
)
await handle_connect(user_id)
await self.accept()
async def disconnect(self, close_code):
user_id = self.scope['user'].id
await self.channel_layer.group_discard(
"connected_users",
self.channel_name
)
await handle_disconnect(user_id)
async def receive(self, text_data):
# 处理接收到的数据
pass
此处使用了Django的信号量机制,通过拦截用户登录和退出事件,来更新连接的用户集合。当用户连接时,会检查该用户是否已经存在于集合中,只有当用户不存在时,才会加入用户组并调用handle_connect
函数。同样,当用户断开连接时,会从用户组中移除该用户,并调用handle_disconnect
函数。
这样,你就可以避免同一个用户多次连接并加入组了。同时,你可以通过connected_users
集合来管理当前连接的用户。
归档
安装
pip3 install channels
快速上手
-
在settings中添加配置
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', ] ASGI_APPLICATION = '项目名.asgi.application'
-
创建websocket应用和路由
#!/usr/bin/env python # -*- coding:utf-8 -*- from channels.routing import ProtocolTypeRouter, URLRouter from django.conf.urls import url from chat import consumers application = ProtocolTypeRouter({ 'websocket': URLRouter([ url(r'^chat/$', consumers.ChatConsumer), ]) })
- 编写处理websocket逻辑业务
案例一
#!/usr/bin/env python # -*- coding:utf-8 -*- from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): self.accept() def websocket_receive(self, message): print('接收到消息', message) self.send(text_data='收到了') def websocket_disconnect(self, message): print('客户端断开连接了') raise StopConsumer()
案例二
#!/usr/bin/env python # -*- coding:utf-8 -*- from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class SimpleChatConsumer(WebsocketConsumer): def connect(self): self.accept() def receive(self, text_data=None, bytes_data=None): self.send(text_data) # 主动断开连接 # self.close() def disconnect(self, code): print('客户端要断开了')
案例三
#!/usr/bin/env python # -*- coding:utf-8 -*- from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer CLIENTS = [] class ChatConsumer(WebsocketConsumer): def connect(self): self.accept() CLIENTS.append(self) def receive(self, text_data=None, bytes_data=None): for item in CLIENTS: item.send(text_data) # 主动断开连接 # self.close() def disconnect(self, code): CLIENTS.remove(self)
channel layer
基于内存的channel layer
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
}
}
业务处理
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
async_to_sync(self.channel_layer.group_add)('x1', self.channel_name)
self.accept()
def receive(self, text_data=None, bytes_data=None):
async_to_sync(self.channel_layer.group_send)('x1', {
'type': 'xxx.ooo',
'message': text_data
})
def xxx_ooo(self, event):
message = event['message']
self.send(message)
def disconnect(self, code):
async_to_sync(self.channel_layer.group_discard)('x1', self.channel_name)
基于 redis的channel layer
安装: pip3 install channels-redis
配置
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [('10.211.55.25', 6379)]
},
},
}
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {"hosts": ["redis://10.211.55.25:6379/1"],},
},
}
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {"hosts": [('10.211.55.25', 6379)],},},
}
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": ["redis://:password@10.211.55.25:6379/0"],
"symmetric_encryption_keys": [SECRET_KEY],
},
},
}
业务逻辑
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
async_to_sync(self.channel_layer.group_add)('x1', self.channel_name)
self.accept()
def receive(self, text_data=None, bytes_data=None):
async_to_sync(self.channel_layer.group_send)('x1', {
'type': 'xxx.ooo',
'message': text_data
})
def xxx_ooo(self, event):
message = event['message']
self.send(message)
def disconnect(self, code):
async_to_sync(self.channel_layer.group_discard)('x1', self.channel_name)
异步
import json
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer
from asgiref.sync import async_to_sync
from loguru import logger
class ChatConsumer(AsyncWebsocketConsumer):
def get_room_id(self):
'''获取组的id'''
return self.scope['url_route']['kwargs']['group']
async def connect(self):
await self.channel_layer.group_add(str(self.get_room_id()), self.channel_name)
await self.accept()
logger.warning(f'组id:【{self.get_room_id()}】,通道名:【{self.channel_name}】连接成功')
logger.success(f'当前全部组:{self.channel_layer}')
async def receive(self, text_data=None, bytes_data=None):
await self.channel_layer.group_send(str(self.get_room_id()), {
'type': 'xxx.ooo',
'message': text_data
})
async def xxx_ooo(self, event):
print(event)
message = event['message']
await self.send(message)
async def disconnect(self, code):
print(code)
await self.channel_layer.group_discard(str(self.get_room_id()), self.channel_name)
await self.close()
在其他地方向通道发送消息
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def flash_data(url: str):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(str(88), {
'type': 'chat_message',
'message': json.dumps({'flash_url_ctypes': str(url)})
})
关于启动
#daphne tree_hole_gpt.asgi:application -p 8002 -b 127.0.0.1
uvicorn tree_hole_gpt.asgi:application --port 8002 --host 127.0.0.1 --workers 2