使用ReactJS、Django Channels 4 和 Redis 实现实时聊天

本文将使用 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 修饰,表明它是一个同步方法,但为了与数据库交互,已转换为异步方法。该方法使用提供的roommessageusername创建消息对象。它将消息保存到数据库并返回消息对象。

从客户端接收到文本信息时,会调用 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。您应该会看到一个页面,提示您输入用户名和房间。

使用ReactJS、Django Channels 4 和 Redis 实现实时聊天

在另一个浏览器中,使用第二个 Django 用户登录。输入每个用户的用户名和房间。确保两个用户加入同一聊天室的房间名称相同。两个用户登录并进入聊天室后,开始从其中一个用户发送消息。您应该会看到消息实时显示在其他用户的屏幕上。

使用ReactJS、Django Channels 4 和 Redis 实现实时聊天

结论

总之,我们已经学会了如何使用 Django Channels、Redis、Daphne 和 ReactJS 构建一个强大的实时聊天应用程序。本教程包括使用 Django 设置后台、为 WebSocket 通信配置 Django Channels 以及集成 Redis 以实现实时功能。在前端,我们使用 ReactJS 创建了用于消息渲染和 WebSocket 通信的组件。

作者:Oet Nnyw

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/44523.html

(0)

相关推荐

发表回复

登录后才能评论