Java版本的SSE服务端实现样例

news2024/10/6 10:53:03

简单记录一下使用netty方式实现SSE的服务端功能

目录

  • 简要说明
  • 基于Netty
  • 功能需求
  • 后端代码
    • 1. 创建一个SpringBoot 应用
    • 2. 创建服务端功能
    • 3. 创建前端功能
    • 4. 测试SSE
  • 总 结

简要说明

Server-Sent Events (SSE) 是一种用于在客户端和服务器之间建立单向通信的技术。
它允许服务器主动向客户端推送实时更新,而不需要客户端不断地请求数据。
Server-Sent Events (SSE) 的流行可以追溯到 HTML5 的引入,

最大特点:

  • 前端JS原生支持
  • 只接受服务端数据,单向通讯
  • 原生支持断开重连

他和我们现在经常接触的 websocket,mqtt,类rabbitmq 有说明区别,
同样是客户端服务端的数据访问,同样用于取代客户端轮询访问方式,他们有审美不一样或者说使用场景是什么,下面表格简要说明一下:

技术SSE (Server-Sent Events)WebSocketMQTT类RabbitMQ
类型单向通信双向通信发布/订阅模式消息队列
协议基于 HTTP独立于 HTTP轻量级消息传递协议支持多种协议(如 AMQP)
使用场景实时更新(如新闻/股票信息推送)实时双向通信(如聊天)物联网设备通信,例如硬件设备主动上报给服务端信号信息可靠消息传递和任务队列,用于服务端系统之间通讯
优点- 简单易用- 低延迟- 轻量级- 强大的消息路由功能
- 自动重连- 支持双向通信- 支持 QoS 级别- 提供持久化消息存储
- 支持文本数据推送- 支持二进制数据传输- 发布/订阅解耦- 支持多种消息模式
缺点- 仅支持单向通信- 实现相对复杂- 需要 MQTT 代理- 设置和管理相对复杂
- 不支持二进制数据- 需要额外的安全措施- 对简单实时应用复杂- 可能需要更多资源

基于Netty

关于java版本的SSE服务端实现,网上大多举例不正确或者说并没有实现SSE的技术特性 (例如网上举例说 创建一个 servlet,你会发现基本上是http轮询,因为一次service请求后,IO通讯就断开了,前端只会不断重连请求)。

Netty 强大健壮的异步IO通讯框架。

功能需求

  • 在SpringBoot项目中创建SSE服务端功能
  • 基于Netty框架
  • 前端样例可自由断开或连接
  • 支持携带Get请求的参数
  • 前端样例支持断开重连,连接状态展示

后端代码

1. 创建一个SpringBoot 应用

这里测试的SpringBoot 版本是 2.6.14
使用的JDK版本是 17

2. 创建服务端功能

引入netty Maven依赖

        <!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.113.Final</version>
        </dependency>

创建 SSE 服务端的EventLoopGroup,假设绑定端口 8849


import com.middol.yfagv.model.oms.properties.SseProperties;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;

import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;


/**
 * SSE服务 server sent events
 *
 * @author admin
 */
@Slf4j
@Service
public class SseServer {
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    private boolean started = false;

    @PostConstruct
    public void init() {
        log.debug("SSE服务初始化完毕");
    }

    public void shutdown() {
        log.debug("SSE服务 Shutting down server...");
        // 优雅关闭 workerGroup
        if (!workerGroup.isShutdown()) {
            workerGroup.shutdownGracefully(5, 10, TimeUnit.SECONDS);
        }
        // 优雅关闭 bossGroup
        if (!bossGroup.isShutdown()) {
            bossGroup.shutdownGracefully(5, 10, TimeUnit.SECONDS);
        }
        log.debug("SSE服务 Server shut down gracefully.");
    }

    @Async
    public void start() throws Exception {
        if (started) {
            return;
        }
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new HttpServerCodec());
                            ch.pipeline().addLast(new HttpObjectAggregator( 1024 * 1024));
                            ch.pipeline().addLast(new ChunkedWriteHandler());
                            ch.pipeline().addLast(new SseHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 绑定端口并同步
            ChannelFuture f = b.bind(8849).sync();
            log.debug("SSE服务启动完成,绑定端口:{}", 8849);
            started = true;
            // 添加关闭钩子
            Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
            // 等待服务器通道关闭
            f.channel().closeFuture().sync();
        } finally {
            shutdown();
        }
    }
}

创建处理前端请求的 ChannelHandler,这里我们假设只处理url 是 /events的前端请求。

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

/**
 * SSE处理器
 *
 * @author admin
 */
@Slf4j
@ChannelHandler.Sharable
public class SseHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private static final String PREFIX = "events";

    @Override
    public void channelActive(io.netty.channel.ChannelHandlerContext ctx) throws Exception {
        // 获取远程地址
        String remoteAddress = ctx.channel().remoteAddress().toString();
        log.debug(">>>>>>>>>>>>>>>>>>>>>>>>SseHandler: channelActive, remoteAddress={}", remoteAddress);
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 获取远程地址
        String remoteAddress = ctx.channel().remoteAddress().toString();
        log.debug(">>>>>>>>>>>>>>>>>>>>>>>>SseHandler: channelInactive, remoteAddress={}", remoteAddress);
        super.channelInactive(ctx);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
        if (request.method() == HttpMethod.OPTIONS) {
            // 处理预检请求
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
            response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS");
            response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type");
            ctx.writeAndFlush(response);
            return;
        }

        if (HttpUtil.is100ContinueExpected(request)) {
            send100Continue(ctx);
        }

        // 检查请求的 URI 是否以指定的前缀开始
        String uri = request.uri();
        // 解析 GET 参数
        QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri);
        Map<String, List<String>> parameters = queryStringDecoder.parameters();
        if (parameters != null && !parameters.isEmpty()) {
            log.debug(">>>>>>>>>>>>>>>>>>>>>>>>SseHandler: parameters={}", parameters);
        }
        if (!uri.startsWith("/" + PREFIX)) {
            ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND));
            return;
        }

        // 设置 CORS 头
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/event-stream");
        response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache");
        response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);

        // CORS 头
        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); // 允许所有域
        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS"); // 允许的请求方法
        response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type"); // 允许的请求头

        ctx.write(response);

        // 发送初始 SSE 事件
        sendSseEvent(ctx, "Connected to SSE server");

        // 定期发送 SSE 事件
        long initialDelay = 0L;
        long period = 5L;
        ctx.executor().scheduleAtFixedRate(
                () -> sendSseEvent(ctx, "CurrentTimeMillis: " + System.currentTimeMillis()),
                initialDelay, period, java.util.concurrent.TimeUnit.SECONDS);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 获取远程地址
        String remoteAddress = ctx.channel().remoteAddress().toString();
        log.error(">>>>>>>>>>>>>>>>>>>>>>>>SseHandler: exceptionCaught, remoteAddress={}", remoteAddress, cause);
        // 关闭连接,自动释放相关资源
        ctx.close();
    }

    protected static void send100Continue(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.write(response);
    }

    protected void sendSseEvent(ChannelHandlerContext ctx, String data) {
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeBytes(("data: " + data + "\n\n").getBytes(StandardCharsets.UTF_8));
        ctx.writeAndFlush(new DefaultHttpContent(buffer));
    }

}

以上核心业务方法是 channelRead0, 里面设置了可以跨越,
为什么要跨越?原因是netty服务端绑定的端口和本身 SpringBoot的应用端口不是一样,前端页面可能即要请求SpringBoot的业务接口也需要SSE服务接口。

以上是简单模拟向前端页面推送时间戳信息,每隔5秒一次,如果我要推送Bean方法里面的业务数据该如何做?

最简单方法是修改sendSseEvent里面的业务逻辑,使用SpringUtil获得Bean

    protected void sendSseEvent(ChannelHandlerContext ctx, String data) {
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeBytes(JSONObject.toJSONString(SpringUtil.getBean(YourService.class).querySome()).getBytes(StandardCharsets.UTF_8));
        
        ctx.writeAndFlush(new DefaultHttpContent(buffer));
    }

最后写一个手动启动Netty服务的Controller方法,这个主要用于测试,可以设置SpringBoot启动时自动启动Netty服务。

    @Lazy
    @Resource
    private SseServer sseServer;

    @ApiOperation(value = "启动SSE服务")
    @PostMapping("startSseServer")
    public ResponseVO<String> startSseServer() {
        try {
            sseServer.start();
        } catch (Exception e) {
            return ResponseVO.fail(e.getMessage(), DateUtil.now());
        }
        return ResponseVO.success(ResponseVO.SUCCESS_MSG, DateUtil.now());
    }

3. 创建前端功能

前端代码,我直接ChatGPT帮我生成一个用于测试SSE功能的页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSE Example</title>
    <script>
        let eventSource; // 声明 eventSource 变量
        let isConnected = false; // 连接状态标志

        function toggleEventSource() {
            const button = document.getElementById('toggleButton');
            const inputUrl = document.getElementById('urlInput').value.trim(); // 获取输入框中的 URL

            if (!inputUrl) {
                alert('请输入有效的 URL');
                return;
            }

            if (isConnected) {
                eventSource.close(); // 关闭连接
                button.innerText = "开启 EventSource"; // 更新按钮文本
                button.classList.remove('close'); // 移除关闭状态的样式
                button.classList.add('open'); // 添加开启状态的样式
                document.getElementById('status').innerText = "Disconnected from SSE server.";
                document.getElementById('status').style.color = "red";
                isConnected = false; // 更新连接状态
            } else {
                eventSource = new EventSource(inputUrl); // 使用输入框中的 URL 创建新的 EventSource 实例

                eventSource.onopen = function() {
                    console.log("Connection to server opened.");
                    const status = document.getElementById('status');
                    status.innerText = "Connected to SSE server.";
                    status.style.color = "green";
                    status.style.fontWeight = "bold";
                    button.innerText = "关闭 EventSource"; // 更新按钮文本
                    button.classList.remove('open'); // 移除开启状态的样式
                    button.classList.add('close'); // 添加关闭状态的样式
                    isConnected = true; // 更新连接状态
                };

                eventSource.onmessage = function(event) {
                    console.log("Received message: " + event.data);
                    const messagesDiv = document.getElementById('messages');
                    messagesDiv.innerHTML += `<p>${event.data}</p>`;

                    // 检查行数并在超过100行时清空内容
                    const lines = messagesDiv.getElementsByTagName('p').length;
                    if (lines > 100) {
                        messagesDiv.innerHTML = ''; // 清空内容
                        console.log("Messages cleared after exceeding 100 lines.");
                    }
                };

                eventSource.onerror = function() {
                    console.error("EventSource failed.");
                    const status = document.getElementById('status');
                    status.innerText = "Exception: Connection to SSE server lost.";
                    status.style.color = "red";
                    status.style.fontWeight = "bold";
                    button.innerText = "开启 EventSource"; // 更新按钮文本
                    button.classList.remove('close'); // 移除关闭状态的样式
                    button.classList.add('open'); // 添加开启状态的样式
                    isConnected = false; // 更新连接状态
                };
            }
        }
    </script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            padding: 20px;
            background-color: #f4f4f4;
            border-radius: 8px;
        }
        #toggleButton {
            padding: 10px 20px; /* 增加内边距 */
            font-size: 16px; /* 增大字体 */
            color: white; /* 字体颜色 */
            border: none; /* 去掉边框 */
            border-radius: 5px; /* 圆角 */
            cursor: pointer; /* 鼠标悬停时显示手型 */
            transition: background-color 0.3s; /* 背景颜色过渡效果 */
        }
        #toggleButton.open {
            background-color: #007bff; /* 开启状态按钮背景颜色 */
        }
        #toggleButton.open:hover {
            background-color: #0056b3; /* 开启状态悬停时的背景颜色 */
        }
        #toggleButton.open:active {
            background-color: #004080; /* 开启状态点击时的背景颜色 */
        }
        #toggleButton.close {
            background-color: #f17b87; /* 关闭状态按钮背景颜色 */
        }
        #toggleButton.close:hover {
            background-color: #d9534f; /* 关闭状态悬停时的背景颜色 */
        }
        #toggleButton.close:active {
            background-color: #c9302c; /* 关闭状态点击时的背景颜色 */
        }
        #urlInput {
            width: 400px; /* 输入框宽度 */
            padding: 8px; /* 增加内边距 */
            font-size: 16px; /* 增大字体 */
            margin-right: 10px; /* 添加与按钮的间距 */
        }
        #messages {
            margin-top: 20px;
            padding: 10px;
            background-color: #fff;
            border: 1px solid #ccc;
            border-radius: 4px;
            max-height: 300px;
            overflow-y: auto;
        }
        p {
            margin: 5px 0;
            font-size: 18px; /* 增大内容字体大小 */
        }
        #status {
            font-size: 20px; /* 增大状态字体大小 */
        }
    </style>
</head>
<body>
<h1>SSE Example</h1>
<label for="urlInput"></label><input type="text" id="urlInput" placeholder="Enter SSE URL" value="http://localhost:8849/events"/> <!-- URL 输入框 -->
<button id="toggleButton" class="open" onclick="toggleEventSource()">开启 EventSource</button>
<br/><br/><br/>
<div id="status">请点击 [开启 EventSource] 按钮开启 EventSource。</div>
<br/>
<div id="messages"></div>
</body>
</html>

4. 测试SSE

启动SpringBoot服务,启动Netty服务

谷歌浏览器输入 http://localhost:8088/test1.html
这里的8088是SpringBoot应用端口,test1.html 是以上创建的页面,放在SpringBoot 的静态资源文件目录下的页面文件。
在这里插入图片描述
点击 【开启 EventSource】
在这里插入图片描述
点击 关闭 按钮
在这里插入图片描述
再次开启,然后关闭后台服务,然后再次开启后台服务测试 SSE自动重连
在这里插入图片描述

在这里插入图片描述

总 结

依据Netty的高性能实现的SSE服务端功能,基本上实现了SSE的所有技术特点,在理想情况下,一台 普通的 32GB 内存的服务器可以支持数几十万个前端连接,基本满足中小企业业务需求量。

使用SSE其实针对中小企业来说最大的优点其实是部署的便捷性,无需引入其他消息队列中间件等服务。

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

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

相关文章

【复习】JS中的数据类型

文章目录 数据类型UndefinedNullBooleanNumberStringSymbolBigIntObjectArrayFunctionDateRegExp 数据类型 其实就两种&#xff0c;原始数据类型&#xff08;Primitive Types&#xff09;和引用数据类型&#xff08;Reference Types&#xff09; JS将数据分为七种数据类型&…

降压芯片TPS54821

降压芯片TPS54821 介绍 价格低廉&#xff0c;只需1.5元。是一个同步整流降压BUCK电路。MOS管内置。输入电压为4.5V至17V&#xff0c;输出电压为0.6V到15V&#xff0c;输出电流最大到8A。是QFN封装&#xff0c;焊接时有些许困难。得益于QFN封装&#xff0c;其引线电感非常的小…

振动分析-30-振动信号的幅值概率密度函数CWRU西楚大学轴承数据(实战)

文章目录 1 背景2 幅值概率密度函数3 实现流程3.1 自定义函数3.2 模拟正弦信号4 CWRU轴承数据4.1 加载数据4.2 相同工况不同故障4.3 相同数据不同份数5 参考附录1 背景 很多初学者刚接触故障诊断可能觉得很简单,套用深度学习模型进行训练,分类准确率达到99%即可。 在写论文时…

LabVIEW提高开发效率技巧----严格类型化定义

在LabVIEW开发过程中&#xff0c;严格类型化定义&#xff08;Strict Typedefs&#xff09; 是一种工具&#xff0c;用于保证程序中控件和常量的一致性&#xff0c;减少错误&#xff0c;提高维护效率。通过使用严格类型化定义&#xff0c;开发者可以确保在程序的多个地方引用相同…

Day02-MySQL数据库服务体系结构

Day02-MySQL数据库服务体系结构 1、数据库服务连接管理2、数据库服务应用配置2.1 服务进行配置有什么作用&#xff1f;2.2 应用配置有三种方式&#xff1a; 3、数据库服务多实例构建4、数据库服务版本升级4.1 实现升级的方法&#xff1a;4.2 常见的数据库服务程序升级方式&…

【深入理解SpringCloud微服务】手写实现断路器算法

【深入理解SpringCloud微服务】手写实现断路器算法 断路器状态切换断路器接口断路器算法实现相关属性failed()success()canPass() 断路器状态切换 在分析断路器算法前&#xff0c;我们先复习一下断路器的状态转换。 断路器一般有三个状态&#xff1a;关闭、打开、半开。 断路…

【瑞昱RTL8763E】歌曲传输

1 概要 Watch 端 SD 卡中的歌曲除了可以通过 USB 传输&#xff0c;还可以通过 SPP/BLE 传输来完成歌曲的添加与删 除操作。其中&#xff0c;Android 手机可以安装 LocalPlayback.apk 使用 SPP 协议与 watch 交互&#xff1b;iOS 手机可以安装 LocalPlayback.ipa 通过 BLE 与 wa…

Python 工具库每日推荐 【Matplotlib】

文章目录 引言Python数据可视化库的重要性今日推荐:Matplotlib工具库主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:数据分析可视化案例分析高级特性自定义样式动画效果3D绘图性能优化技巧扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 T…

【可答疑】基于51单片机的无线病床呼叫系统(含仿真、代码、报告、演示视频等)

✨哈喽大家好&#xff0c;这里是每天一杯冰美式oh&#xff0c;985电子本硕&#xff0c;大厂嵌入式在职0.3年&#xff0c;业余时间做做单片机小项目&#xff0c;有需要也可以提供就业指导&#xff08;免费&#xff09;~ &#x1f431;‍&#x1f409;这是51单片机毕业设计100篇…

什么是虚拟化?| 裸机 vs 虚拟机 vs 容器

“云计算&#xff01;DevOps&#xff01;Docker&#xff01;Kubernetes&#xff01;……” 如果您是一名软件工程师&#xff0c;还没有遇到过以上这些流行词&#xff0c;那么您可能一直生活在与世隔绝的地方。 所有这些技术都与同一样东西有关&#xff0c;对&#xff0c;就是…

openEuler 24.03 (LTS) 部署 K8s(v1.31.1) 高可用集群(Kubespray Ansible 方式)

写在前面 实验需要一个 CNI 为 flannel 的 K8s 集群之前有一个 calico 的版本有些旧了,所以国庆部署了一个v1.31.1 版本 3 * master 5 * work时间关系直接用的工具 kubespray博文内容为部署过程以及一些躺坑分享需要科学上网理解不足小伙伴帮忙指正 &#x1f603;,生活加油 99…

IEC104规约的秘密之七----配置参数t1,t2,t3

104通讯前需要配置通讯参数&#xff0c;一般有如下参数&#xff1a; IP地址&#xff0c;端口号&#xff0c;k&#xff0c;w&#xff0c;t1&#xff0c;t2&#xff0c;t3&#xff0c;公共地址&#xff0c;遥控超时参数&#xff0c;104主规约还有一个t0参数。 本次只讲解t1&#…

2-113 基于matlab的图像的配准融合

基于matlab的图像的配准融合&#xff0c;采用互信息配准&#xff0c;PV差值&#xff0c;powell算法&#xff0c;小波变换的图像融合算法。在GUI界面输入两幅图像&#xff0c;完成图像的配准融合。融合图像要求像素 一样。程序代码已经有详细的注释。程序已调通&#xff0c;可直…

对操作系统中的用户态和内核态的理解

目录 引言 为什么要有用户态和内核态&#xff1f;只有一个内核态不行么&#xff1f; 一、用户态&#xff08;User Mode&#xff09; 定义 特点 应用 二、内核态&#xff08;Kernel Mode&#xff09; 定义 特点 应用 三、用户态与内核态的联系和区别 四、用户态和内…

通过dem2terrain生成MapboxGL地形服务

概述 MapboxGL在2的版本之后通过地形服务开始支持三维的展示了&#xff0c;之前也有文章“mapboxGL2中Terrain的离线化应用”对该服务进行过说明与分析。前些天在翻公众号的时候翻到了dem2terrain可以生成地形服务&#xff0c;同时做了一些优化&#xff0c;今天就给大家分享一…

2024全面升级!从零开始的大模型开发学习路线图——精通之路

第一阶段&#xff1a;基础理论入门 目标&#xff1a;了解大模型的基本概念和背景。 内容&#xff1a; 人工智能演进与大模型兴起。 大模型定义及通用人工智能定义。 GPT模型的发展历程。 第二阶段&#xff1a;核心技术解析 目标&#xff1a;深入学习大模型的关键技术和工…

多文件并发多线程MD5工具(相对快速的MD5一批文件),适配自定义MD5 Hash I/O缓存。

自己写的多文件 MD5校验工具&#xff0c;一个文件开一个线程&#xff0c;有最大I/O 缓存设置&#xff0c;兼容读写MD5后缀文件。 共计91个文件&#xff0c;合计180G左右 12分钟左右&#xff0c;UI基本卡废&#xff0c;但程序没蹦&#xff0c;属于正常。 卡的原因是基本是用 I/O…

每日OJ题_牛客_牛牛冲钻五_模拟_C++_Java

目录 牛客_牛牛冲钻五_模拟 题目解析 C代码 Java代码 牛客_牛牛冲钻五_模拟 牛牛冲钻五 (nowcoder.com) 描述&#xff1a; 牛牛最近在玩炉石传说&#xff0c;这是一款一对一对战的卡牌游戏&#xff0c;牛牛打算努力冲上钻五分段&#xff0c;获得丰厚的天梯奖励。…

力扣 中等 78.子集

文章目录 题目介绍解法解法一&#xff1a;解法二&#xff1a; 题目介绍 解法 有两种解法&#xff0c;对于计算[1,2]的子集问题&#xff1a; 解法一&#xff1a; 站在输入的角度思考&#xff1a;每个元素都可以选/不选 代码如下&#xff1a; class Solution {List<List&…

ReGCL Rethinking Message Passingin Graph Contrastive Learning

AAAI24 推荐指数&#xff1a; #paper/⭐ 总体说&#xff1a;利用梯度对对比正负样本加权的。个人觉得和与正负样本加权没有区别&#xff0c;读完之后不想做笔记了。