Web服务端通过SSE推送消息给浏览器客户端的实现方案(附详细代码和仓库地址)

news2024/12/28 4:10:00

目录

    • 1、SSE(Server-Sent Events)简介
    • 2、SSE 的工作原理
    • 3、SSE 与客户端轮询的区别和优势比较
      • 区别
      • 优势
    • 4、SSE简单实现(单机应用Demo)
      • 演示效果
      • SSE-Demo仓库地址
      • 下面直接贴代码:
      • 前端实现:
      • 后端实现:
    • 5、SSE简单实现(分布式应用Demo)
      • SSE-Demo仓库地址
      • 关键代码
      • 方案说明

1、SSE(Server-Sent Events)简介

Server-Sent Events (SSE) 是一种基于 HTTP 协议的服务器推送技术,允许服务器通过单个持久连接向客户端发送实时更新。客户端使用标准的 EventSource API 来接收服务器推送的事件,这种通信方式非常适合实时应用,如消息通知、股票行情更新、社交媒体更新等。

2、SSE 的工作原理

  • 单向连接:SSE 建立的是单向通道,即服务器向客户端推送数据,客户端只能接收,不能发送。
  • 持久连接:SSE 使用的是长连接(Long Polling),即连接一旦建立,将会持续存在,直到客户端或服务器关闭连接。
  • 文本数据:SSE 通过 text/event-stream MIME 类型传输数据,数据是纯文本格式。
  • 自动重连:如果连接中断,EventSource 会自动尝试重新连接,确保客户端能够接收后续的推送。

3、SSE 与客户端轮询的区别和优势比较

客户端轮询(Client Polling) 是一种传统的客户端从服务器请求数据的方式。客户端会定期向服务器发送请求,检查是否有新数据可用。

区别

连接方式:
SSE:建立后服务器主动推送数据,连接是持久的,数据在有更新时实时传递。
轮询:客户端定期发送请求获取数据,连接是间歇性的。

实时性:
SSE:数据几乎是实时推送的,延迟极低。
轮询:数据获取延迟取决于轮询的频率,频率高则延迟低,但频率低可能导致数据延迟。

网络和服务器负载:
SSE:由于是单个持久连接,减少了频繁的请求与响应开销,降低了服务器负载。
轮询:频繁的请求会增加服务器和网络的负担,尤其是在轮询频率较高时。

连接控制:
SSE:自动处理连接中断和重连,客户端实现简单。
轮询:需要客户端定期发起请求,且如果请求频率不当,可能导致资源浪费。

数据传输效率:
SSE:只在有数据更新时推送,传输效率高。
轮询:即使没有数据更新,客户端也会定期请求,效率低下。

优势

SSE 的优势:
更高效的网络和服务器资源利用率。
实时性更高,延迟更低。
实现简单,特别是在浏览器环境中,支持自动重连和事件处理。
适合需要频繁更新但客户端无需响应的场景。

客户端轮询的优势:
在不支持 SSE 的环境下仍然可以使用。
实现和理解相对简单,兼容性更好。

4、SSE简单实现(单机应用Demo)

演示效果

在这里插入图片描述

SSE-Demo仓库地址

https://github.com/deepjava-gm/SSE-Demo.git

下面直接贴代码:

前端实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE 用户消息推送 Demo</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        h1 {
            text-align: center;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 8px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        input[type="text"] {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            color: white;
            background-color: #007bff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #0056b3;
        }
        #messages {
            margin-top: 20px;
        }
        .message {
            background-color: #f1f1f1;
            padding: 10px;
            margin-bottom: 10px;
            border-radius: 4px;
        }
        .status {
            margin-top: 10px;
            font-weight: bold;
        }
        .success {
            color: green;
        }
        .error {
            color: red;
        }
    </style>
</head>
<body>
    <h1>SSE 用户消息推送 Demo</h1>
    <div class="container">
        <!-- 连接部分 -->
        <div class="form-group">
            <label for="userId">用户 ID:</label>
            <input type="text" id="userId" placeholder="请输入您的用户 ID">
            <br><br>
            <button onclick="connect()">连接</button>
        </div>
        <div id="connectionStatus" class="status"></div>

        <!-- 消息发送部分 -->
        <div class="form-group">
            <label for="targetUserId">目标用户 ID:</label>
            <input type="text" id="targetUserId" placeholder="请输入目标用户 ID">
        </div>
        <div class="form-group">
            <label for="message">消息内容:</label>
            <input type="text" id="message" placeholder="请输入要发送的消息">
        </div>
        <button onclick="sendMessage()">推送消息</button>

        <!-- 消息显示部分 -->
        <div id="messages"></div>
    </div>

    <script>
        let eventSource;
        let currentUserId = '';

        function connect() {
            const userId = document.getElementById('userId').value;
            const connectionStatus = document.getElementById('connectionStatus');

            if (!userId) {
                alert('请输入用户 ID');
                return;
            }

            // 显示连接状态为“连接中”
            connectionStatus.textContent = '已连接...';
            connectionStatus.className = 'status';

            eventSource = new EventSource(`http://localhost:9999/sse/connect/${userId}`);
            currentUserId = userId; // 保存当前用户 ID

            eventSource.onopen = function() {
                connectionStatus.textContent = '接收成功';
                connectionStatus.className = 'status success';
            };

            eventSource.onmessage = function(event) {
                try {
                    // 解析 JSON 消息
                    const data = JSON.parse(event.data);
                    const newElement = document.createElement('div');
                    newElement.className = 'message';
                    newElement.innerText = `用户 ${data.senderId} 接收的消息: ${data.message}`;
                    document.getElementById('messages').appendChild(newElement);
                } catch (e) {
                    console.error('消息解析错误:', e);
                }
            };

            eventSource.onerror = function(event) {
                connectionStatus.textContent = '连接失败,请检查网络或服务器';
                connectionStatus.className = 'status error';
                console.error("连接错误: ", event);
                eventSource.close();
            };
        }

        function sendMessage() {
            const targetUserId = document.getElementById('targetUserId').value;
            const message = document.getElementById('message').value;
            
            if (!targetUserId || !message) {
                alert('请填写目标用户 ID 和消息内容');
                return;
            }

            // 发送 GET 请求推送消息
            fetch(`http://localhost:9999/sse/push/${targetUserId}?message=${encodeURIComponent(message)}`, {
                method: 'GET'
            }).then(response => {
                if (response.ok) {
                    console.log('消息发送成功');
                } else {
                    console.log('消息发送失败');
                }
            }).catch(error => {
                console.error('发送错误:', error);
                alert('消息发送失败');
            });
        }
    </script>
</body>
</html>

后端实现:

启动类:

package io.github.deepjava;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.util.concurrent.ConcurrentHashMap;

@SpringBootApplication
public class SseDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SseDemoApplication.class);
    }


    // 注入一个全局缓存 用来保存不同用户的SSE连接信息
    @Bean("userSSEMap")
    public ConcurrentHashMap<String, SseEmitter> getUserSSEMap(){
        return new ConcurrentHashMap<>();
    }

}

Controller:

package io.github.deepjava.controller;

import io.github.deepjava.service.SseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.Resource;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RequestMapping("/sse")
@CrossOrigin(origins = "*")
@Slf4j
public class SseController {

    @Resource(name = "userSSEMap")
    private ConcurrentHashMap<String, SseEmitter> userSSEMap;

    @Resource
    private SseService sseService;

    // 连接方法:为用户 ID 注册 SSE 链接
    @GetMapping("/connect/{userId}")
    public SseEmitter connect(@PathVariable String userId) {
        SseEmitter emitter = new SseEmitter(0L); // 设置超时时间为无限大
        userSSEMap.put(userId, emitter);
        // 连接正常关闭回调 移除连接
        emitter.onCompletion(() -> {
            userSSEMap.remove(userId);
            log.info("连接正常关闭回调 移除连接");
        });
        // 连接超时回调 移除连接
        emitter.onTimeout(() -> {
            userSSEMap.remove(userId);
            log.info("连接超时回调 移除连接");

        });
        // 连接出错回调 移除连接
        emitter.onError((e) -> {
            userSSEMap.remove(userId);
            log.info("连接出错回调 移除连接");
        });
        log.info("连接成功!");
        return emitter;
    }

    // 推送方法:根据用户 ID 发送消息
    @GetMapping("/push/{userId}")
    public void push(@PathVariable String userId, @RequestParam String message) {
        sseService.extracted(userId, message);
    }

}

Service:

package io.github.deepjava.service;


import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Service
@Slf4j
public class SseService {

    @Resource(name = "userSSEMap")
    private ConcurrentHashMap<String, SseEmitter> clients;

    public void extracted(String userId, String message) {
        SseEmitter emitter = clients.get(userId);
        if (emitter != null) {
            try {
                // 创建包含用户 ID 和消息内容的 JSON 对象
                String jsonMessage = String.format("{\"senderId\":\"%s\", \"message\":\"%s\"}", userId, message);
                emitter.send(jsonMessage);
                log.info("消息推送成功!");
            } catch (IOException e) {
                clients.remove(userId);
                log.info("消息推送失败!");
            }
        }
    }

}

配置文件:application.properties

spring.application.name=sse-demo
server.port=9999

Maven的pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.1</version>
    </parent>

    <groupId>org.example</groupId>
    <artifactId>SSE-Demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>


    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

5、SSE简单实现(分布式应用Demo)

注意:
SSE 连接(如 SseEmitter)是持久化的、与具体服务器实例相关联的动态对象,无法直接存储在 Redis 等外部存储中。Redis 主要用于消息传递和共享数据,但无法直接管理活跃的连接。所以下面方案仅使用 Redis 进行消息广播。

解决方案概述
为了在分布式环境中实现 SSE,通常采用以下架构:

  • 每个服务器实例维护本地的 SSE 连接:每个实例只管理与自身连接的客户端。
  • 使用 Redis 进行消息广播:当需要向特定用户推送消息时,将消息发布到 Redis 频道。所有实例订阅该频道,并检查自己是否有需要向某个用户推送的连接。
  • 用户与实例的映射:使用 Redis 存储用户与服务器实例的映射信息,确保消息能够被正确路由到处理该用户连接的实例。
    虽然无法完全将连接信息存储在 Redis 中,但通过这种方式,可以有效地在分布式环境中管理 SSE 连接和消息推送。

这里只贴主要的后端代码:完整代码去下载仓库代码看。

SSE-Demo仓库地址

https://github.com/deepjava-gm/SSE-Demo.git

关键代码

redis配置:

 // 配置redis的序列化
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }


    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }

redis监听主题:

package io.github.deepjava.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RequestMapping("/dis/sse")
@CrossOrigin(origins = "*")
@Slf4j
public class DistributedSseController {

    @Resource(name = "userSSEMap")
    private ConcurrentHashMap<String, SseEmitter> userSSEMap;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Resource
    private RedisMessageListenerContainer redisMessageListenerContainer;

    private final ChannelTopic topic = new ChannelTopic("sse-messages");

    @PostConstruct
    public void init() {
        // 订阅 Redis 频道
        redisMessageListenerContainer.addMessageListener(
                new MessageListenerAdapter((MessageListener) (message, pattern) -> {
                    String payload = new String(message.getBody(), StandardCharsets.UTF_8);

                    // 假设消息的格式为 "userId:message"
                    String[] parts = payload.split(":", 2);
                    if (parts.length == 2) {
                        String userId = parts[0];
                        String userMessage = parts[1];
                        // 发送消息给本地的 SSE 连接
                        SseEmitter emitter = userSSEMap.get(userId);
                        if (emitter != null) {
                            try {
                                String jsonMessage = String.format("{\"senderId\":\"%s\", \"message\":\"%s\"}", userId, userMessage);
                                emitter.send(jsonMessage);
                            } catch (IOException e) {
                                emitter.completeWithError(e);
                                userSSEMap.remove(userId);
                            }
                        }
                    }
                }), topic);
    }

    // 连接方法:为用户 ID 注册 SSE 链接
    @GetMapping("/connect/{userId}")
    public SseEmitter connect(@PathVariable String userId) {
        SseEmitter emitter = new SseEmitter(0L); // 设置超时时间为无限大
        userSSEMap.put(userId, emitter);
        // 连接正常关闭回调 移除连接
        emitter.onCompletion(() -> {
            userSSEMap.remove(userId);
            log.info("连接正常关闭回调 移除连接");
        });
        // 连接超时回调 移除连接
        emitter.onTimeout(() -> {
            userSSEMap.remove(userId);
            log.info("连接超时回调 移除连接");

        });
        // 连接出错回调 移除连接
        emitter.onError((e) -> {
            userSSEMap.remove(userId);
            log.info("连接出错回调 移除连接");
        });
        log.info("连接成功!");
        return emitter;
    }


    @GetMapping("/push/{userId}")
    public void push(@PathVariable String userId, String message) {
        // 将消息发布到 Redis 频道
        redisTemplate.convertAndSend(topic.getTopic(), userId + ":" + message);
    }
}

方案说明

SSE 连接管理:
使用 ConcurrentHashMap<String, SseEmitter> 存储用户的连接信息,每个服务器实例只维护与自身连接的客户端。
connect 方法用于创建 SSE 连接并保存到本地缓存。

Redis 消息广播:
通过 Redis 的 发布订阅(Pub/Sub) 机制,所有实例订阅同一个频道(sse-messages)。
push 方法将消息发布到 Redis 频道,所有订阅了该频道的实例都会收到消息,并检查是否有对应的连接需要推送。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2094821.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【pycharm】汉化及翻译插件

汉化插件 翻译插件 使用 选中右键翻译

一键解决LBP2900通信错误的问题(同样支持Win 11系统)

**目录** **前言****常见解决方式****方案一&#xff1a;端口排除****方案二&#xff1a;服务重启****方案三&#xff1a;注册表注入修复** 前言 佳能LBP2900向来是经典耐用款的打印机。想必各位可能遇到过&#xff0c;由于老旧会出现奇葩的问题&#xff0c;譬如 就算USB接口已…

【C++篇】~类和对象(上)

【C篇】 类和对象上 一类二实例化内存对齐原因&#xff08;用空间换时间&#xff0c;提高效率&#xff09; 一类 ‘类’class可以理解为C语言阶段的‘结构体’&#xff0c;它的用法与struct大差不差很多地方都相同&#xff0c;但是C毕竟是C&#xff0c;类的用法肯定比结构体的…

Linux Kernel 6.12版预计将支持在崩溃后显示二维码 后续可以解码排查错误

7 月份时红帽工程师基于 systemd 255 版的全屏显示错误消息功能为 Linux Kernel 开发崩溃后显示二维码选项&#xff0c;这与微软在 Windows 10/11 蓝屏死机后显示二维码有异曲同工之妙。 不过 Linux 与 Windows 在崩溃时显示的二维码内容则有本质区别&#xff0c;因为 Window…

单链表反转(C语言)

1 问题描述 力扣&#xff08;LeetCode&#xff09;--反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 例如&#xff1a; 输入&#xff1a;head [1,2,3,4,5]输出&#xff1a;[5,4,3,2,1] 输入&#xff1a;head [1,2]输出&#x…

11 Java 方法引用、异常处理

文章目录 前言一、Java接口之函数式编程 --- 接口知识补充1 Function<T,R>泛型接口2 BiFunction<T, U, R>泛型接口3 自定义泛型函数式编程接口 二、方法引用1 方法引用初体验&#xff08;以Array.sort()方法为例&#xff09;2 引用静态方法3 引用其他类成员方法 前…

【面试五】PID控制算法

一、 PID算法简介 PID&#xff08;Proportional-Integral-Derivative&#xff09;控制算法是一种经典的反馈控制方法&#xff0c;广泛应用于自动控制系统&#xff0c;例如温度控制、速度控制、位置控制等。 PID控制算法的核心包含三个部分&#xff1a;比例项&#xff08;P&…

@antv/g6 业务场景:流程图

1、流程图是流经一个系统的信息流、观点流或部件流的图形代表。在企业中&#xff0c;流程图主要用来说明某一过程。这种过程既可以是生产线上的工艺流程&#xff0c;也可以是完成一项任务必需的管理过程。业务场景流程图如下&#xff1a; 2、绘制流程图的 Tips 流程图一般是用…

计算机毕业设计选题推荐-果树生长信息管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

Redis三种集群模式:主从模式、哨兵模式和Cluster模式

1. 总结经验 redis主从&#xff1a;可实现高并发(读)&#xff0c;典型部署方案&#xff1a;一主二从 redis哨兵&#xff1a;可实现高可用&#xff0c;典型部署方案&#xff1a;一主二从三哨兵 redis集群&#xff1a;可同时支持高可用(读与写)、高并发&#xff0c;典型部署方…

探索Python数据持久化的秘密:ZODB库的神奇之旅

文章目录 探索Python数据持久化的秘密&#xff1a;ZODB库的神奇之旅背景ZODB是什么&#xff1f;如何安装ZODB&#xff1f;简单库函数使用方法场景应用常见Bug及解决方案总结 探索Python数据持久化的秘密&#xff1a;ZODB库的神奇之旅 背景 在Python的广阔世界中&#xff0c;数…

基于单片机的水箱水质监测系统设计

本设计基于STM32F103C8T6为核心控制器设计了水质监测系统&#xff0c;选用DS18B20温度传感器对水箱水体温度进行采集&#xff1b;E-201-C PH传感器获取水体PH值&#xff1b;选用TS-300B浊度传感器检测水体浊度&#xff1b;采用YW01液位传感器获取水位&#xff0c;当检测水位低于…

网络压缩之知识蒸馏(knowledge distillation)

因为直接训练一个小的网络&#xff0c;往往结果就是没有从大的网络剪枝好。知识蒸馏的概念是 一样的&#xff0c;因为直接训练一个小的网络&#xff0c;没有小的网络根据大的网络来学习结果要来得 好。 因而&#xff0c;先训练一个 大的网络&#xff0c;这个大的网络在知识蒸馏…

Flutter 初识:Chip控件

Flutter Chip控件小结 Chip属性解析示例 InputChip属性解析示例 ChoiceChip属性解析示例 FilterChip属性解析示例 ActionChip属性解析示例 在 Flutter 中&#xff0c;Chip 是一种用于显示简洁信息的组件。它通常用来展示标签、属性、短的文本片段等&#xff0c;并可以包含可选的…

C语言推箱子迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <Windows.h> typedef stru…

python内置模块datetime.date类详细介绍

Python的datetime模块是一个强大的日期和时间处理库&#xff0c;它提供了多个类来处理日期和时间。主要包括几个功能类datetime.date、datetime.time、datetime.datetime、datetime.timedelta,datetime.timezone等。 使用datetime模块 要使用 datetime模块&#xff0c;直接导…

大模型辅助软件开发,助力工程师的开发之路

大模型与软件工程师&#xff1a;改变开发范式的力量 “是人类工程师的能力&#xff0c;而不是大模型的能力&#xff0c;决定了大模型协作式开发的上限。” 这句话深刻地揭示了在人工智能&#xff0c;尤其是大型语言模型&#xff08;LLM&#xff09;飞速发展的今天&#xff0c…

《数字信号处理》学习02-序列的能量及周期性

目录 一&#xff0c;序列的能量 二&#xff0c;序列的周期性 一&#xff0c;序列的能量 序列能量在数字信号处理中的应用&#xff1a;能量归一化。在信号处理中&#xff0c;有时需要对信号进行归一化处理&#xff0c;使得信号的能量为特定的值&#xff0c;这在一些算法和系统…

无主灯吊顶的精致做法:打造光影艺术的居家空间

在现代家居设计中&#xff0c;无主灯吊顶以其独特的照明效果和空间层次感&#xff0c;逐渐成为追求高品质生活人群的首选。无主灯设计不仅能够有效避免传统主灯带来的刺眼感&#xff0c;还能通过多点光源的巧妙布局&#xff0c;营造出温馨、舒适的居家氛围。作为无主灯照明灯具…

洛谷 P3183 [HAOI2016]食物链(记忆化搜索/拓扑排序)

[HAOI2016]食物链 给定 n 个物种和 m 条能量流动关系&#xff0c;求其中的食物链条数。物种的名称从 1 到 n 编号&#xff0c; M 条能量流动关系形如 a1​→b1​,a2​→b2​,a3​→b3​⋯am−1​→bm−1​,am​→bm​ 其中 ai​→bi​ 表示能量从物种 ai​ 流向物种 bi​ ,注意…