WebSocket vs SSE: 实时数据推送到前端的选择与实现(详细)
Websocket和Server-Sent Events 对比推送数据给前端及各自的实现
- 二者对比
- WebSocket:
- Server-Sent Events (SSE):
- 选择 WebSocket 还是 SSE:
- Websocket 实现
- 使用原生 WebSocket API:
- 使用 Netty 创建 WebSocket:
- 总结和选择:
- Netty 实现 Websocket
- Server-Sent Events (SSE)实现
- 创建DataManager
- 接口实现
- 实现说明
- 前端实现
- 弊端以及解决方案
在现代 Web 应用程序中,实时数据推送给前端变得越来越重要。无论是实时聊天、实时通知还是仪表板上的实时更新,都需要一种有效的方式来将数据推送给前端。本文将介绍两种常用的实现方法:WebSocket 和 Server-Sent Events(SSE),并提供详细的实现步骤。
二者对比
WebSocket 和 Server-Sent Events (SSE) 都是用于实现实时数据推送的技术,但它们在设计、用途和实现上有一些重要的区别。让我们详细比较这两种技术。
WebSocket:
-
双向通信:
- WebSocket 允许双向通信,客户端和服务器都可以在任何时候向对方发送数据。
- 这使得 WebSocket 非常适用于需要双向交互的应用,如在线聊天、多人协作工具等。
-
持久连接:
- WebSocket 建立持久连接,客户端和服务器之间的连接保持打开状态。
- 这减少了与建立和关闭连接相关的开销,适用于频繁的数据交换。
-
低延迟:
- 由于持久连接,WebSocket 可以实现低延迟的实时数据传输,适用于需要快速响应的应用。
-
复杂性:
- 实现 WebSocket 可能相对复杂,需要更多的服务器资源和额外的协议处理。
-
跨域通信:
- WebSocket 通常需要配置服务器以允许跨域通信,因为它们使用自定义协议。
-
浏览器支持:
- WebSocket 在现代浏览器中得到广泛支持。
Server-Sent Events (SSE):
-
单向通信:
- SSE 是一种单向通信,只允许服务器向客户端发送数据。客户端无法向服务器发送数据。
-
HTTP 协议:
- SSE 建立在 HTTP 协议之上,使用标准 HTTP 请求和响应。
- 这使得 SSE 更容易部署,因为它与现有的 HTTP 基础设施兼容。
-
简单性:
- SSE 的实现相对简单,服务器和客户端都不需要太多复杂的逻辑。
-
无需专用库:
- SSE 不需要额外的库或协议处理,客户端可以使用浏览器的原生 EventSource API 来接收数据。
-
跨域通信:
- SSE 支持跨域通信,可以通过 CORS(跨域资源共享)机制进行配置。
-
浏览器支持:
- SSE 在现代浏览器中也得到广泛支持,但与 WebSocket 相比,它的历史要长一些。
选择 WebSocket 还是 SSE:
-
WebSocket 适用于需要双向通信和低延迟的场景,例如在线游戏、实时聊天应用等。
-
SSE 适用于单向服务器到客户端的实时数据推送,例如新闻更新、实时股票报价、天气预报等,特别是当你希望使用现有的 HTTP 基础设施时。
-
在某些情况下,你甚至可以同时使用 WebSocket 和 SSE,根据不同的需求选择合适的技术。
无论选择哪种技术,都需要考虑你的应用程序的具体需求和复杂性。WebSocket 提供了更多的灵活性和功能,而 SSE 更加简单和易于部署。最终的选择取决于你的项目目标和资源。
Websocket 实现
使用原生 WebSocket API:
-
简单性:
- Spring Boot 提供了对原生 WebSocket API 的支持,使得创建 WebSocket 应用相对简单。
- 开发人员可以直接使用 Java 标准库中的 WebSocket 相关类来处理 WebSocket 通信。
-
依赖:
- 原生 WebSocket 不需要额外的依赖,因为 WebSocket API 已经包含在 Java 标准库中。
-
性能:
- 原生 WebSocket API 在性能方面表现良好,适用于大多数中小型应用。
-
生态系统:
- 使用原生 WebSocket 可以更容易地集成到现有的 Spring Boot 生态系统中,例如 Spring Security 等。
-
简单应用:
- 当你需要创建相对简单的 WebSocket 应用时,原生 WebSocket 是一个不错的选择。
使用 Netty 创建 WebSocket:
-
灵活性:
- Netty 是一个高度可定制的异步事件驱动框架,它可以用于创建各种网络应用,包括 WebSocket。
- Netty 提供了更多的灵活性和自定义选项,适用于复杂的 WebSocket 应用。
-
性能:
- Netty 以其高性能和低延迟而闻名,适用于需要处理大量并发连接的应用。
-
协议支持:
- Netty 支持多种协议,不仅限于 WebSocket。这意味着你可以在同一个应用程序中处理多种网络通信需求。
-
集成:
- 尽管 Netty 可以集成到 Spring Boot 中,但其集成可能需要更多的配置和代码。
-
复杂应用:
- 当你需要处理复杂的 WebSocket 场景,如高并发、自定义协议、复杂的消息处理等时,使用 Netty 是更好的选择。
总结和选择:
选择原生 WebSocket 还是使用 Netty 创建 WebSocket 应取决于你的项目需求和复杂性:
-
如果你的应用相对简单,对性能要求不是很高,可以考虑使用原生 WebSocket API,它更容易上手并且不需要额外的依赖。
-
如果你的应用需要处理高并发、复杂的协议、自定义消息处理或需要最大程度的性能和灵活性,那么使用 Netty 创建 WebSocket 可能更合适。Netty 为你提供了更多的控制权和自定义选项。
无论你选择哪种方法,Spring Boot 都提供了良好的支持,使得在应用中集成 WebSocket 变得相对容易。因此,你可以根据具体的项目需求来选择适合你的方法。
Netty 实现 Websocket
-
添加 maven 坐标
<!-- netty --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-common</artifactId> <version>4.1.79.Final</version> </dependency>
-
创建 NettyWebsocketServer
package com.todoitbo.baseSpringbootDasmart.netty.server; import com.todoitbo.baseSpringbootDasmart.netty.handler.HeartbeatHandler; import com.todoitbo.baseSpringbootDasmart.netty.handler.WebSocketHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; 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.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.handler.timeout.IdleStateHandler; import io.netty.handler.traffic.ChannelTrafficShapingHandler; /** * @author xiaobo * @date 2023/9/5 */public class NettyWebsocketServer { private final int port; public NettyWebsocketServer(int port) { this.port = port; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 创建用于接受客户端连接的 boss 线程池 EventLoopGroup workerGroup = new NioEventLoopGroup(); // 创建用于处理客户端请求的 worker 线程池 try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelTrafficShapingHandler trafficShapingHandler = new ChannelTrafficShapingHandler( 1, // 读取速率限制(字节/秒) 1, // 写入速率限制(字节/秒) 1, // 流量检查时间间隔(毫秒) 1 // 最大允许的时间窗口(毫秒) ); ChannelPipeline pipeline = ch.pipeline(); // 添加心跳检测处理器,3秒内没有读写事件将触发 IdleStateEvent,下面的顺序错了也会出现问题的 pipeline.addLast(new IdleStateHandler(30, 0, 0)); pipeline.addLast(new HeartbeatHandler()); pipeline.addLast(new HttpServerCodec()); // 处理 HTTP 请求 pipeline.addLast(new ChunkedWriteHandler()); // 写大数据流的处理器 pipeline.addLast(new HttpObjectAggregator(8192)); // 将 HTTP 消息聚合为 FullHttpRequest 或 FullHttpResponse // pipeline.addLast(new WebSocketFrameAggregator(8192)); // 将 HTTP 消息聚合为 FullHttpRequest 或 FullHttpResponse // pipeline.addLast(new WebSocketServerCompressionHandler()); // 消息压缩 pipeline.addLast(new WebSocketHandler()); // 自定义 WebSocket 处理器 pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10)); // 处理 WebSocket 升级握手和数据帧处理 } }) .option(ChannelOption.SO_BACKLOG, 128) // 设置服务器接受队列大小 .childOption(ChannelOption.SO_KEEPALIVE, true); // 开启 TCP 连接的 Keep-Alive 功能 // Bind and start to accept incoming connections. System.out.println("TCP server started successfully"); ChannelFuture f = b.bind(port).sync(); // 绑定端口并等待绑定完成 // Wait until the server socket is closed. // In this example, this does not happen, but you can do that to gracefully // shut down your server. f.channel().closeFuture().sync(); // 阻塞直到服务器关闭 } finally { // 优雅地关闭线程池 workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
这里需要注意一下,
pipeline.addLast
的顺序不一致可能会导致程序报错,运行时
-
创建心跳 handle
package com.todoitbo.baseSpringbootDasmart.netty.handler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleState; import io.netty.handler.timeout.IdleStateEvent; public class HeartbeatHandler extends ChannelInboundHandlerAdapter { int readTimeOut = 0; @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { IdleStateEvent event = (IdleStateEvent) evt; if(event.state() == IdleState.READER_IDLE){ readTimeOut++; } if(readTimeOut >= 3){ System.out.println("超时超过3次,断开连接"); ctx.close(); } } }
-
创建WebSocketHandler
package com.todoitbo.baseSpringbootDasmart.netty.handler; import cn.hutool.core.collection.CollectionUtil; import com.todoitbo.baseSpringbootDasmart.netty.NamedChannelGroup; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.websocketx.*; import io.netty.util.AttributeKey; import io.netty.util.CharsetUtil; import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author xiaobo */@Slf4j public class WebSocketHandler extends SimpleChannelInboundHandler<Object> { private WebSocketServerHandshaker handshaker; public static final AttributeKey<String> USER_ID_KEY = AttributeKey.valueOf("userId"); public static final AttributeKey<String> GROUP_ID_KEY = AttributeKey.valueOf("groupId"); private static final Map<Channel, String> WORK_CHANNEL_MAP = new HashMap<Channel,String>(); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info("与客户端建立连接,通道开启!"); // 添加到channelGroup通道组(广播) // 之后可以根据ip来进行分组 NamedChannelGroup.getChannelGroup("default").add(ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { log.info("与客户端断开连接,通道关闭!"); // 从channelGroup通道组(广播)中删除 // 之后可以根据ip来进行分组 Channel channel = ctx.channel(); NamedChannelGroup.getChannelGroup("default").remove(channel); WORK_CHANNEL_MAP.remove(channel); } public boolean userAuthentication(ChannelHandlerContext ctx,FullHttpRequest req) { // 提取URI参数 QueryStringDecoder queryStringDecoder = new QueryStringDecoder(req.uri()); Map<String, List<String>> parameters = queryStringDecoder.parameters(); // 根据参数进行处理 List<String> userId = parameters.get("userId"); List<String> groupId = parameters.get("groupId"); if (CollectionUtil.isNotEmpty(userId) && CollectionUtil.isNotEmpty(groupId)) { ctx.channel().attr(USER_ID_KEY).set(userId.get(0)); ctx.channel().attr(GROUP_ID_KEY).set(groupId.get(0)); return true; }else { return false; } } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { // 检查是否升级到WebSocket if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) { // 如果不是WebSocket协议的握手请求,返回400 Bad Request响应 sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } // 构建握手响应 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( getWebSocketLocation(req), null, true); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { // 如果不支持WebSocket版本,返回HTTP 426 Upgrade Required响应 WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req); // 进行WebSocket握手 // 在认证成功后,设置用户ID到Channel属性中 boolean authentication = userAuthentication(ctx,req);// 这里需要实现用户认证逻辑 if (!authentication) { // 用户认证失败,可能需要关闭连接或发送认证失败消息 // 1. 关闭连接: ctx.close(); // 2. 发送认证失败消息给客户端: String failureMessage = "认证失败,请提供有效的身份验证信息。"; ctx.writeAndFlush(failureMessage); return; } // 其他逻辑... WORK_CHANNEL_MAP.put(ctx.channel(), ctx.channel().attr(GROUP_ID_KEY).get()); } } private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // 发送HTTP响应 if (res.status().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); HttpUtil.setContentLength(res, res.content().readableBytes()); } ChannelFuture future = ctx.channel().writeAndFlush(res); if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) { future.addListener(ChannelFutureListener.CLOSE); } } private String getWebSocketLocation(FullHttpRequest req) { return "ws://" + req.headers().get(HttpHeaderNames.HOST) + req.uri(); } private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) { // 处理WebSocket消息,可以根据实际需求进行处理 if (frame instanceof TextWebSocketFrame) { // 处理文本消息 String text = ((TextWebSocketFrame) frame).text(); System.out.println("Received message: " + text); // 可以在这里处理WebSocket消息并发送响应 // ... } else if (frame instanceof BinaryWebSocketFrame) { // 处理二进制WebSocket消息 // ... System.out.println("123"); } else if (frame instanceof CloseWebSocketFrame) { // 处理WebSocket关闭请求 handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); } else if (frame instanceof PingWebSocketFrame) { // 处理WebSocket Ping消息 System.out.println("cs"); ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); } } @Override protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { // 处理HTTP握手请求 handleHttpRequest(ctx, (FullHttpRequest) msg); } else if (msg instanceof WebSocketFrame) { // 处理WebSocket消息 handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 发生异常时的处理 log.error(cause.getMessage()); ctx.close(); } }
-
创建NamedChannelGroup
package com.todoitbo.baseSpringbootDasmart.netty;import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.util.concurrent.GlobalEventExecutor;import java.util.Map; import java.util.concurrent.ConcurrentHashMap;public class NamedChannelGroup{private String groupName;public static Map<String,ChannelGroup> channelGroupMap = new ConcurrentHashMap<>();static {channelGroupMap.put("default", new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));}public static void setGroupName(String groupName){channelGroupMap.put(groupName, new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));}public static ChannelGroup getChannelGroup(String groupName){return channelGroupMap.get(groupName);} }
Server-Sent Events (SSE)实现
创建DataManager
package com.todoitbo.baseSpringbootDasmart.sse;import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 数据管理器用于管理Server-Sent Events (SSE) 的订阅和数据推送。* @author xiaobo*/
@Component
public class DataManager {private final Map<String, List<SseEmitter>> dataEmitters = new HashMap<>();/*** 订阅特定数据类型的SSE连接。** @param dataType 要订阅的数据类型* @param emitter SSE连接*/public void subscribe(String dataType, SseEmitter emitter) {dataEmitters.computeIfAbsent(dataType, k -> new ArrayList<>()).add(emitter);emitter.onCompletion(() -> removeEmitter(dataType, emitter));emitter.onTimeout(() -> removeEmitter(dataType, emitter));}/*** 推送特定数据类型的数据给所有已订阅的连接。** @param dataType 要推送的数据类型* @param data 要推送的数据*/public void pushData(String dataType, String data) {List<SseEmitter> emitters = dataEmitters.getOrDefault(dataType, new ArrayList<>());emitters.forEach(emitter -> {try {emitter.send(SseEmitter.event().data(data, MediaType.TEXT_PLAIN));} catch (IOException e) {removeEmitter(dataType, emitter);}});}private void removeEmitter(String dataType, SseEmitter emitter) {List<SseEmitter> emitters = dataEmitters.get(dataType);if (emitters != null) {emitters.remove(emitter);}}
}
接口实现
package com.todoitbo.baseSpringbootDasmart.controller; import com.todoitbo.baseSpringbootDasmart.sse.DataManager;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.annotation.Resource; /** * @author xiaobo */
@RestController
@RequestMapping("/environment")
public class EnvironmentController { @Resource private DataManager dataManager; @GetMapping(value = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter subscribe() { SseEmitter emitter = new SseEmitter(); dataManager.subscribe("environment", emitter); return emitter; } // 示例:推送环境监测数据给前端 @GetMapping("/push/{testText}") public ResponseEntity<String> pushEnvironmentData(@PathVariable String testText) { dataManager.pushData("environment", testText); return ResponseEntity.ok("Data pushed successfully."); }
}
实现说明
每个不同类型的数据推送都需要一个对应的SSE订阅端点(subscribe)。每个数据类型都有一个对应的订阅端点,用于前端建立SSE连接,并在后端接收和处理特定类型的数据推送。
在你的后端应用中,对于每种数据类型,你需要创建一个对应的Controller或处理器来处理该数据类型的SSE订阅。每个Controller会有自己的SSE订阅端点,前端可以连接到不同的端点以接收相应类型的数据。
这种方式允许你将不同类型的数据推送逻辑分离,使代码更具可维护性和可扩展性。当有新的数据可用时,只需调用相应类型的数据推送方法,而不必修改通用的SSE管理逻辑。
前端实现
<!DOCTYPE html>
<html>
<head><title>SSE Data Receiver</title>
</head>
<body><h1>Real-time Data Display</h1><div id="data-container"></div><script>const dataContainer = document.getElementById('data-container');// 创建一个 EventSource 对象,指定 SSE 服务器端点的 URLconst eventSource = new EventSource('http://127.0.0.1:13024/environment/subscribe'); // 根据你的控制器端点来设置URL// 添加事件处理程序,监听服务器端发送的事件eventSource.onmessage = (event) => {const data = event.data;// 在这里处理从服务器接收到的数据// 可以将数据显示在页面上或进行其他操作const newDataElement = document.createElement('p');newDataElement.textContent = data;dataContainer.appendChild(newDataElement);};eventSource.onerror = (error) => {// 处理连接错误console.error('Error occurred:', error);};</script>
</body>
</html>
弊端以及解决方案
如果你没什么处理的话,在它首次调用subscribe时候可能会出现连接超时的问题,因为这个是一个长连接,出现这种问题是因为,此时并没有数据产生,至此,除非你刷新页面,否则即使有数据产生前端也不会受到了
你希望前端在第一次订阅SSE连接后,即使后端没有数据产生,之后也能接收到数据。这可以通过以下方式来实现:
-
保持持久连接: 确保前端建立的SSE连接是持久性连接,不会在第一次连接成功后关闭。这可以让连接一直保持打开状态,即使后端没有即时数据产生。你可以在前端代码中使用以下方式来确保连接持久:
const eventSource = new EventSource('/environment/subscribe');
默认情况下,EventSource对象会自动重连,以保持连接的持久性。
-
定期发送心跳数据: 在后端定期发送一些心跳数据,以确保连接保持活跃。这可以防止连接超时关闭。你可以在后端定期发送一个包含无用信息的SSE事件,例如:
@Scheduled(fixedRate = 30000) // 每30秒发送一次心跳数据 public void sendHeartbeat() {dataManager.pushData("heartbeat", "Heartbeat data"); }
前端可以忽略这些心跳事件,但它们会保持连接处于活跃状态。
-
前端自动重连: 在前端代码中添加自动重连逻辑,以处理连接断开的情况。这样,如果连接由于某种原因断开,前端会自动尝试重新建立连接。示例:
const eventSource = new EventSource('/environment/subscribe');eventSource.onerror = (error) => {// 处理连接错误console.error('Error occurred:', error);// 重新建立连接eventSource.close();setTimeout(() => {// 重新建立连接eventSource = new EventSource('/environment/subscribe');}, 1000); // 1秒后重试 };
通过结合上述方法,你可以确保前端能够建立并保持持久SSE连接,即使后端没有即时数据产生。这样,一旦后端有数据产生,前端也可以接收到数据而无需重新订阅。
相关文章:
WebSocket vs SSE: 实时数据推送到前端的选择与实现(详细)
Websocket和Server-Sent Events 对比推送数据给前端及各自的实现 二者对比WebSocket:Server-Sent Events (SSE):选择 WebSocket 还是 SSE: Websocket 实现使用原生 WebSocket API:使用 Netty 创建 WebSocket:总结和选择…...

Redis从入门到精通(二:数据类型)
数据存储类型介绍 Redis 数据类型(5种常用) string hash list set sorted_set/zset(应用性较低) redis 数据存储格式 redis 自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储 数据类型指的是存储的数据…...

基于SSM的珠宝首饰交易平台
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…...

4款视频号数据分析平台!
很多人在做视频号的时候就会有创作参考的需求,那么你们知道视频号中有哪些数据平台?今天就和大家来分享一下 接下来就总结一下视频号数据平台有哪些?排名不分前后。 1:视频号助手(channels.weixin.qq.com)…...

【系统架构】什么是集群?为什么要使用集群架构?
什么是集群?为什么要使用集群架构? 1.什么是集群?2.为什么要使用集群?2.1 高性能2.2 价格有效性2.3 可伸缩性2.4 高可用性2.5 透明性2.6 可管理性2.7 可编程性 3.集群的常见分类3.1 负载均衡集群3.2 高可用性集群3.3 高性能计算集…...
Java手写拓扑排序和拓扑排序应用拓展案例
Java手写拓扑排序和拓扑排序应用拓展案例 1. 算法思维导图 #mermaid-svg-o8KpEXzxukfDM8c9 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-o8KpEXzxukfDM8c9 .error-icon{fill:#552222;}#mermaid-svg-o8KpEXzxukfD…...

练习:使用servlet显示试卷页面
试卷页面代码 在浏览器输入如下地址: http://localhost/examPageServlet 效果如下:...

视频监控系统/视频云存储EasyCVR接入国标GB28181设备无法播放设备录像,是什么原因?
安防视频监控平台EasyCVR支持将部署在监控现场的前端设备进行统一集中接入,可兼容多协议、多类型设备,管理员可选择任意一路或多路视频实时观看,视频画面支持单画面、多画面显示,视频窗口数量有1、4、9、16个可选,还能…...

四叶草clover配置工具:Clover Configurator for Mac
Clover Configurator是一款Mac上的工具,用于配置和优化Clover引导加载器。Clover引导加载器是一种用于启动macOS的开源引导加载器。它允许用户在启动时选择操作系统和配置启动选项。 Clover Configurator提供了一个可视化的界面,让用户可以轻松地编辑和…...

计算机网络第四章——网络层(中)
提示:待到山花烂漫时,她在丛中笑。 文章目录 需要加头加尾,其中头部最重要的就是加了IP地址和MAC地址(也就是逻辑地址和物理地址)集线器物理层设备,交换机是物理链路层的设备,如上图路由器左边就…...

时序分解 | MATLAB实现基于小波分解信号分解分量可视化
时序分解 | MATLAB实现基于小波分解信号分解分量可视化 目录 时序分解 | MATLAB实现基于小波分解信号分解分量可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 基于小波分解的分量可视化,MATLAB编程程序,用于将信号分解成不同尺度和频率的子信…...

VMware虚拟化环境搭建
虚拟化环境搭建 1. 什么是虚拟化环境?未来工作中在何处使用? 在网络安全中,虚拟化环境是一种技术,它将一个物理计算机系统划分成多个独立、可管理的虚拟环境。这种虚拟环境技术允许多个完全不同的操作系统、显示装置和软件在同一…...

Jenkins :添加node权限获取凭据、执行命令
拥有Jenkins agent权限的账号可以对node节点进行操作,通过添加不同的node可以让流水线项目在不同的节点上运行,安装Jenkins的主机默认作为master节点。 1.Jenkins 添加node获取明文凭据 通过添加node节点,本地监听ssh认证,选则凭…...

如何实现不同MongoDB实例间的数据复制?
作为一种Schema Free文档数据库,MongoDB因其灵活的数据模型,支撑业务快速迭代研发,广受开发者欢迎并被广泛使用。在企业使用MongoDB承载应用的过程中,会因为业务上云/跨云/下云/跨机房迁移/跨地域迁移、或数据库版本升级、数据库整…...

微服务保护-隔离
个人名片: 博主:酒徒ᝰ. 个人简介:沉醉在酒中,借着一股酒劲,去拼搏一个未来。 本篇励志:三人行,必有我师焉。 本项目基于B站黑马程序员Java《SpringCloud微服务技术栈》,SpringCloud…...
报错:appium AttributeError: ‘NoneType‘ object has no attribute ‘to_capabilities‘
报错如下 Traceback (most recent call last):File "C:\Users\wlb\Desktop\test\python\2.py", line 16, in <module>driver webdriver.Remote("http://127.0.0.1:4723/wd/hub", caps)File "D:\software\python3\lib\site-packages\appium\we…...

MFC - 一文带你从小白到项目应用(全套1)
文章篇幅可能会比较长,从入门到基本能上项目的全部内容。建议观看的过程中,用电脑跟着学习案例。 持续输出优质文章是作者的追求,因为热爱,所以热爱。 最近看动漫被一句鸡汤感动到了,也送给各位朋友: 只要有…...

(2596. 检查骑士巡视方案leetcode,经典深搜)-------------------Java实现
(2596. 检查骑士巡视方案leetcode,经典深搜)-------------------Java实现 题目表述 骑士在一张 n x n 的棋盘上巡视。在 有效 的巡视方案中,骑士会从棋盘的 左上角 出发,并且访问棋盘上的每个格子 恰好一次 。 给你一个 n x n …...

Docker 部署 Bitwarden RS 服务
Bitwarden RS 服务是官方 Bitwarden server API 的 Rust 重构版。因为 Bitwarden RS 必须要通过 https 才能访问, 所以在开始下面的步骤之前, 建议先参考 《Ubuntu Nginx 配置 SSL 证书》 配置好域名和 https 访问。 部署 Bitwarden RS 拉取最新版本的 docker.io/vaultwarden…...
python与mongodb交互-->pymongo
from pymongo import MongoClient# 创建数据库连接对象 client=MongoClient(ip,27017)# 选择一个数据库 db=client[admin]db.authenticate(python,python)# 选择一个集合 col=client[pydata][test]col.insert({"class":"python"})col.find() for data in c…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命
在华东塑料包装行业面临限塑令深度调整的背景下,江苏艾立泰以一场跨国资源接力的创新实践,重新定义了绿色供应链的边界。 跨国回收网络:废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点,将海外废弃包装箱通过标准…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...

基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...

招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...

GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...

毫米波雷达基础理论(3D+4D)
3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文: 一文入门汽车毫米波雷达基本原理 :https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...