使用Django来开发HTTP连接和应用程序请求的服务器是很常见的。然而,当开发一个需要连接一直处于开放状态的双向连接的应用程序时,比如会议和聊天程序,使用HTTP连接是低效的。在这种情况下,使用WebSockets是必不可少的。
通过使用WebSockets,所有连接到该开放网络的用户都可以实时接收相关数据,这就提供了一种在客户端和服务器之间建立双向连接的方式。这是一个有状态的协议,意味着在最初的连接认证之后,客户端的凭证被保存下来,在连接被破坏之前不需要进一步认证。
在本教程中,我们将学习如何使用Django和React构建一个聊天应用程序。经过本教程,你应该更熟悉WebSockets在Django和React中的工作方式。
WebSocket的特点
WebSocket是一个双向协议,这意味着数据可以在客户端和服务器之间不间断地即时交换。出于同样的原因,WebSockets也被认为是全双工通信。
WebSockets不需要任何特定的浏览器来工作;所有的浏览器都兼容。WebSocket是一个有状态的协议。由于客户端凭证在主要连接验证后被保存,因此在连接丢失之前不需要再次进行额外的验证。
如何在Django中使用WebSockets
当你想用WebSockets做任何事情时,Django Channels是必不可少的,所以继续用下面的命令安装它:
pip install channels
在这一节中,我们将设置Django以使用WebSockets,并将其与建立一个普通的Django应用程序进行比较。
多亏了Django通道,在Django中使用WebSockets是很简单的。你可以使用Django Channels建立一个ASGI(Asynchronous Server Gateway Interface)服务器,之后你可以建立一个群组,成员之间可以即时发短信。沟通的对象不是某个特定的用户,而是一个组,任何数量的用户都可以加入。
创建一个文件夹,其中将包含你项目的所有代码。在终端上导航到你刚刚创建的文件夹,运行startproject 命令来创建一个新的Django项目:
$ django-admin startproject chat .
现在,运行$ python3 manage.py startapp app ,创建一个新的应用程序。
你需要让你的Django项目意识到一个新的应用程序已经被添加,并且你安装了Channels插件。你可以通过更新chat/settings.py 文件并将'app' 加入到INSTALLED_APPS 列表中来实现。它看起来会像下面的代码:
# project/settings.py
INSTALLED_APPS = [
...
'channels',
'app',
]
在settings.py 文件中,你应该设置配置,以允许 Django 和 Django 频道通过一个消息代理来相互连接。要做到这一点,我们可以利用像Redis这样的工具,但在这个例子中,我们将坚持使用本地后端。在你的settings.py 文件中添加以下一行代码:
ASGI_APPLICATION = "chat.routing.application" #routing.py will handle the ASGI
CHANNEL_LAYERS = {
'default': {
'BACKEND': "channels.layers.InMemoryChannelLayer"
}
}
在上面的代码中,ASGI_APPLICATION ,需要运行ASGI服务器,并告诉Django在事件发生时该怎么做。我们将把这个配置放在一个名为routing.py 的文件中。路由Django通道与Django URL配置类似;它选择了当WebSocket请求被发送到服务器时要运行的代码。
在创建路由之前,我们首先要开发消费者。在Django通道中,消费者使你能够在你的代码中创建一组函数,每当事件发生时就会被调用。它们类似于Django中的views 。
要开发消费者,打开app/ 文件夹,创建一个名为consumers.py 的新文件,并粘贴以下代码:
# app/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
class TextRoomConsumer(WebsocketConsumer):
def connect(self):
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()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
def receive(self, text_data):
# Receive message from WebSocket
text_data_json = json.loads(text_data)
text = text_data_json['text']
sender = text_data_json['sender']
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': text,
'sender': sender
}
)
def chat_message(self, event):
# Receive message from room group
text = event['message']
sender = event['sender']
# Send message to WebSocket
self.send(text_data=json.dumps({
'text': text,
'sender': sender
}))
现在,我们可以创建路由来处理你刚刚创建的消费者。创建一个名为routing.py 的新文件,并粘贴下面的代码,它将协调消费者的工作:
from channels.routing import ProtocolTypeRouter, URLRouter
# import app.routing
from django.urls import re_path
from app.consumers import TextRoomConsumer
websocket_urlpatterns = [
re_path(r'^ws/(?P<room_name>[^/]+)/$', TextRoomConsumer.as_asgi()),
]
# the websocket will open at 127.0.0.1:8000/ws/<room_name>
application = ProtocolTypeRouter({
'websocket':
URLRouter(
websocket_urlpatterns
)
,
})
构建前台
现在,让我们建立一个聊天应用程序的前端,它使用WebSockets连接到Django的后端。我们将用React来构建这一部分,并且用MUI来添加样式。
在你的终端,导航到你的项目根部,运行以下命令以获得React的Create React App模板代码:
npx create-react-app frontend
接下来,cd 到frontend/ 目录,并运行以下命令来安装 MUI 和WebSocket依赖项,这使我们能够将 React 应用程序连接到 WebSocket 服务器:
npm install --save --legacy-peer-deps @material-ui/core
npm install websocket
删除frontend/src/App.js 中的所有代码。我们将用本教程其余部分的代码取代它,从初始状态开始:
import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";
class App extends Component {
state = {
filledForm: false,
messages: [],
value: '',
name: '',
room: 'test',
}
client = new W3CWebSocket('ws://127.0.0.1:8000/ws/' + this.state.room + '/'); //gets room_name from the state and connects to the backend server
render(){
}
}
现在,我们需要处理组件在浏览器上安装时的情况。我们希望应用程序能够连接到后台服务器,并在组件挂载时获得消息,因此我们将使用componentDidMount() 。你可以通过在render() 函数前粘贴以下代码来实现这一点:
...
componentDidMount() {
this.client.onopen = () => {
console.log("WebSocket Client Connected");
};
this.client.onmessage = (message) => {
const dataFromServer = JSON.parse(message.data);
if (dataFromServer) {
this.setState((state) => ({
messages: [
...state.messages,
{
msg: dataFromServer.text,
name: dataFromServer.sender,
},
],
}));
}
};
}
render() {
...
接下来,我们将创建用于更新状态的表单。我们将创建一个表单,用于更新发送者的name 和房间名称。然后,我们将创建另一个表单来处理表单的提交。将下面的代码粘贴在render() 函数中:
render() {
const { classes } = this.props;
return (
<Container component="main" maxWidth="xs">
{this.state.filledForm ? (
<div style={{ marginTop: 50 }}>
Room Name: {this.state.room}
<Paper
style={{height: 500, maxHeight: 500, overflow: "auto", boxShadow: "none", }}
>
{this.state.messages.map((message) => (
<>
<Card className={classes.root}>
<CardHeader title={message.name} subheader={message.msg} />
</Card>
</>
))}
</Paper>
<form
className={classes.form}
noValidate
onSubmit={this.onButtonClicked}
>
<TextField id="outlined-helperText" label="Write text" defaultValue="Default Value"
variant="outlined"
value={this.state.value}
fullWidth
onChange={(e) => {
this.setState({ value: e.target.value });
this.value = this.state.value;
}}
/>
<Button type="submit" fullWidth variant="contained" color="primary"
className={classes.submit}
>
Send Message
</Button>
</form>
</div>
) : (
<div>
<CssBaseline />
<div className={classes.paper}>
<form
className={classes.form}
noValidate
onSubmit={(value) => this.setState({ filledForm: true })}
>
<TextField variant="outlined" margin="normal" required fullWidth label="Room name"
name="Room name"
autoFocus
value={this.state.room}
onChange={(e) => {
this.setState({ room: e.target.value });
this.value = this.state.room;
}}
/>
<TextField variant="outlined" margin="normal" required fullWidth name="sender" label="sender"
type="sender"
id="sender"
value={this.state.name}
onChange={(e) => {
this.setState({ name: e.target.value });
this.value = this.state.name;
}}
/>
<Button type="submit" fullWidth variant="contained" color="primary"
className={classes.submit}
>
Submit
</Button>
</form>
</div>
</div>
)}
</Container>
);
}
export default withStyles(useStyles)(App);
当你填写房间名称和发件人的名字时,filledForm ,在状态中会被改为true ,然后输入信息的表单会被呈现。在我们的代码中,我们使用了一些MUI类,我们需要导入这些类。你可以通过在你的App.js 文件的顶部粘贴下面的代码来做到这一点:
import Button from "@material-ui/core/Button";
import CssBaseline from "@material-ui/core/CssBaseline";
import TextField from "@material-ui/core/TextField";
import Container from "@material-ui/core/Container";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import Paper from "@material-ui/core/Paper";
import { withStyles } from "@material-ui/core/styles";
const useStyles = (theme) => ({
submit: {
margin: theme.spacing(3, 0, 2),
},
});
一旦消息表单被提交,我们将在点击提交按钮时将文本发送到后端服务器。将下面的代码直接粘贴在componentDidMount() 函数的上方:
onButtonClicked = (e) => {
this.client.send(
JSON.stringify({
type: "message",
text: this.state.value,
sender: this.state.name,
})
);
this.state.value = "";
e.preventDefault();
};
componentDidMount() {
...
测试应用程序
现在我们已经完成了应用程序的编码,我们可以对它进行测试。首先,通过运行以下命令启动后台服务器。确保你是在manage.py 文件所在的目录中:
python manage.py runserver
在另一个终端窗口,导航到frontend/ 目录,通过运行下面的命令运行前端服务器。React应用程序将自动打开:
npm start
填写一个名字和一个房间名称。然后,在另一个浏览器中以不同的名字但相同的房间名称打开该应用程序。现在,你可以开始和自己聊天了,你会发现信息是实时收到的。

结论
在这篇文章中,我们已经了解了WebSockets,它的应用,以及如何通过利用Django通道在Django中使用它。最后,我们介绍了如何使用React建立与Django服务器的WebSocket连接。
尽管我们建立了一个高效的实时聊天应用程序,但仍有一些改进是你可以做到的。例如,为了存储消息,你可以加入一个数据库连接。作为本地后端的替代品,你可以考虑使用Redis作为消息代理。
我希望你喜欢这篇文章,如果你有任何问题,请务必留下评论。编码愉快!