前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用Django构建即时通讯应用的最简单方法

使用Django构建即时通讯应用的最简单方法

作者头像
一只大鸽子
发布于 2024-01-09 07:16:04
发布于 2024-01-09 07:16:04
43700
代码可运行
举报
运行总次数:0
代码可运行

使用Django构建即时通讯应用的最简单方法

原文:《The simplest way to build an instant messaging app with Django》 https://www.photondesigner.com/articles/instant-messenger

利用 Server-Sent Events(见参考1)实现通信。效果如下:

运行结果

0. 设置Django和Daphne

创建项目和应用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
pip install django daphne
django-admin startproject core .
python manage.py startapp sim

修改core/settings.py

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# core/settings.py

INSTALLED_APPS = [
    'daphne',  # Add this at the top.
    # ...
    'sim',
    # ...
]

ASGI_APPLICATION = 'core.asgi.application'

1. 在Django view中以流式传输数据

sim/views.py

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from datetime import datetime

from typing import AsyncGenerator
from django.shortcuts import render, redirect
from django.http import HttpRequest, StreamingHttpResponse, HttpResponse
from . import models
import json
import random

# 大厅
def lobby(request: HttpRequest) -> HttpResponse:
    if request.method == 'POST':
        username = request.POST.get('username')
        if username:
            request.session['username'] = username
        else:
            names = [
                "Horatio", "Benvolio", "Mercutio", "Lysander", "Demetrius", "Sebastian", "Orsino",
                "Malvolio", "Hero", "Bianca", "Gratiano", "Feste", "Antonio", "Lucius", "Puck", "Lucio",
                "Goneril", "Edgar", "Edmund", "Oswald"
            ]
            request.session['username'] = f"{random.choice(names)}-{hash(datetime.now().timestamp())}"

        return redirect('chat')
    else:
        return render(request, 'lobby.html')


# 聊天页面
def chat(request: HttpRequest) -> HttpResponse:
    if not request.session.get('username'):
        return redirect('lobby')
    return render(request, 'chat.html')


# 创建消息
def create_message(request: HttpRequest) -> HttpResponse:
    content = request.POST.get("content")
    username = request.session.get("username")

    if not username:
        return HttpResponse(status=403)
    author, _ = models.Author.objects.get_or_create(name=username)

    if content:
        models.Message.objects.create(author=author, content=content)
        return HttpResponse(status=201)
    else:
        return HttpResponse(status=200)


async def stream_chat_messages(request: HttpRequest) -> StreamingHttpResponse:
    """
    当我们创建消息时,将聊天消息流式传输到客户端。
    """
    async def event_stream():
        """
        发送连续的数据流至已连接的客户端。
        """
        async for message in get_existing_messages():
            yield message

        last_id = await get_last_message_id()

        # 持续检查新消息
        while True:
            new_messages = models.Message.objects.filter(id__gt=last_id).order_by('created_at').values(
                'id', 'author__name', 'content'
            )
            async for message in new_messages:
                yield f"data: {json.dumps(message)}\n\n"
                last_id = message['id']

    async def get_existing_messages() -> AsyncGenerator:
        messages = models.Message.objects.all().order_by('created_at').values(
            'id', 'author__name', 'content'
        )
        async for message in messages:
            yield f"data: {json.dumps(message)}\n\n"

    async def get_last_message_id() -> int:
        last_message = await models.Message.objects.all().alast()
        return last_message.id if last_message else 0

    return StreamingHttpResponse(event_stream(), content_type='text/event-stream')

2. 为视图添加 URL

创建sim/urls.py并写入:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from django.urls import path
from . import views

urlpatterns = [
    path('lobby/', views.lobby, name='lobby'),
    path('', views.chat, name='chat'),
    path('create-message/', views.create_message, name='create-message'),
    path('stream-chat-messages/', views.stream_chat_messages, name='stream-chat-messages'),
]

更新core/urls.py包含sim.urls

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# core/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]

3. 添加模板

模板中包括一个 EventSource 脚本,用于接收来自 Django 的服务器发送的事件。 在sim下创建templates文件夹,在templates下创建chat.html。写入:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat</title>
</head>
<body>
<div class="header">
    <h1>Welcome {{ request.session.username }}</h1>
</div>

<div class="container">
    <div class="messages">
        <div id="sse-data"></div>
    </div>

    <form x-cloak
          @submit.prevent="submit" x-data="{state: 'composing', errors: {}}">
        <div>
            <textarea name="content" @input="state = 'composing'" autofocus placeholder="Your next message..."></textarea>
            <button class="button">
                Send
            </button>
        </div>

        <div x-show="state === 'error'">
            <p>
                Error sending your message ❌
            </p>
        </div>
    </form>

    <form action="/lobby/" method="get">
        <button type="submit">Return to Lobby</button>
    </form>
</div>

<script>
    let eventSource;
    const sseData = document.getElementById('sse-data');

    function startSSE() {
        eventSource = new EventSource('/stream-chat-messages/');
        eventSource.onmessage = event => {
            const data = JSON.parse(event.data);
            const messageHTML = `
                    <div class="message-box">
                        <div class="message-author">${data.author__name}</div>
                        <div class="message-content">${data.content}</div>
                    </div>`;
            sseData.innerHTML += messageHTML;
            const msg = document.getElementsByClassName('messages')[0];
            msg.scrollTo(0, msg.scrollHeight);
        };
    }

    // On load, start SSE if the browser supports it.
    if (typeof(EventSource) !== 'undefined') {
        startSSE();
    } else {
        sseData.innerHTML = 'Whoops! Your browser doesn\'t receive server-sent events.';
    }
</script>

<script>
    function submit(event) {
        event.preventDefault();
        const formData = new FormData(event.target);

        const endpointUrl = "/create-message/"
        fetch(endpointUrl, {
            method: "post",
            body: formData,
            headers: {
                'X-CSRFToken': '{{ csrf_token }}',
            },
        })
            .then(response => {
                this.state = response.ok ? 'success' : 'error';
                return response.json();
            })
            .then(data => {
                this.errors = data.errors || {};
            });
    }
</script>
<script defer="" src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
</body>
</html>

templates目录下创建lobby.html

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sign-in Page</title>
    <style>
        body {
            font-family: 'Helvetica Neue', sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #e8eff1;
            margin: 0;
            color: #333;
        }

        .sign-in-container {
            background: #ffffff;
            padding: 40px 50px;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            width: 300px;
        }

        .sign-in-container h2 {
            text-align: center;
            margin-bottom: 30px;
            font-size: 24px;
            color: #0a3d62;
        }

        .sign-in-container form {
            display: flex;
            flex-direction: column;
        }

        .sign-in-container input {
            margin-bottom: 15px;
            padding: 15px;
            border: 1px solid #ced6e0;
            border-radius: 6px;
            font-size: 16px;
        }

        .sign-in-container button {
            padding: 15px;
            background-color: #2ecc71;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }

        .sign-in-container button:hover {
            background-color: #27ae60;
        }
    </style>
</head>
<body>
    <div class="sign-in-container">
        <h2>Enter your chat name:</h2>
        <form method="post">
            {% csrf_token %}
            <input type="text" name="username" placeholder="Username" required>
            <button type="submit">Join the chat</button>
        </form>
    </div>
</body>
</html>

4. 创建 Django 模型来存储要实时发送的数据

将以下内容添加到 sim/models.py :

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=500)


class Message(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

应用模型:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
python manage.py makemigrations
python manage.py migrate

5. 运行

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
python manage.py runserver

访问http://127.0.0.1:8000,用另一个浏览器作为第二个用户访问。

6. 奖励:为您的聊天界面添加样式

chat.html模板添加样式以包含聊天界面和样式。以下是完整模板

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            background-color: #e8eff1;
            margin: 0;
            padding: 0;
            color: #333;
        }
        .header {
            color: #022c22;
            font-size: 14px;
            text-align: center;
        }
        .container {
            max-width: 60%;
            margin: auto;
        }
        .messages {
            background: #ffffff;
            border-radius: 8px;
            padding: 20px;
            margin-bottom: 30px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            font-size: 16px;
            height: 50vh;
            overflow-y: scroll;
        }
        .message {
            border-bottom: 1px solid #ced6e0;
            padding: 15px 0;
        }
        .message:last-child {
            border-bottom: none;
        }
        form {
            display: flex;
            flex-direction: column;
        }
        textarea, input, button {
            margin-bottom: 15px;
            padding: 15px;
            border: 1px solid #ced6e0;
            border-radius: 6px;
            font-size: 16px;
        }
        .button {
            background-color: #2ecc71;
            color: white;
            border: none;
            cursor: pointer;
            transition: background-color 0.3s;
        }
        .button:hover {
            background-color: #27ae60;
        }

        .message-box {
            background: rgba(247, 248, 245, 0.42);
            border-left: 4px solid rgba(51, 177, 104, 0.42);
            margin-bottom: 15px;
            padding: 10px 15px;
            border-radius: 6px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .message-author {
            font-weight: bold;
            margin-bottom: 5px;
        }

        .message-content {
            font-size: 16px;
            line-height: 1.4;
        }

        textarea {
            background: #f8f9fa;
            border: 1px solid #ced4da;
            box-sizing: border-box;
            width: 100%;
            padding: 12px 20px;
            border-radius: 6px;
            min-height: 100px;
            font-size: 16px;
            line-height: 1.5;
            resize: none;
            outline: none;
        }
    </style>
    <style>
        [x-cloak] {
            display: none !important;
        }
    </style>
</head>
<body>
<div class="header">
    <h1>Welcome {{ request.session.username }}</h1>
</div>

<div class="container">
    <div class="messages">
        <div id="sse-data"></div>
    </div>

    <form x-cloak
          @submit.prevent="submit" x-data="{state: 'composing', errors: {}}">
        <div>
            <textarea name="content" @input="state = 'composing'" autofocus placeholder="Your next message..."></textarea>
            <button class="button">
                Send
            </button>
        </div>

        <div x-show="state === 'error'">
            <p>
                Error sending your message ❌
            </p>
        </div>
    </form>

    <form action="/lobby/" method="get">
        <button type="submit">Return to Lobby</button>
    </form>
</div>

<script>
    let eventSource;
    const sseData = document.getElementById('sse-data');

    function startSSE() {
        eventSource = new EventSource('/stream-chat-messages/');
        eventSource.onmessage = event => {
            const data = JSON.parse(event.data);
            const messageHTML = `
                    <div class="message-box">
                        <div class="message-author">${data.author__name}</div>
                        <div class="message-content">${data.content}</div>
                    </div>`;
            sseData.innerHTML += messageHTML;
        };
    }

    // On load, start SSE if the browser supports it.
    if (typeof(EventSource) !== 'undefined') {
        startSSE();
    } else {
        sseData.innerHTML = 'Whoops! Your browser doesn\'t receive server-sent events.';
    }
</script>

<script>
    function submit(event) {
        event.preventDefault();
        const formData = new FormData(event.target);

        const endpointUrl = "/create-message/"
        fetch(endpointUrl, {
            method: "post",
            body: formData,
            headers: {
                'X-CSRFToken': '{{ csrf_token }}',
            },
        })
            .then(response => {
                this.state = response.ok ? 'success' : 'error';
                return response.json();
            })
            .then(data => {
                this.errors = data.errors || {};
            });
    }
</script>
<script defer="" src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
</body>
</html>

参考

  1. 1. Server-Sent Events 教程:https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html 严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
  2. 2. Django文档StreamingHttpResponse:https://docs.djangoproject.com/zh-hans/4.2/ref/request-response/#streaminghttpresponse-objects StreamingHttpResponse 用于从 Django 向浏览器流式传输响应。在 WSGI 下的一个示例用法是在生成响应时需要太长时间或使用太多内存的情况下进行流式传输内容。例如,在 生成大型 CSV 文件 时非常有用。
  3. 3. https://deepinout.com/django/django-questions/222_django_django_31_streaminghttpresponse_with_an_async_generator.html StreamingHttpResponse的使用方法与常规的HttpResponse类似,只需将生成数据的逻辑写入一个生成器函数,并将该函数作为StreamingHttpResponse的参数传入即可。在每次迭代时,StreamingHttpResponse都会将生成器函数的返回值作为响应内容的一部分发送给客户端,直到生成器结束。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from django.http import StreamingHttpResponse
import time

def async_generator():
    for i in range(5):
        yield str(i)
        time.sleep(1)

def streaming_view(request):
    response = StreamingHttpResponse(async_generator())

    return response

Django 3.1的新特性是将StreamingHttpResponse与异步生成器的结合使用。这样,我们可以在异步的环境中生成响应数据,以实现更高效的处理。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from django.http import StreamingHttpResponse
import asyncio

async def async_generator():
    for i in range(5):
        await asyncio.sleep(1)
        yield str(i)

def streaming_view(request):
    response = StreamingHttpResponse(async_generator())

    return response
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-01-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一只大鸽子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
springboot整合ActiveMQ1(基本使用)
说明:acitveMQ 版本为:5.9.1,springboot 版本为 2.0.3
用户2038589
2019/06/04
6440
springboot整合ActiveMQ1(基本使用)
SpringBoot开发案例之整合ActiveMQ实现秒杀队列
在实际生产环境中中,通常生产者和消费者会是两个独立的应用,这样才能通过消息队列实现了服务解耦和广播。因为此项目仅是一个案例,为了方便期间,生产和消费定义在了同一个项目中。
小柒2012
2018/10/09
4.6K1
SpringBoot开发案例之整合ActiveMQ实现秒杀队列
SpringBoot整合ActiveMQ实现Queue和Topic两种模式
最近小编在学习消息队列,然后选中了ActiveMQ,来进行学习.于是探索了好久,来整理一下自己的学习心得!大家一起学习,希望对你有用.我把一些我自己的理解写在注释里了注意看!!
终码一生
2022/04/15
7090
SpringBoot整合ActiveMQ实现Queue和Topic两种模式
springboot集成ActiveMQ
1、pom依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-po
麦克劳林
2018/09/11
9391
springboot集成ActiveMQ
Spring Boot整合ActiveMQ
在本地或者虚拟机中安装ActiveMQ并启动ActiveMQ服务,启动完毕之后通过浏览器来访问ActiveMQ的管理页面http://localhost:8161(这是本地安装的ActiveMQ,如果是在虚拟机中,localhost需要替换成虚拟机的IP)来确认一下服务是否正常启动,页面正常显示说明启动成功。 为了让你的Spring Boot应用支持ActiveMQ,需要在pom.xml文件中添加如下依赖:
itlemon
2020/04/03
1.5K0
Spring Boot整合ActiveMQ
SpringBoot整合ActiveMq实现Queue和Topic两种模式
一、前言 最近小编在学习消息队列,然后选中了ActiveMq,来进行学习.于是探索了好久,来整理一下自己的学习心得!大家一起学习,希望对你有用.我把一些我自己的理解写在注释里了注意看!! 二、ActiveMq的下载和使用 下载 大家直接下载解压就可以使用了---> 链接:https://pan.baidu.com/s/1W0MZtQAya0mOEKMWqJK1iA 提取码:29mz 使用 三、依赖准备 <!-- activemq --> <dependency> <grou
掉发的小王
2022/07/11
3910
SpringBoot整合ActiveMq实现Queue和Topic两种模式
SpringBoot整合ActiveMQ
此处 spring.activemq.pool.enabled=false,表示关闭连接池。
崔笑颜
2020/06/08
3950
springboot与activeMQ入门(1)
下一篇,springboot与activeMQ入门(2:主从备份,负载均衡) 说明:acitveMQ版本为:5.9.1,springboot版本为2.0.3
用户2038589
2018/09/06
5250
springboot与activeMQ入门(1)
JMS学习之路(一):整合activeMQ到SpringMVC
JMS的全称是Java Message Service,即Java消息服务。它主要用于在生产者和消费者之间进行消息传递,生产者负责产生消息,而消费者负责接收消息。把它应用到实际的业务需求中的话我们可以在特定的时候利用生产者生成一消息,并进行发送,对应的消费者在接收到对应的消息后去完成对应的业务逻辑。对于消息的传递有两种类型,一种是点对点的,即一个生产者和一个消费者一一对应;另一种是发布/订阅模式,即一个生产者产生消息并进行发送后,可以由多个消费者进行接收。
肖哥哥
2019/02/22
1.6K1
JMS学习之路(一):整合activeMQ到SpringMVC
SpringBoot整合ActiveMQ
MQ的作用在前面文章中我已经介绍过了,那么今天就教大家学习整合ActiveMQ,篇幅有点长希望大家能看完。
Java编程指南
2019/08/02
4850
SpringBoot整合ActiveMQ
SpringBoot整合 ActiveMQ快速入门 实现点对点推送
ActiveMQ是一个高性能的消息服务, 它已经实现JMS接口(Java消息服务(Java Message Service),Java平台中关于面向消息中间件的接口), 所以我们可以直接在 Java 中使用
憧憬博客
2020/07/21
1.5K0
Spring Cloud 2.x系列之springboot集成ActiveMQ
消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构;是大型分布式系统不可缺少的中间件。目前使用较多的消息队列有ActiveMQ、RabbitMQ、Kafka、RocketMQ、MetaMQ等。springboot提供了对JMS系统的支持;springboot很方便就可以集成这些消息中间件。
BUG弄潮儿
2022/06/30
5840
Spring Cloud 2.x系列之springboot集成ActiveMQ
SpringBoot入门建站全系列(十七)整合ActiveMq(JMS类消息队列)
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。对于消息中间件,常见的角色大致也就有Producer(生产者)、Consumer(消费者)
品茗IT
2019/08/21
8920
ActiveMQ --- 整合篇
之前说到了activeMQ的一些基本用法,本文将介绍activeMQ如何与spring以及spring boot整合。
贪挽懒月
2019/10/22
5780
SpringBoot 整合 gradle 集成ActiveMQ
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-activemq/2.3.1.RELEASE
Java深度编程
2020/08/10
8740
SpringBoot 整合 gradle 集成ActiveMQ
SpringBoot整合ActiveMQ消息组件
1、ActiveMQ是Apache提供的开源组件,是基于JMS标准的实现组件。利用SpringBoot整合ActiveMQ组件,实现队列消息的发送与接收。修改pom.xml配置文件,追加spring-boot-starter-activemq依赖库。
别先生
2020/11/26
4080
SpringBoot整合ActiveMQ消息组件
ActiveMQ学习之SpringBoot整合ActiveMQ------>队列生产者和消费者
一、pom <!--聚合工程集成关系--> <!--统一整合第三方框架依赖信息--> <parent> <groupId>org.springframework.boot</grou
用户5899361
2020/12/07
5400
Spring Boot中集成ActiveMQ(九)
大家好,我是默语,一个专注于技术分享的博主。今天我们来聊聊 Spring Boot中集成ActiveMQ 的话题。在现代微服务架构中,消息队列(Message Queue,MQ)是一种非常重要的组件,用于实现服务间的异步通信、解耦和负载均衡。ActiveMQ是一个流行的开源消息队列实现,支持JMS(Java Message Service)规范。本文将详细介绍JMS和ActiveMQ的基础知识,如何在Spring Boot项目中集成ActiveMQ,包括依赖导入、配置、消息发送和消费的实现。通过这篇文章,您将全面掌握Spring Boot中使用ActiveMQ的技能,为您的项目添加强大的消息处理能力。让我们开始吧!🚀
默 语
2024/11/20
1760
ActiveMQ 客户端的开发
上篇文章 ActiveMQ 服务器的部署 实现了 ActiveMQ 服务器的部署,本文分别以官方 API、Spring、SpringBoot 三种方式,实现 ActiveMQ 消息的生成者和消费者。
IT技术小咖
2019/06/26
8660
ActiveMQ学习之SpringBoot整合ActiveMQ------>主题生产者和消费者
一、pom <!--聚合工程集成关系--> <!--统一整合第三方框架依赖信息--> <parent> <groupId>org.springframework.boot</grou
用户5899361
2020/12/07
3470
推荐阅读
相关推荐
springboot整合ActiveMQ1(基本使用)
更多 >
LV.0
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验