Django利用Channels+websocket开发聊天室完整案例
作者:Aiwin-Hacker
前言
数据库系统课程设计要求,要开发一个B2B的售卖平台,本来开发浅薄的我,粗糙又基础的完成了一些基本的功能,想要开发一个单独的一对一聊天的功能(类似于微信这类),查阅了不少资料,依旧没思路,但是却知晓了服务器推送信息和聊天室的开发,记个笔记。
提示:以下是本篇文章正文内容,下面案例可供参考
一、什么是Websocket?
1,Websocket的诞生背景:网站为了实现推送技术,用的基本是轮询,轮询是基于浏览器不断对服务器发出HTTP请求,服务器范围最新数据给客户端浏览器,这种模式缺点明显,带宽浪费严重,在这种背景下,HTML5诞生了Websocket协议,能更好的节省服务器资源并实现通讯。
2,WebSocket特点:WebSocket是单个TCP连接上的全双工通信,浏览器和服务器只需要完成一次握手,就可以创建持久性的连接,实现数据的双向传输。
二、Python-Django ASGI
1,WSGI:Python Web Server Gateway Interface,就是Web服务器网关接口,主要是规范了Web服务器和Web应用之间的交互,WSGI将Django分成了三类,服务器,APP,中间件。服务器就是用来监听某端口,APP用来调用某个函数,而中间件则位于两者中间,相当于一道门,起审核承接等作用。但是WSGI始终是为同步世界编写的,无法编写异步对象。因此,ASGI诞生了。
2,ASGI:Async Sever Gateway Interface,说白了,就是相当于WSGI+异步功能,而要调用WebSocket,Django就需要使用ASGI接口。
三、Django开发聊天室或信息推送
第一步:下载Channels模块
pip install channels
第二步:创建Django项目
Django-admin startproject websocket
创建后会看到Django3.0以后自存在asgi.py文件
第三步:创建一个APP应用
python manage.py startapp websocket_demo
第四步:进入settings.py模块,进行配置
首先在INSTALLED_APPS添加channels和websocket_demo
然后添加再在settings中添加ASGI应用
第五步:在创建的APP中创建routing.py文件以及进入asgi.py模块进行修改如下:
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from websocket_demo import routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_socket.settings') application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": URLRouter(routing.websocket_urlpatterns), })
图中代码很容易理解:将服务器的请求分成了两类,http请求则走默认的asgi APP流程,是Websocket请求则进入URL路由,即websocket_demo创建的routing.py文件
第六步,在websocket_demo的APP中views视图下创建chat.py并且配置routing.py的文件:
from django.urls import re_path from websocket_demo.views import chat websocket_urlpatterns = [ re_path(r'chat/(?P<group>\w+)/$', chat.ChatConsumer.as_asgi()), ]
使用正则路径匹配chat/数字/的路径,是则进入chat.py文件中的CharConsumer函数
第七步,在urls.py中配置路由,指向websocket_demo中的chat.py文件
from django.urls import path, include from websocket_demo.views import chat urlpatterns = [ path('index/', chat.chat) ]
第八步,编写chat.py文件的内容如下:
from django.shortcuts import render from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer from asgiref.sync import async_to_sync from datetime import datetime def chat(request): group_number = request.GET.get('num') return render(request, 'web/index.html', context) class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): self.accept() group = self.scope['url_route']['kwargs'].get("group") async_to_sync(self.channel_layer.group_add)(group, self.channel_name) def websocket_receive(self, message): group = self.scope['url_route']['kwargs'].get("group") async_to_sync(self.channel_layer.group_send)(group, {"type": "chat", 'message': message}) def chat(self, event): text = event['message']['text'] self.send(text) def websocket_disconnect(self, message): group = self.scope['url_route']['kwargs'].get("group") async_to_sync(self.channel_layer.group_discard)(group, self.channel_name) print('客户端断开连接了') raise StopConsumer()
首先第一个函数的意思是,进入index页面后会先进入chat函数,获取URL中请求的参数num,渲染到index.html页面,然后index.html页面使用javascript发起websocket请求,发起websocket请求后,ASGI服务器通过"websocket": URLRouter(routing.websocket_urlpatterns)路由访问routing文件,routing文件指向了ChatConsumer(WebsocketConsumer)类。
第二个函数ChatConsumer(WebsocketConsumer)类对websocket请求进行了处理,接收websocket的请求后获取组号即num的值,然后接收websocket发送的信息后调用chat函数将收到的信息同等的发送回去,websocket_disconnet则是客户端断开请求后触发的函数。
前端index.html的文件如下:
reset.min.css文件:
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}
style.css文件:
*, *:before, *:after { box-sizing: border-box; } :root { --white: #fff; --black: #000; --bg: #f8f8f8; --grey: #999; --dark: #1a1a1a; --light: #e6e6e6; --wrapper: 1000px; --blue: #00b0ff; } body { background-color: var(--bg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; font-family: 'Source Sans Pro', sans-serif; font-weight: 400; background-image: url("../img/image.jpg"); background-size: cover; background-repeat: none; } .wrapper { position: relative; left: 50%; width: var(--wrapper); height: 800px; -webkit-transform: translate(-50%, 0); transform: translate(-50%, 0); } .container { position: relative; top: 50%; left: 50%; width: 80%; height: 75%; background-color: var(--white); -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); } .container .left { float: left; width: 37.6%; height: 100%; border: 1px solid var(--light); background-color: var(--white); } .container .left .top { position: relative; width: 100%; height: 96px; padding: 29px; } .container .left .top:after { position: absolute; bottom: 0; left: 50%; display: block; width: 80%; height: 1px; content: ''; background-color: var(--light); -webkit-transform: translate(-50%, 0); transform: translate(-50%, 0); } .container .left input { float: left; width: 188px; height: 42px; padding: 0 15px; border: 1px solid var(--light); background-color: #eceff1; border-radius: 21px; font-family: 'Source Sans Pro', sans-serif; font-weight: 400; } .container .left input:focus { outline: none; } .container .left a.search { display: block; float: left; width: 42px; height: 42px; margin-left: 10px; border: 1px solid var(--light); background-color: var(--blue); background-image: url("../img//name-type.png"); background-repeat: no-repeat; background-position: top 12px left 14px; border-radius: 50%; } .container .left .people { margin-left: -1px; border-right: 1px solid var(--light); border-left: 1px solid var(--light); width: calc(100% + 2px); } .container .left .people .person { position: relative; width: 100%; padding: 12px 10% 16px; cursor: pointer; background-color: var(--white); } .container .left .people .person:after { position: absolute; bottom: 0; left: 50%; display: block; width: 80%; height: 1px; content: ''; background-color: var(--light); -webkit-transform: translate(-50%, 0); transform: translate(-50%, 0); } .container .left .people .person img { float: left; width: 40px; height: 40px; margin-right: 12px; border-radius: 50%; } .container .left .people .person .name { font-size: 14px; line-height: 22px; color: var(--dark); font-family: 'Source Sans Pro', sans-serif; font-weight: 600; } .container .left .people .person .time { font-size: 14px; position: absolute; top: 16px; right: 10%; padding: 0 0 5px 5px; color: var(--grey); background-color: var(--white); } .container .left .people .person .preview { font-size: 14px; display: inline-block; overflow: hidden !important; width: 70%; white-space: nowrap; text-overflow: ellipsis; color: var(--grey); } .container .left .people .person.active, .container .left .people .person:hover { margin-top: -1px; margin-left: -1px; padding-top: 13px; border: 0; background-color: var(--blue); width: calc(100% + 2px); padding-left: calc(10% + 1px); } .container .left .people .person.active span, .container .left .people .person:hover span { color: var(--white); background: transparent; } .container .left .people .person.active:after, .container .left .people .person:hover:after { display: none; } .container .right { position: relative; float: left; width: 62.4%; height: 100%; } .container .right .top { width: 100%; height: 47px; padding: 15px 29px; background-color: #eceff1; } .container .right .top span { font-size: 15px; color: var(--grey); } .container .right .top span .name { color: var(--dark); font-family: 'Source Sans Pro', sans-serif; font-weight: 600; } .container .right .chat { position: relative; display: none; overflow: hidden; padding: 0 35px 92px; border-width: 1px 1px 1px 0; border-style: solid; border-color: var(--light); height: calc(100% - 48px); justify-content: flex-end; flex-direction: column; } .container .right .chat.active-chat { display: block; display: flex; } .container .right .chat.active-chat .bubble { transition-timing-function: cubic-bezier(0.4, -0.04, 1, 1); } .container .right .chat.active-chat .bubble:nth-of-type(1) { -webkit-animation-duration: 0.15s; animation-duration: 0.15s; } .container .right .chat.active-chat .bubble:nth-of-type(2) { -webkit-animation-duration: 0.3s; animation-duration: 0.3s; } .container .right .chat.active-chat .bubble:nth-of-type(3) { -webkit-animation-duration: 0.45s; animation-duration: 0.45s; } .container .right .chat.active-chat .bubble:nth-of-type(4) { -webkit-animation-duration: 0.6s; animation-duration: 0.6s; } .container .right .chat.active-chat .bubble:nth-of-type(5) { -webkit-animation-duration: 0.75s; animation-duration: 0.75s; } .container .right .chat.active-chat .bubble:nth-of-type(6) { -webkit-animation-duration: 0.9s; animation-duration: 0.9s; } .container .right .chat.active-chat .bubble:nth-of-type(7) { -webkit-animation-duration: 1.05s; animation-duration: 1.05s; } .container .right .chat.active-chat .bubble:nth-of-type(8) { -webkit-animation-duration: 1.2s; animation-duration: 1.2s; } .container .right .chat.active-chat .bubble:nth-of-type(9) { -webkit-animation-duration: 1.35s; animation-duration: 1.35s; } .container .right .chat.active-chat .bubble:nth-of-type(10) { -webkit-animation-duration: 1.5s; animation-duration: 1.5s; } .container .right .write { position: absolute; bottom: 29px; left: 30px; height: 42px; padding-left: 8px; border: 1px solid var(--light); background-color: #eceff1; width: calc(100% - 58px); border-radius: 5px; } .container .right .write input { font-size: 16px; float: left; width: 347px; height: 40px; padding: 0 10px; color: var(--dark); border: 0; outline: none; background-color: #eceff1; font-family: 'Source Sans Pro', sans-serif; font-weight: 400; } .container .right .write .write-link.attach:before { display: inline-block; float: left; width: 20px; height: 42px; content: ''; background-image: url("../img/attachment.png"); background-repeat: no-repeat; background-position: center; } .container .right .write .write-link.smiley:before { display: inline-block; float: left; width: 20px; height: 42px; content: ''; background-image: url("../img/smiley.png"); background-repeat: no-repeat; background-position: center; } .container .right .write .write-link.send:before { display: inline-block; float: left; width: 20px; height: 42px; margin-left: 11px; content: ''; background-image: url("../img/send.png"); background-repeat: no-repeat; background-position: center; } .container .right .bubble { font-size: 16px; position: relative; display: inline-block; clear: both; margin-bottom: 8px; padding: 13px 14px; vertical-align: top; border-radius: 5px; } .container .right .bubble:before { position: absolute; top: 19px; display: block; width: 8px; height: 6px; content: '\00a0'; -webkit-transform: rotate(29deg) skew(-35deg); transform: rotate(29deg) skew(-35deg); } .container .right .bubble.you { float: left; color: var(--white); background-color: var(--blue); align-self: flex-start; -webkit-animation-name: slideFromLeft; animation-name: slideFromLeft; } .container .right .bubble.you:before { left: -3px; background-color: var(--blue); } .container .right .bubble.me { float: right; color: var(--dark); background-color: #eceff1; align-self: flex-end; -webkit-animation-name: slideFromRight; animation-name: slideFromRight; } .container .right .bubble.me:before { right: -3px; background-color: #eceff1; } .container .right .conversation-start { position: relative; width: 100%; margin-bottom: 27px; text-align: center; } .container .right .conversation-start span { font-size: 14px; display: inline-block; color: var(--grey); } .container .right .conversation-start span:before, .container .right .conversation-start span:after { position: absolute; top: 10px; display: inline-block; width: 30%; height: 1px; content: ''; background-color: var(--light); } .container .right .conversation-start span:before { left: 0; } .container .right .conversation-start span:after { right: 0; } @keyframes slideFromLeft { 0% { margin-left: -200px; opacity: 0; } 100% { margin-left: 0; opacity: 1; } } @-webkit-keyframes slideFromLeft { 0% { margin-left: -200px; opacity: 0; } 100% { margin-left: 0; opacity: 1; } } @keyframes slideFromRight { 0% { margin-right: -200px; opacity: 0; } 100% { margin-right: 0; opacity: 1; } } @-webkit-keyframes slideFromRight { 0% { margin-right: -200px; opacity: 0; } 100% { margin-right: 0; opacity: 1; } }
index.js文件:
document.querySelector('.chat[data-chat=person2]').classList.add('active-chat'); document.querySelector('.person[data-chat=person2]').classList.add('active'); var friends = { list: document.querySelector('ul.people'), all: document.querySelectorAll('.left .person'), name: '' }, chat = { container: document.querySelector('.container .right'), current: null, person: null, name: document.querySelector('.container .right .top .name') }; friends.all.forEach(function (f) { f.addEventListener('mousedown', function () { f.classList.contains('active') || setAciveChat(f); }); }); function setAciveChat(f) { friends.list.querySelector('.active').classList.remove('active'); f.classList.add('active'); chat.current = chat.container.querySelector('.active-chat'); chat.person = f.getAttribute('data-chat'); chat.current.classList.remove('active-chat'); chat.container.querySelector('[data-chat="' + chat.person + '"]').classList.add('active-chat'); friends.name = f.querySelector('.name').innerText; chat.name.innerHTML = friends.name; }
index.html文件:
{% load static from static %} <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>聊天窗口界面</title> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet"> <link rel="stylesheet" href="{% static 'web/chat/css/reset.min.css' %}"> <link rel="stylesheet" href="{% static 'web/chat/css/style.css' %}"> </head> <body> <div class="wrapper"> <div class="container"> <div class="left"> <div class="top"> <input type="text" placeholder="Search" /> <a href="javascript:;" class="search"></a> </div> <ul class="people"> <li class="person" data-chat="person2"> <img src="/static/myadmin/dist/img/user2-160x160.jpg" alt="" /> <span class="name">{{admin_name}}</span> <span class="preview">I was wondering...</span> </li> </ul> </div> <div class="right"> <div class="top"><span>To: <span class="name">{{admin_name}}</span></span></div> <div class="chat" data-chat="person2" id="chat-send" > <div class="conversation-start"> <span>{{time}}</span> </div> </div> <div class="write"> <a href="javascript:;" class="write-link attach"></a> <input type="text" id="text"/> <a href="javascript:;" class="write-link smiley"></a> <a onclick="sendMessage()" class="write-link send"></a> </div> </div> </div> </div> <script src="{% static 'web/chat/js/index.js' %}"></script> <script type="text/javascript"> var socket=new WebSocket("ws://127.0.0.1:8000/chat/{{group_number}}/"); socket.onopen = function(event) { var tag=document.getElementById("chat-send"); var d=document.createElement("div"); d.className="bubble you"; d.innerHTML ="连接成功"; tag.appendChild(d); } function sendMessage(){ let text=document.getElementById("text"); var tag=document.getElementById("chat-send"); var d=document.createElement("div"); d.className="bubble me"; d.innerHTML =text.value; tag.appendChild(d); socket.send(text.value); text.value=" "; } socket.onclose = function(){ var tag=document.getElementById("chat-send"); var d=document.createElement("div"); d.className="bubble you"; d.innerHTML ="服务器主动断开连接"; tag.appendChild(d); } socket.onmessage=function(event){ var tag=document.getElementById("chat-send"); var d=document.createElement("div"); d.className="bubble you"; d.innerHTML =event.data; tag.appendChild(d); } </script> <div style="text-align:center;margin:1px 0; font:normal 14px/24px 'MicroSoft YaHei';"> </div> </body> </html>
主要看javascript使用的函数,首先进行Websocket请ws://127.0.0.1:8000/chat/{{group_number}}/,然后分别的通过websocket发送信息,接受信息。
第九步,启动manage.py文件,同时打开两个页面观看效果:
可以看到,某个用户发送的信息,将会被服务器发送到同一聊天室的所有客户端,实现了信息推送的功能。
但是要实现一对一聊天的功能,目前实现是没有思路,有大神看到烦请指点指点。
总结
到此这篇关于Django利用Channels+websocket开发聊天室的文章就介绍到这了,更多相关Django开发聊天室内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!