本文将使用 Django Channels 4、Redis 和 ReactJS 构建一个强大的实时聊天应用程序。
Django 应用程序通常使用 WSGI 服务器(如 Gunicorn 或 uWSGI)进行部署。WSGI(Web 服务器网关接口)服务器使用 WSGI 标准与 Django 应用程序通信。Django 设计用于处理同步、请求-响应式交互。但是,对于聊天或通知等实时应用程序,需要长期连接和异步通信时,Django 通道就会发挥作用。
Django Channels
Django Channels 是 Django 的一个扩展,它为处理 WebSockets 和其他异步协议添加了支持。与遵循请求-响应模型的传统 HTTP 不同,WebSockets 通过单个长期连接提供全双工通信通道。建立 WebSocket 连接时,Django Channels 会使用 ASGI(异步服务器网关接口)协议来管理连接。ASGI 是 WSGI 的异步版本,旨在处理异步应用程序。
Daphne
Daphne 是一个 ASGI 服务器,通常与 Django 通道一起使用。Daphne 设计用于为使用通道的 Django 应用程序提供服务,并能处理 HTTP 和 WebSocket 连接。在 INSTALLED_APPS 中添加 “daphne “可指示 Django 在开发过程中自动加载和配置 Daphne。这样,我们就不必手动单独设置和管理服务器,从而简化了工作流程。我们可以利用 runserver 命令同时启动 Django 应用程序和 Daphne 服务器。
后端实现
在我们开始之前,请确保您已经安装了 Python。打开终端,运行以下命令进行验证:
python -v
设置 virtualenv
Virtualenv 是一种创建隔离 Python 环境的工具。这对开发非常有用。要安装 virtualenv,请在终端运行以下命令:
mkdir django_channels_chatapp
cd django_channels_chatapp
pip3 install virtualenv
virtualenv env
source env/bin/activate
pip3 install --upgrade pip
设置 Django 项目和应用程序
首先创建一个新的 Django 项目和一个 Django 应用程序来封装 API:
pip3 install django djangorestframework daphne channels psycopg2-binary
# This is the version used in this tutorial
channels==4.0.0
channels-redis==4.1.0
daphne==4.0.0
Django==4.2.7
django-admin startproject backend
cd backend
python3 manage.py startapp api
现在 api
应用程序已经创建,我们需要在 settings.py
中的 INSTALLED_APPS
中对其进行配置。这将使应用程序的模型、视图、表单或其他组件等应用程序功能在 Django 项目中可用。同时,我们还需要加入 Channels。
# settings.py
INSTALLED_APPS = [
# ...
"daphne" ,
"django.contrib.staticfiles" ,
"channels" ,
"rest_framework" ,
"api" ,
]
ASGI 配置
在项目根目录下新建一个名为 asgi.py 的文件:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
# Define your WebSocket routing here
)
),
})
下一步更新 WebSocket 路由。现在,我们需要让 Django 知道 ASGI 应用程序的位置。在 settings.py
中添加以下一行。
ASGI_APPLICATION = 'backend.asgi.application'
设置 Redis 服务器
Redis 是一种内存数据存储,可提供高性能的数据检索和操作。由于它能以低延迟处理大量数据,因此是实时应用程序的热门选择。在聊天应用程序中,Redis 可用于存储实时消息数据和用户存在信息,从而实现高效的消息传递和用户间交互。在 Django Channels 实时聊天应用程序中,Redis 通常用作通道层的后端。
运行以下命令启动 Redis 服务器:
# To install redis server
brew install redis
# To start the redis server
redis-server
设置Channel Layer(通道层)
通道层是 Django Channels 的重要组成部分,负责管理 WebSocket 连接和处理实时事件。
channels-redis
是 Django Channels 使用 Redis 作为后备存储的通道层。我们可以使用 pip 安装它:
pip3 install channels-redis
在Django项目设置中,需要配置channels
使用Redis通道层。更新CHANNEL_LAYERS
设置:
REDIS_HOST = os.environ.get("REDIS_HOST", "redis")
REDIS_PORT = os.environ.get("REDIS_PORT", 6379)
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [(REDIS_HOST, REDIS_PORT)],
},
},
}
创建模型
在 Django 中,模型是应用程序处理和存储数据的蓝图。每个模型对应一个数据库表,并概述该表中每个条目应具备的特征或字段。为了表示聊天数据,我们在 models.py
中定义了两个模型: 房间和消息。
class Room(models.Model):
name = models.CharField(max_length=100)
userslist = models.ManyToManyField(to=User, blank=True)
class Message(models.Model):
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
room = models.ForeignKey(Room, related_name="messages", on_delete=models.CASCADE)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "chat_message"
ordering = ("timestamp",)
运行迁移命令来更新数据库:
python manage.py makemigrations
python manage.py migrate
创建 Serializers
Serializers 是 Django 模型实例与标准 Python 数据结构(如字典或列表)之间的桥梁。这有助于应用程序与外部实体(如应用程序接口或网络客户端)之间的无缝数据交换。Serializers 还可以作为数据验证器,确保传入信息的完整性。我们在 serializers.py
中为每个模型定义了相应的Serializers。
class RoomSerializer(serializers.ModelSerializer):
class Room:
model = Room
fields = ("id", "name", "userslist")
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ("id", "room", "user", "content", "timestamp")
read_only_fields = ("id", "timestamp")
创建聊天消费者
消费者会建立 WebSocket 连接、管理消息创建并向特定房间中所有已连接的客户端广播消息。我们在聊天应用中创建了 consumers.py 文件,用于处理实时聊天交互。
class ChatConsumer(AsyncWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.room_name = None
self.room_group_name = None
self.room = None
self.user = None
async def connect(self):
print("Connecting...")
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.user = self.scope["user"] or "Anonymous"
if not self.room_name or len(self.room_name) > 100:
await self.close(code=400)
return
self.room_group_name = f"chat_{self.room_name}"
self.room = await self.get_or_create_room()
# Join room group
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
await self.accept()
await self.create_online_user(self.user)
await self.send_user_list()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
await self.remove_online_user(self.user)
await self.send_user_list()
async def receive(self, text_data=None, bytes_data=None):
data = json.loads(text_data)
message = data["message"]
if not message or len(message) > 255:
return
message_obj = await self.create_message(message)
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "chat_message",
"message": message_obj.content,
"username": message_obj.user.username,
"timestamp": str(message_obj.timestamp),
},
)
async def send_user_list(self):
user_list = await self.get_connected_users()
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "user_list",
"user_list": user_list,
},
)
async def chat_message(self, event):
message = event["message"]
username = event["username"]
timestamp = event["timestamp"]
await self.send(
text_data=json.dumps(
{"message": message, "username": username, "timestamp": timestamp}
)
)
async def user_list(self, event):
user_list = event["user_list"]
await self.send(text_data=json.dumps({"user_list": user_list}))
@database_sync_to_async
def create_message(self, message):
try:
return Message.objects.create(
room=self.room, content=message, user=self.user
)
except Exception as e:
print(f"Error creating message: {e}")
return None
@database_sync_to_async
def get_or_create_room(self):
room, _ = Room.objects.get_or_create(name=self.room_group_name)
return room
@database_sync_to_async
def create_online_user(self, user):
try:
self.room.online.add(user)
self.room.save()
except Exception as e:
print("Error joining user to room:", str(e))
return None
@database_sync_to_async
def remove_online_user(self, user):
try:
self.room.online.remove(user)
self.room.save()
except Exception as e:
print("Error removing user to room:", str(e))
return None
@database_sync_to_async
def get_connected_users(self):
# Get the list of connected users in the room
return [user.username for user in self.room.online.all()]
ChatConsumer
类继承自 AsyncWebsocketConsumer
,这表明该类旨在异步处理 WebSocket 连接。
当建立 WebSocket 连接时,将执行 connect
方法。它会从 URL 路由 kwargs 中提取房间名称,并将其赋值给 self.room_name。然后,它使用房间名组成房间组名,并将通道加入该组。最后,它接受连接。
WebSocket 连接关闭时,会调用 disconnect
方法。它会将通道从房间组中删除,以确保它不再接收信息。
create_message
方法用 @database_sync_to_async
修饰,表明它是一个同步方法,但为了与数据库交互,已转换为异步方法。该方法使用提供的room
、message
和username
创建消息对象。它将消息保存到数据库并返回消息对象。
从客户端接收到文本信息时,会调用 receive
方法。它会解析 JSON 数据,提取信息、用户名和房间对象,并使用 create_message
方法创建一个新的信息对象。然后,它会使用 channel_layer.group_send
将消息广播给房间组的所有成员。
当收到来自房间组的信息时,就会调用 chat_message
方法。它会从事件数据中提取信息、用户名和时间戳,并使用 self.send
将信息发送回 WebSocket。
接下来,我们需要更新 asgi.py
,加入 WebSocket 路由:
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
[
re_path(r"ws/chat/(?P<room_name>\w+)/$", ChatConsumer.as_asgi()),
]
)
),
})
创建视图
视图负责处理 HTTP 请求并生成响应。它们接收来自客户端的请求,使用模型和序列化器处理数据,并发回适当的响应。我们在 views.py
中创建一个 MessageList
来检索所有消息。
class MessageList(generics.ListCreateAPIView):
queryset = Message.objects.all()
serializer_class = MessageSerializer
ordering = ('-timestamp',)
def get_queryset(self):
room_name = self.kwargs.get('room_name')
if room_name:
queryset = Message.objects.filter(room__name=room_name)
else:
queryset = Message.objects.all()
return queryset
要为 MessageList 视图创建相应的 URL,我们必须定义与所需 URL 结构相匹配的路径模式。下面是一个如何定义按房间检索消息的 URL 的示例:
urlpatterns = [
path('chat/<slug:room_name>/messages/', views.MessageList.as_view(), name='chat-messages'),
]
前端实现
现在我们有了一个使用 Django Channels 和 Redis 的强大后端,是时候让它与动态前端结合起来了!以下是连接聊天应用程序并让消息流动起来的分步指南:
ReactJS 设置
在项目目录中创建一个新的 React 应用程序:
npx create-react-app frontend
导航到frontend
目录并安装必要的依赖项:
cd frontend
npm install react-dom react-websocket axios
聊天组件
在 src 目录中创建一个新的 Chat.js
组件,用于处理用户界面、消息渲染和 WebSocket 连接:
function Chat() {
const [socket, setSocket] = useState(null);
const [username, setUsername] = useState("");
const [room, setRoom] = useState("");
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([]);
const [activeUsers, setActiveUsers] = useState([]);
useEffect(() => {
const storedUsername = localStorage.getItem("username");
if (storedUsername) {
setUsername(storedUsername);
} else {
const input = prompt("Enter your username:");
if (input) {
setUsername(input);
localStorage.setItem("username", input);
}
}
const storedRoom = localStorage.getItem("room");
if (storedRoom) {
setRoom(storedRoom);
} else {
const input = prompt("Enter your room:");
if (input) {
setRoom(input);
localStorage.setItem("room", input);
}
}
if (username && room) {
const newSocket = new WebSocket(`ws://localhost:8000/ws/chat/${room}/`);
setSocket(newSocket);
newSocket.onopen = () => console.log("WebSocket connected");
newSocket.onclose = () => {
console.log("WebSocket disconnected");
localStorage.removeItem("username");
localStorage.removeItem("room");
};
return () => {
newSocket.close();
};
}
}, [username, room]);
useEffect(() => {
if (socket) {
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.user_list) {
setActiveUsers(data.user_list);
} else {
setMessages((prevMessages) => [...prevMessages, data]);
}
};
}
}, [socket]);
const handleSubmit = (event) => {
event.preventDefault();
if (message && socket) {
const data = {
message: message,
username: username,
};
socket.send(JSON.stringify(data));
setMessage("");
}
};
return (
<div className="chat-app">
<div className="chat-wrapper">
<div className="active-users-container">
<h2>Active Users ({activeUsers.length})</h2>
<ul>
{activeUsers.map((user, index) => (
<li key={index}>{user}</li>
))}
</ul>
</div>
<div className="chat-container">
<div className="chat-header">Chat Room: {room}</div>
<div className="message-container">
{messages.map((message, index) => (
<div key={index} className="message">
<div className="message-username">{message.username}:</div>
<div className="message-content">{message.message}</div>
<div className="message-timestamp">{message.timestamp}</div>
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Type a message..."
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
<button type="submit">Send</button>
</form>
</div>
</div>
</div>
);
}
export default Chat;
更新应用程序组件
使用下面的代码片段更新 App.js,以渲染 Chat
组件:
function App ( ) {
return (
<div className="App">
<Chat />
</div>
);
}
测试
为了测试实时聊天应用程序,使用 Django 管理命令创建两个 Django 用户。
python manage.py createsuperuser
按照提示创建两个用户账户。我们将使用这些账户模拟两个不同的用户在聊天中进行交互。
运行开发服务器
确保 Django 开发服务器和 React 开发服务器都在运行。如果未运行,请使用以下命令启动它们:
# Start Django development server
python manage.py runserver
# Start React development server
cd frontend
npm start
Django 开发服务器通常运行在 localhost:8000,React 开发服务器运行在 localhost:3000。
现在,打开两个独立的Web浏览器。例如,我们可以使用 Chrome 浏览器和火狐浏览器,或者 Chrome 浏览器和隐身窗口。在一个浏览器中,使用你创建的 Django 用户之一登录。导航到 localhost:3000。您应该会看到一个页面,提示您输入用户名和房间。
在另一个浏览器中,使用第二个 Django 用户登录。输入每个用户的用户名和房间。确保两个用户加入同一聊天室的房间名称相同。两个用户登录并进入聊天室后,开始从其中一个用户发送消息。您应该会看到消息实时显示在其他用户的屏幕上。
结论
总之,我们已经学会了如何使用 Django Channels、Redis、Daphne 和 ReactJS 构建一个强大的实时聊天应用程序。本教程包括使用 Django 设置后台、为 WebSocket 通信配置 Django Channels 以及集成 Redis 以实现实时功能。在前端,我们使用 ReactJS 创建了用于消息渲染和 WebSocket 通信的组件。
作者:Oet Nnyw
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/44523.html