django_◆_channels_◆_layim_实现用户一对一,一对多,群组聊天实时通讯
Django Channels介绍
首先要理解Django现有的请求响应策略是这样的:浏览器发出请求,Django服务器接受请求后通过路由匹配该请求到某个视图,视图将会返回一个响应并由服务器发送回浏览器.类似的请求响应在Flask实现也是如此.对于一般性的网页浏览(比如新闻阅读),这样的响应机制是没有问题的,但对于需要一个保持不断会话的请求来说,这是行不通的,因为Django的声明周期只能存在一个请求中,它不能让服务器在没有请求的情况下不断地发送数据岛浏览器客服端.这样的场景目前正在不断地涌现,例如在线聊天室,会话机器人,以及最近很流行的微服务应用.Channels改变了Django的工作方式,让它实现了一种包括通道、消费者和worker的worker监听的模式,所有消费者都会分配有单独的通道,worker监听通道的消息,确保消息到来时能进行处理.为了确保上述机制运行,Channels需要有三个工作层:
接口服务器,Django和用户(浏览器)之间通信的桥梁,包括一个实现WSGI协议的适配器和一个独立的websocket服务器.
通道后端, 在接口服务器和worker之间传递消息,由插拔式的python代码和存储组成,存储可以是内存、数据库或者redis,推荐使用redis,兼具其余两者的优点.
worker,监听所有channel,当有新消息到来时候唤醒功能函数.
Channels可以让Django的框架变得更为可靠和可拓展,整个通信的服务器数可以按需拓展,至少保证一台协议服务器和一台工作服务器即可.使用Channels后,你不再需要组织code去为异步调用,Channls已经将一切都已经帮你准备好.
实验教程
前端框架:?https://www.layui.com/layim/
演示实例:
用户名:user001
密码:p@ssw0rdwcx
用户名:kefu001
服务端使用IE浏览器打开:https://www.szyfd.xyz/itkf/app/index/
前端使用:?https://www.layui.com/doc/modules/layim.html
运行效果图:
项目目录:
1.pycharm 新建django 项目
2.安装?pip install -r requirements.txt
aioredis==1.2.0 asgiref==3.2.7 asn1crypto==0.24.0 async-timeout==3.0.1 attrs==19.1.0 autobahn==19.9.2 Automat==0.7.0 backports.csv==1.0.7 certifi==2019.6.16 cffi==1.12.3 channels==2.2.0 channels-redis==2.3.3 chardet==3.0.4 constantly==15.1.0 cryptography==2.7 daphne==2.3.0 defusedxml==0.6.0 diff-match-patch==20181111 Django==2.1.11 django-import-export==1.2.0 django-redis==4.10.0 django-simpleui==4.0 django-utils==0.0.2 et-xmlfile==1.0.1 hiredis==1.0.0 hyperlink==19.0.0 idna==2.8 incremental==17.5.0 jdcal==1.4.1 lark-parser==0.7.4 msgpack==0.6.1 numpy==1.17.1 odfpy==1.4.0 openpyxl==2.6.3 optionaldict==0.1.1 Pillow==7.1.2 pycparser==2.19 PyHamcrest==1.9.0 PyMySQL==0.9.3 python-dateutil==2.8.0 pytz==2019.2 PyYAML==5.1.2 redis==3.3.8 requests==2.22.0 required==0.4.0 six==1.12.0 sqlparse==0.3.0 tablib==0.13.0 Twisted==19.7.0 txaio==18.8.1 urllib3==1.25.3 wechatpy==1.8.3 xlrd==1.2.0 xlwt==1.3.0 xmltodict==0.12.0 zope.interface==4.6.0
3.新建?routing.py
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import app.routing application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter( app.routing.websocket_urlpatterns ) ), })
4.配置?settings.py
""" Django settings for itkf project. Generated by 'django-admin startproject' using Django 3.0.5. For more information on this file, see https://docs.djangoproject.com/en/3.0/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'avamfpy-nj-9q#91nn89^(zjl0s-iu3*◆g◆strpqjxqwerh' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ 'simpleui', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app.apps.AppConfig', 'import_export', 'channels', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'itkf.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] ASGI_APPLICATION = 'itkf.routing.application' WSGI_APPLICATION = 'itkf.wsgi.application' # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases # redis配置 CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100}, "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", # "PASSWORD": "密码", } } } CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, } DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'HOST': '127.0.0.1', 'PORT': '3306', 'NAME': 'itkf', 'USER': 'root', 'PASSWORD': '123456' } } # 开发redis 路径 C:\Program Files\Redis redis-server redis.windows.conf ''' windows下安装Redis第一次启动报错: [2368] 21 Apr 02:57:05.611 # Creating Server TCP listening socket 127.0.0.1:6379: bind: No error 解决方法:在命令行中运行 redis-cli.exe 127.0.0.1:6379>shutdown not connected>exit 然后重新运行redis-server.exe redis.windows.conf ''' # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ domain = "http://127.0.0.1:8000" # 图片上传路径 MEDIA_URL = '/' MEDIA_ROOT = r'D:/itkf/itkfstatic/uploadImage/' STATIC_URL = '/itkfstatic/' SIMPLEUI_HOME_INFO = False # SIMPLEUI 配置 SIMPLEUI_STATIC_OFFLINE = True STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'itkfstatic'), ) # 登录页面 LOGIN_URL = '/itkf/admin/login/' # 权限缓存配置 SESSION_ENGINE = 'django.contrib.sessions.backends.db' # 引擎(默认) SESSION_COOKIE_NAME = "sessionid" # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认) SESSION_COOKIE_PATH = "/" # Session的cookie保存的路径(默认) SESSION_COOKIE_DOMAIN = None # Session的cookie保存的域名(默认) SESSION_COOKIE_SECURE = False # 是否Https传输cookie(默认) SESSION_COOKIE_HTTPONLY = True # 是否Session的cookie只支持http传输(默认) SESSION_COOKIE_AGE = 1209600 # Session的cookie失效日期(2周)(默认) SESSION_EXPIRE_AT_BROWSER_CLOSE = True # 是否关闭浏览器使得Session过期(默认) SESSION_SAVE_EVERY_REQUEST = False # 是否每次请求都保存Session,默认修改之后才保存(默认) weChatWork = { 'corpid': "", 'secret': "", 'sourceFile': "static/source", 'serviceUser_': 'serviceUser_', 'customeUser_': 'customeUser_', "media_image_url": "/itkfstatic/uploadImage/", "avatar_image_url": "/itkfstatic/avatar/" }
5.urls.py 配置
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('itkf/admin/', admin.site.urls), path('itkf/app/', include("app.urls")), ]
6. 项目名称下? >>?__init__.py? 文件配置
import pymysql #pymysql.version_info = (1, 3, 13, "final", 0) pymysql.install_as_MySQLdb()
7.应用名称(app) >>?models.py
from django.contrib.auth.models import User from django.db import models # Create your models here. from django.utils.html import format_html from django.db import models import datetime import uuid from django.db import models from django.contrib.auth.models import User # Create your models here. from django.utils.html import format_html from django.db.models import IntegerField, Model from django.core.validators import MaxValueValidator, MinValueValidator import datetime import random, os # Create your models here. from django.contrib.auth.models import AbstractUser from django.db import models ENV_PROFILE = os.getenv("ENV") if ENV_PROFILE == "test": import itkf.test_settings as config elif ENV_PROFILE == "production": import itkf.prd_settings as config else: import itkf.settings as config corpid = config.weChatWork["corpid"] sourceFile = config.weChatWork["sourceFile"] media_image_url = config.weChatWork["media_image_url"] def rename(newname): def decorator(fn): fn.__name__ = newname return fn return decorator def newImageName(instance, filename): filename = '{}.{}'.format(uuid.uuid4().hex, "png") return filename # 生成预约订单号 # 用时间生成一个唯一随机数 def random_with_N_digits(n): range_start = 10 ** (n - 1) range_end = (10 ** n) - 1 return random.randint(range_start, range_end) def get_ran_dom(): nowTime = datetime.datetime.now().strftime("%Y%m%d%H%M%S") # 生成当前时间 randomNum = random_with_N_digits(3) # 生成的随机整数n,其中0<=n<=100 if randomNum <= 10: randomNum = str(0) ◆ str(randomNum) uniqueNum = str(nowTime) ◆ str(randomNum) return uniqueNum # 应用管理 class agent(models.Model): name = models.CharField(max_length=225, verbose_name="部门名称", blank=True, default="") agentid = models.CharField(max_length=225, verbose_name="应用ID", blank=True, default="") secret = models.CharField(max_length=225, verbose_name="应用密钥", blank=True, default="") avatar = models.ImageField(max_length=225, verbose_name="部门Logo", blank=True, default="") conversationTime = models.IntegerField(verbose_name="会话时长(分钟)", default=20) webhook_url = models.URLField(verbose_name="群机器人地址", default="", blank=True, null=True) createTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") lastTime = models.DateTimeField(auto_now=True, verbose_name="修改时间") author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="创建者", related_name="agent_author") editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者", related_name="agent_creator") @rename("部门Logo") def showAvatar(self): return format_html("", media_image_url, self.avatar) @rename("详情") def checkMessage(self): return format_html("回复", self.id) class Meta: verbose_name = verbose_name_plural = '部门管理' ordering = ['id'] def __str__(self): return self.name # 客服人员 class KF(models.Model): agent = models.ForeignKey(agent, null=True, on_delete=models.CASCADE, verbose_name="应用名称") username = models.CharField(max_length=225, verbose_name="姓名", blank=True, default="") userid = models.CharField(max_length=225, verbose_name="UM", blank=True, default="") status = models.BooleanField(verbose_name="是否在线", default=False) avatar = models.ImageField(max_length=225, verbose_name="头像", blank=True, default="") createTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") lastTime = models.DateTimeField(auto_now=True, verbose_name="修改时间") author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="创建者", related_name="kf_author") editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者", related_name="kf_creator") class Meta: verbose_name = verbose_name_plural = '在线客服' ordering = ['id'] @rename("头像") def showAvatar(self): return format_html("", media_image_url, self.avatar) def __str__(self): return self.username # 行内员工 def randomSign(): switch = { 0: "只要还有明天,今天就永远是起跑线.", 1: "只要还有明天,今天就永远是起跑线.", 2: "只要还有明天,今天就永远是起跑线." } return switch[0] class userList(models.Model): agent = models.ForeignKey(agent, null=True, on_delete=models.CASCADE, verbose_name="应用名称") username = models.CharField(max_length=225, verbose_name="姓名", blank=True, default="") userid = models.CharField(max_length=225, verbose_name="UM", blank=True, default="") avatar = models.ImageField(max_length=225, verbose_name="头像", blank=True, default="") sign = models.CharField(max_length=225, verbose_name="个性签名", blank=True, default=randomSign) ISLEAD_CHOICES = ((0, '是'), (1, '否'),) islead = models.IntegerField(choices=ISLEAD_CHOICES, verbose_name="等级", default=1) createTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") lastTime = models.DateTimeField(auto_now=True, verbose_name="修改时间") author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="创建者", related_name="userlist_author") editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者", related_name="userlist_creator") @rename("头像") def showAvatar(self): return format_html("", media_image_url, self.avatar) class Meta: verbose_name = verbose_name_plural = '用户列表' ordering = ['id'] def __str__(self): return self.username # 接受的消息 class Message(models.Model): ToUserName = models.CharField(max_length=225, verbose_name="接受者", blank=True, default="") FromUserName = models.CharField(max_length=225, verbose_name="发送者", blank=True, default="") CreateTime = models.DateTimeField(verbose_name="发送时间", blank=True, default=None) MsgId = models.CharField(max_length=225, verbose_name="消息ID", blank=True, default="") AgentID = models.CharField(max_length=225, verbose_name="部门名称", blank=True, default="") MsgType = models.CharField(max_length=225, verbose_name="消息类型", blank=True, default="") content = models.TextField(max_length=2000, verbose_name="消息内容", blank=True, default="") userList = models.ForeignKey('userList', null=True, to_field="id", on_delete=models.CASCADE) createDateTime = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") lastTime = models.DateTimeField(auto_now=True, verbose_name="修改时间") author = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="创建者", related_name="message_author") editor = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE, verbose_name="修改者", related_name="message_creator") class Meta: verbose_name = verbose_name_plural = '所有消息' ordering = ['id'] def __str__(self): return self.FromUserName # 员工服务 class staffService(models.Model): agent = models.ForeignKey('agent', null=True, on_delete=models.CASCADE, verbose_name="应用名称") title = models.CharField(max_length=225, verbose_name="标题", blank=True, default="") avatar = models.ImageField(max_length=225, verbose_name="头像", blank=True, default="") desc = models.TextField(max_length=500, verbose_name="描述", default="", blank=True, null=True) welcomeText = models.TextField(max_length=2000, verbose_name="<