网站部署在华为云服务器上,Debian系统,使用Django+Nginx+uwsgi搭建。最终效果如下图所示。
一、响应逻辑顺序
1. 聊天页面请求
客户端请求/chat/(输入聊天室房间号界面)和/chat/room_name(某个聊天室页面)链接时,由Nginx转到Django由urls.py解析并返回相应页面,在返回的聊天室页面内置了javascript程序,请求建立wss:/ws/chat/room_name的websocket连接。
2. websocket连接请求
客户端向Nginx发送websocket连接请求,根据Ngnix配置文件location /ws {}中设置的反向代理,请求被转到本地的7001端口( http://127.0.0.1:7001),将由运行在该端口上的由daphne托管的asgi服务解析并响应(配置文件为myproject/asgi.py)。
3. asgi.py解析并响应
asgi.py中定义的application定义了websocket的解析规则,具体由/myproject/routing.py中的websocket_urlpatterns进行地址解析,通过Channels由myapp/comsumers.py进行异步信息处理并响应。
二、配置Channels和asgi
1. 安装Channels
sudo pip3 install channels
2. settings.py中添加应用并配置channels和asgi
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.admin',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'myapp',
]
ASGI_APPLICATION = 'myproject.asgi.application'
# Channels_Layers相关配置
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
# 配置认证后端
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
Django Channels利用 Redis 作为通道层来管理客户端和服务器之间的通信。当客户端通过 WebSocket 连接发送消息时,Django Channels 会使用 Redis 将消息分发到所有连接的客户端。Redis 充当消息代理,实时有效地处理消息的路由和传递。
ASGI_APPLICATION指向Django项目下的asgi.py文件中的application。
3. 配置asgi.py文件
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from myproject import routings
application = ProtocolTypeRouter({
'http':get_asgi_application(), #支持http请求
'websocket':AuthMiddlewareStack(
URLRouter(routings.websocket_urlpatterns) #支持webscoket请求
),
})
此处django.setup()和DJANGO_SETTINGS_MODULE设置应放在导入模块前,否则容易出现如下报错:
django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.
Requested setting DEBUG, but settings are not configured.
You must either define the environment variable DJANGO_SETTINGS_MODULE
or call settings.configure() before accessing settings...
WSGI和ASGI 的主要区别是同步VS异步,WSGI是同步的,每个请求必须等待前一个请求完成。而ASGI是异步的,可以同时处理多个请求。 WSGI主要用于HTTP协议,ASGI旨在支持WebSocket、HTTP2等协议。ASGI的主要特点是异步非阻塞,它能够更好地处理并发请求。
上述设置中websocket地址解析由Django项目下的routings.py完成,其功能与urls.py类似。
4. 配置routings.py
from django.urls import path
from myapp import consumers
websocket_urlpatterns = [
path(r'ws/chat/<str:room_name>/', consumers.ChatConsumer.as_asgi())
]
myapp为新建的应用,其下的consumers.py文件定义了websocket的响应函数ChatConsumer。
5. 配置myapp/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async
from myapp.models import ChatMessage
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope["url_route"]["kwargs"].get("room_name") #获取传输的房间号
if self.room_name:
self.room_group_name = f"chat_{self.room_name}"
# Join room group
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
await self.accept()
else:
await self.close()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
username = text_data_json["username"]
#保存消息
await self.save_message(username, self.room_name, message)
# 向群组内的所有客户端发送消息,type为“chat_message"
await self.channel_layer.group_send(
self.room_group_name, {"type": "chat_message", "message": message,'username':username}
)
# Receive message from room group
async def chat_message(self, event):
message = event["message"]
username = event['username']
# Send message to WebSocket
await self.send(text_data=json.dumps({"message": message,'username':username}))
# 信息写入数据库
@sync_to_async()
def save_message(self, username, room, message):
if len(message) !=0 :
newmsg = ChatMessage.objects.create(username=username, room=room, content=message)
newmsg.save()
文件中定义了websocket的连接,消息的接收、发送和存储,每条消息在发送给群组内的所有客户端前先存储到数据库。
三、消息存储
1. 配置myapp/models.py,用于消息存储
from django.db import models
class ChatMessage(models.Model):
username = models.CharField(max_length=100,verbose_name='用户名')
room = models.CharField(max_length=100)
content = models.TextField(verbose_name='内容')
create_time = models.DateTimeField(auto_now_add=True,verbose_name='写入时间')
def __str__(self):
return f"房间号:{self.room}--用户:{self.username}({self.content})"
2. 配置myapp/admin.py,便于在Django后台处理存储的消息
from django.contrib import admin
# Register your models here.
from .models import ChatMessage
admin.site.register(ChatMessage)
效果如下:
四、安装redis并启动
1. 安装redis
sudo apt-get install redis-server
安装后redis-server会自动启动,可以使用sudo systemctl start/stop/restart/status redis.service来启动/停止/重启/查看服务器。
$sudo systemctl status redis.service
● redis-server.service - Advanced key-value store
Loaded: loaded (/lib/systemd/system/redis-server.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2024-11-19 23:38:28 CST; 20h ago
Docs: http://redis.io/documentation,
man:redis-server(1)
Main PID: 123767 (redis-server)
Status: "Ready to accept connections"
Tasks: 5 (limit: 2321)
Memory: 7.2M
CPU: 1min 36.022s
CGroup: /system.slice/redis-server.service
└─123767 /usr/bin/redis-server 127.0.0.1:6379
五、安装daphne,并启动asgi服务
1. 安装daphne
Daphne 是 Django Channels 项目的一部分,专门用于为 Django 提供支持 WebSocket和 ASGI 协议的异步服务器。
sudo pip3 install daphne
启动asgi服务的命令如下,监听端口为7001。
daphne -p 7001 myproject.asgi:application
asgi服务的配置文件即为前面已经完成的asgi.py。
为了管理方便,将其做成系统服务开机启动。
2. 设置daphne.service服务
新建/etc/systemd/system/daphne.service文件
sudo nano /etc/systemd/system/daphne.service
内容如下:
[Unit]
Description=Daphne Server
After=network.target
[Service]
Type=exec
WorkingDirectory=/home/yislwll/Django/myproject
ExecStart=/usr/bin/daphne -p 7001 myproject.asgi:application
Restart=always
User=yislwll
Group=Yisl
Environment=PYTHONPATH=/home/yislwll/Django/myproject
[Install]
WantedBy=multi-user.target
然后启动daphne服务:
sudo systemctl enable daphne.service
sudo systemctl start daphne.service
需要特别注意的是,修改consumers.py文件后一定要重启daphne服务,否则对websocket通讯内容的更改无法更新。
sudo systemctl restart daphne.service
六、配置Nginx
根据响应逻辑,Ngnix应该对websocket的连接“wss://域名/ws/chat/room_name”做出响应,请求将被代理到本地的7001端口,由运行在该端口上由daphne托管的asgi服务解析并响应。
1. 配置/etc/nginx/nginx.conf
网站为https://连接,所以webscoket连接为wss://
server {
listen 443 ssl;
server_name xxxx.com;
charset utf-8;
ssl_certificate /path/to/fullchain.pem; # SSL证书的路径
ssl_certificate_key /path/to/privkey.pem; # 私钥的路径
# SSL 配置
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 4000 sessions
ssl_session_tickets off;
# 指定加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'xxxxxxxxx';
ssl_prefer_server_ciphers on;
location / {#由django响应
uwsgi_pass 127.0.0.1:8080;
include uwsgi_params;
include /etc/nginx/uwsgi_params;
uwsgi_param UWSGI_SCRIPT iCourse.wsgi;
uwsgi_param UWSGI_CHDIR /iCourse;
index index.html index.htm;
client_max_body_size 35m;
index index.html index.htm;
}
location /ws {#wss协议由asgi服务响应
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Nginx-Proxy true;
proxy_redirect off;
client_max_body_size 10m;
proxy_pass http://127.0.0.1:7001;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
2. 重启Nginx服务
sudo systemctl restart nginx.service
七、客户端界面
1. 配置urls.py,响应客户端界面请求
from myapp import views as channelsview
urlpatterns = [
......
path('chat/',channelsview.chat, name='webchat'),
path('chat/<str:room_name>/', channelsview.chatroom, name='chatroom'),
......
]
客户端页面请求将由myapp/views.py来处理。
2. 配置views.py
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from .models import ChatMessage
# Create your views here.
@login_required(login_url='/login/')
def chat(request):
return render(request,"channels/chattingindex.html")
@login_required(login_url='/login/')
def chatroom(request,room_name):
username = request.session.get('username','游客')
msgs = ChatMessage.objects.filter(room=room_name).order_by('-create_time')[0:20]
return render(request,"channels/chattingroom.html",{'room_name':room_name,'username':username, 'msgs':msgs})
在chatroom函数中,对当前聊天室历史消息的最近20条进行了查询,并由页面展示出来。数据库查询的时候使用'-create_time'倒序选取前20条,在模板文件chattingroom.html中还要使用{% for m in msgs reversed %}来正序展示。
3. 聊天页面设计及通讯
chattingroom.html文件内容大致如下:
{% extends "newdesign/newbase.html" %}
{% load django_bootstrap5 %}
{% block mytitle %}
<title>{{room_name}}号聊天室</title>
<style>
.chat-window {
max-width: 900px;
height: 500px;
overflow-y: scroll; /* 添加垂直滚动条 */
margin: auto;
background-color: #f1f1f1;
border: 2px solid #09e3f7;
border-radius: 5px;
padding: 10px;
}
.chat-message {
clear: both;
overflow: hidden;
margin-bottom: 10px;
text-align: left;
}
.chat-message .message-content {
border-radius: 5px;
padding: 8px;
max-width: 500px;
float: left;
clear: both;
}
.chat-message.right .message-content {
background-color: #428bca;
color: white;
float: right;
width:420px;
}
.chat-message.right .user-content {
background-color: #f7e91d;
border-radius:4px;
color: black;
float: right;
width: auto;
text-align: right;
padding-left:10px;
padding-right:10px;
}
.chat-message.left .message-content {
background-color: #2ef3be;
border-color: #ddd;
float:left;
width:420px;
}
.chat-message.left .user-content {
background-color: #f7e91d;
border-radius:4px;
border-color: #ddd;
float: left;
width: auto;
text-align: left;
padding-left:8px;
padding-right:8px;
}
</style>
{% endblock %}
{% block maincontent %}
<div class="container">
<div id="chat-record" class="chat-window">
{% for m in msgs reversed %}
{% if m.username == request.user.username %}
<div class="chat-message right"><div class="user-content">{{m.username}}</div><div class="message-content"><span>{{m.content}}</span></div></div>
<br>
{% else %}
<div class="chat-message left"><div class="user-content">{{m.username}}</div><div class="message-content"><span>{{m.content}}</span></div></div>
<br>
{% endif %}
{% endfor %}
</div>
</div>
<br>
<div class="mx-auto px-auto" style="text-align: center;">
<input id="chat-message-input" type="text" style="width:900px;" size="150">
<br>
<br>
<input id="chat-message-submit" type="button" value="发送消息">
</div>
{{ room_name|json_script:"room-name" }}
{{ username|json_script:"username" }}
<script>
window.onload = function() {
var scrollableDiv = document.getElementById('chat-record');
// 设置scrollTop使得滚动条向下翻
scrollableDiv.scrollTop = scrollableDiv.scrollHeight;
};
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const username = JSON.parse(document.getElementById('username').textContent);
const chatSocket = new WebSocket(
'wss://scybbd.com/ws/chat/' + roomName + '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
//data为收到的后端发出来的数据
console.log(data);
if (data['message']) {
if(data['username'] == username){
document.querySelector('#chat-record').innerHTML += ('<div class="chat-message right"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' +data['message'] + '</span></div></div><br>');
}else{
document.querySelector('#chat-record').innerHTML += ('<div class="chat-message left"><div class="user-content">' + data['username'] + '</div><div class="message-content"><span>' + data['message'] + '</span></div></div><br>');
}
} else {
alert('消息为空!')
}
var scrollableDiv = document.getElementById('chat-record');
// 设置scrollTop使得滚动条向下翻
scrollableDiv.scrollTop = scrollableDiv.scrollHeight;
};
chatSocket.onclose = function(e) {
console.error('聊天端口非正常关闭!');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message,
'username':username
}));
messageInputDom.value = '';
};
</script>
{% endblock %}
聊天界面比较简陋,客户端与服务器的wss://连接和通讯由页面中javacript脚本完成。
至此,一个简单的多人在线聊天的页面基本完成。