提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
前言
django下使用websocket实现即时通信并部署到iis服务器(无nginx)踩了许多坑,记录下。
一、安装对应库
python版本需要在3.7以上,博主使用的是python3.8。
pip install channels
版本如下
channels==3.0.1
daphne==3.0.2
二、使用步骤
1.后端配置:
settings.py:
INSTALLED_APPS = [
'channels',
]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
# 'CONFIG': {
# "hosts": [('127.0.0.1', 6379)],
# },
}
} #如需使用redis需要安装
ASGI_APPLICATION = 'your_project.asgi.application' # 新添加的,就是将wsgi都改成asgi
asgi.py:
"""
ASGI config for mpSimba project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mpSimba.settings')
django.setup()
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from . import routing
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": URLRouter(routing.websocket_urlpatterns)
})
在settings.py同目录下创建routing.py和consumers.py。
routing.py:
#这就是asgi的路由
from django.urls import re_path
from mpSimba import consumers
websocket_urlpatterns = [
re_path(r'wss/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
#对应url:var ws = new WebSocket(`ws://192.168.8.84:8100/wss/${comm_recId}/`);
#room_name是聊天室的名称
consumers.py:
#这是websocket的视图函数
import json
from connectModule import models
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import logging
logg = logging.getLogger('log')
class ChatConsumer(WebsocketConsumer):
def connect(self):
try:
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()
except Exception as e:
print(e)
logg.info(e)
def disconnect(self, close_code):
# logg.info('disconnect')
# 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):
# logg.info(time.time())
try:
text_data_json = json.loads(text_data)
print(text_data)
message = text_data_json['message']
if message == 'ping':
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
}
)
elif message == 'pic':
messageSender = text_data_json['messageSender']
recId = text_data_json['recId']
pic = text_data_json['pic']
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'messageSender': messageSender,
'recId': recId,
'pic': pic,
}
)
else:
messageSender = text_data_json['messageSender']
recId = text_data_json['recId']
print(text_data)
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'messageSender': messageSender,
'recId': recId,
}
)
except Exception as e:
logg.info(e)
print(e)
# Receive message from room group
def chat_message(self, event):
try:
message = event['message']
if event['message'] == 'ping':
self.send(text_data=json.dumps({
'message': message
}))
elif event['message'] == 'pic':
models.messageList.objects.create(recId=event['recId'], messageSender=event['messageSender'],
messageDescription=event['pic'], messageType=event['message'],
messageId=event['recId'], )
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message,
'pic': event['pic'],
'messageSender': event['messageSender'],
}))
else:
models.messageList.objects.create(recId=event['recId'], messageSender=event['messageSender'],
messageDescription=event['message'], messageId=event['recId'], )
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))
except Exception as e:
print(e)
以上就是后端的配置了。
后端思路:
建立连接后获取到路由中的"room_name"参数作为group的名称,这就是一个聊天室
var ws = new WebSocket(`ws://192.168.8.84:8100/wss/70/`);
收到用户发送的数据会调用receive接口这个时候就要做个判断,如果是心跳,那么就不要发送回前端,如果是图片,或者文字,那么就做一下对应的处理 。一定要注意'type'='chat_message'不能漏。
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
})
如果需要做数据处理,那么就在 def chat_message:下做对应处理就好。
2.前端代码:
html部分:
<div class="" v-if="cusReplyType=='需要文字回复'&&link_communicationFlag=='会话未结束'">
<!-- 上半部分-->
<div class="" id="chat" ref="chat"
style="overflow: scroll;background-color: rgb(235,235,235);padding-top: 0.4rem">
<div class="you">
<div class="">
<div class="you_pic">
<van-image
round
width="3rem"
height="3rem"
src="https://img01.yzcdn.cn/vant/cat.jpeg"
/>
</div>
<div class="" style="font-size: 0.15rem">技术支持</div>
</div>
<div class="you_msg">
<div class="tip-trangle-left"></div>
<span style="text-align: left;font-size: 0.4rem;">您好,我是您的技术支持,请问有什么可以帮到您?</span>
</div>
</div>
</div>
<!-- 下半部分-->
<div class=""
style="position: fixed;bottom:0rem;width: 100%;background-color: rgb(245,245,245);">
<van-row style="
display: flex;align-items:center;padding:0.3rem">
<van-col span="3">
<van-uploader :after-read="afterRead">
<van-icon name="photo-o"/>
</van-uploader>
</van-col>
<van-col span="18"><input style="width: 90%;height: 1.7rem;" type="text"
id="chat_message" disabled
placeholder="请输入内容" v-model="words"></van-col>
<van-col span="3">
<div class="">
<van-button type="info" size="small" @click="sendMsg()">发送</van-button>
</div>
</van-col>
</van-row>
</div>
</div>
js部分:
<script src="../../static/jquery/jquery-3.6.0.js"></script>
<script src="../../static/js/vant.js"></script>
var heart_beat
//var ws = new WebSocket(`wss://mpalpha.chinasimba.com:8803/wss/70`); 实测wss可以带端口-_-
var ws = new WebSocket(`ws://127.0.0.1:8100/wss/70/`);
var inpval = ''
var messageSender = ''
if (kf) {
messageSender = 'kefu'
} else {
messageSender = 'cus'
}
//心跳包内容
var heartBeat = {
'message': "ping",
'timestamp': new Date().getTime()
}
// 1 握手环节验证成功后自动触发 onopen
ws.onopen = function () {
alert('握手成功!')
heart_beat = setInterval(() => {
{#ws.send(JSON.stringify({'message': 'ping'}))#}
ws.send(JSON.stringify(heartBeat))
}, 20000)
$(`#chat_message`).prop('disabled', false);
}
// 2 给服务端发送消息 人为触发 send
function sendMsg() {
if (kf) {
$('#chat').append(`<div class="you"><div><div class="you_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div> <div style="font-size: 0.15rem;">技术支持</div></div> <div class="you_msg"><div class="tip-trangle-left"></div> <span style="text-align: left; font-size: 0.4rem;">${$('#chat_message').val()}</span></div></div>`)
} else {
$('#chat').append(`<div class="me"><div class="me_msg"><div class="tip-trangle-right"></div> <span style="text-align: left; font-size: 0.4rem;">${$('#chat_message').val()}</span></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)
}
ws.send(JSON.stringify({
'message': $('#chat_message').val(),
'messageSender': messageSender,
'recId': comm_recId,
}))
inpval = $('#chat_message').val()
$('#chat_message').val('')
}
// 3 服务端一旦有了消息 自动触发 onmessage
ws.onmessage = function (args) {
console.log('args:', (args))
if (JSON.parse(args.data).message != 'ping') {
if (kf) {
console.log('kf')
if (inpval != JSON.parse(args.data).message) {
if (JSON.parse(args.data).message == 'pic') {
if (JSON.parse(args.data).messageSender == 'kefu') {
$('#chat').append(`<div class="you" style="height: auto;"><div><div class="you_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div> <div style="font-size: 0.15rem;">技术支持</div></div> <div class="you_msg"><div class="tip-trangle-left"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div></div>`)
} else {
$('#chat').append(`<div class="me" style="height: auto;"><div class="me_msg" style="height: 4rem;"><div class="tip-trangle-right"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;width: 3rem"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)
}
} else {
$('#chat').append(`<div class="me"><div class="me_msg"><div class="tip-trangle-right"></div> <span style="text-align: left; font-size: 0.4rem;">${JSON.parse(args.data).message}</span></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)
}
inpval = ''
} else {
inpval = ''
}
} else {
if (inpval != JSON.parse(args.data).message) {
if (JSON.parse(args.data).message == 'pic') {
if (JSON.parse(args.data).messageSender == 'kefu') {
$('#chat').append(`<div class="you" style="height: auto;"><div><div class="you_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div> <div style="font-size: 0.15rem;">技术支持</div></div> <div class="you_msg"><div class="tip-trangle-left"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div></div>`)
} else {
$('#chat').append(`<div class="me" style="height: auto;"><div class="me_msg" style="height: 4rem;"><div class="tip-trangle-right"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;width: 3rem"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)
}
{#$('#chat').append(`<div class="me" style="height: auto;"><div class="me_msg" style="height: 4rem;"><div class="tip-trangle-right"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;width: 3rem"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)#}
{#$('#chat').append(`<div class="you" style="height: auto;"><div><div class="you_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div> <div style="font-size: 0.15rem;">技术支持</div></div> <div class="you_msg"><div class="tip-trangle-left"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div></div>`)#}
{#$('#chat').append(`<div class="me" style="height: auto;"><div class="me_msg" style="height: 4rem;"><div class="tip-trangle-right"></div> <div class="van-image" style="height: 4rem;width: 4rem;"><img onclick="vant.ImagePreview(['${location.origin + '/' + JSON.parse(args.data).pic}']);" src="${location.origin + '/' + JSON.parse(args.data).pic}" class="van-image__img"></div></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;width: 3rem"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)#}
} else {
$('#chat').append(`<div class="you"><div><div class="you_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div> <div style="font-size: 0.15rem;">技术支持</div></div> <div class="you_msg"><div class="tip-trangle-left"></div> <span style="text-align: left; font-size: 0.4rem;">${JSON.parse(args.data).message}</span></div></div>`)
{#$('#chat').append(`<div class="me"><div class="me_msg"><div class="tip-trangle-right"></div> <span style="text-align: left; font-size: 0.4rem;">${JSON.parse(args.data).message}</span></div> <div class="me_pic"><div class="van-image van-image--round" style="width: 3rem; height: 3rem;"><img src="https://img01.yzcdn.cn/vant/cat.jpeg" class="van-image__img"></div></div></div>`)#}
}
inpval = ''
} else {
inpval = ''
}
}
}
};
// 4 断开链接之后 自动触发 onclose
ws.onclose = function () {
{#ws.close()#}
console.log('断开连接。ws连接状态:' + ws.readyState);
clearInterval(heart_beat)
{#ws = new WebSocket('wss://mpalpha.chinasimba.com:8803/wss/${comm_recId}/');#}
const heart_again = setInterval(() => {
ws = new WebSocket(`wss://mpalpha.chinasimba.com:8803/wss/${comm_recId}/`);
{#ws = new WebSocket(`ws://127.0.0.1:8100/wss/${comm_recId}/`);#}
}, 30000)
{#ws = new WebSocket(`ws://127.0.0.1:8100/wss/${comm_recId}/`);#}
};
前端思路:
前端的主要思路就是建立websocket连接,然后定时发送心跳包维持websocket,时间大概20秒一次就足够了。当发送消息后调用ws.sent(),接收后端传来的websocket的时候会有标识比如发送者,根据标识区分是谁发送的信息并渲染到页面上就可以。
3.一对一和群聊:
上面的代码主要还是通过群聊的方式,双方在一个房间内聊天,消息的话根据发送方不同来做处理。只需要定义一个room_name作为group_room就可以了,问题是一对一该如何实现呢?
博主的思路是:用户A和用户B在实时通讯的时候,将url中的room_name改成A的id_b的id,按照大小来排列拼接。例如a.id=563543,b.id=432534,那么ws的url就是ws://127.0.0.1:8000/wss/432534_563543,按照大小排列的方式就可以保证ab同时在同一个room内。当然我的想法仅供参考,有更好的方案评论区指教。
4.发送图片:
先调用上传图片接口,上传成功后前端再调用ws.send()执行,做个标识例如"type"="pic"后端判断后返回图片url。前端拿到后渲染即可。具体可以去上面代码里面找。
5.daphne的启动和部署:
在编译器上会自动帮你启动。出现这个ASGI/Channels就是成功了。
重点来了:那么如何在服务器上启动呢,博主要在iis服务器上启动websocket服务,但是由于iis只支持wsgi协议,不支持asgi,导致无法支持websocket。所以只能使用命令行启动,可以直接在项目根目录用CMD打开或者写一个.bat批处理文件来启动它。刚刚安装channels的时候,已经顺便安装好了daphne服务器,这是channels官方推荐的服务器。我们只需要启动daphne服务即可。
http不带证书启动:
@echo off
daphne -p 8700 mpSimba.asgi:application
pause
# 使用批处理文件可以按我这样写,如果手动CMD打开那只要中间哪一行即可。意思是在127.0.0.1:8700端口启动服务
https带证书启动:
如果你的站点的https的,需要你下载证书并且配置,一定要检查cmd的环境是否正确,如果服务器上环境错了,会报错,最好是进入你项目的虚拟环境中。网上也有许多文章可以借鉴。
@echo off
D:\wwwroot\xxx\venv\Scripts\daphne.exe -e ssl:8803:privateKey=xxx.com.key:certKey=xxx.com.crt your_project.asgi:application
pause
#或
@echo off
daphne -e ssl:8803:privateKey=xxx.com.key:certKey=xxx.com.crt your_project.asgi:application
pause
总结
nginx代理的部署方案太多了,就不写了。在做需求的时候,实际上卡的最久的就是服务器上的部署问题,要确定服务器能不能支持asgi启动,如果不行,就使用命令行手动启动,服务器的运行环境一定要确认清楚。如果想要提高并发能力,就把consumers.py中的同步方法去掉,使用异步写法。连接服务端websocket的时候,不要挂代理,IP变了就只能重启电脑才能连了。