当前位置: 首页 > news >正文

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:

  1. 双向通信

    • WebSocket 允许双向通信,客户端和服务器都可以在任何时候向对方发送数据。
    • 这使得 WebSocket 非常适用于需要双向交互的应用,如在线聊天、多人协作工具等。
  2. 持久连接

    • WebSocket 建立持久连接,客户端和服务器之间的连接保持打开状态。
    • 这减少了与建立和关闭连接相关的开销,适用于频繁的数据交换。
  3. 低延迟

    • 由于持久连接,WebSocket 可以实现低延迟的实时数据传输,适用于需要快速响应的应用。
  4. 复杂性

    • 实现 WebSocket 可能相对复杂,需要更多的服务器资源和额外的协议处理。
  5. 跨域通信

    • WebSocket 通常需要配置服务器以允许跨域通信,因为它们使用自定义协议。
  6. 浏览器支持

    • WebSocket 在现代浏览器中得到广泛支持。

Server-Sent Events (SSE):

  1. 单向通信

    • SSE 是一种单向通信,只允许服务器向客户端发送数据。客户端无法向服务器发送数据。
  2. HTTP 协议

    • SSE 建立在 HTTP 协议之上,使用标准 HTTP 请求和响应。
    • 这使得 SSE 更容易部署,因为它与现有的 HTTP 基础设施兼容。
  3. 简单性

    • SSE 的实现相对简单,服务器和客户端都不需要太多复杂的逻辑。
  4. 无需专用库

    • SSE 不需要额外的库或协议处理,客户端可以使用浏览器的原生 EventSource API 来接收数据。
  5. 跨域通信

    • SSE 支持跨域通信,可以通过 CORS(跨域资源共享)机制进行配置。
  6. 浏览器支持

    • SSE 在现代浏览器中也得到广泛支持,但与 WebSocket 相比,它的历史要长一些。

选择 WebSocket 还是 SSE:

  • WebSocket 适用于需要双向通信和低延迟的场景,例如在线游戏、实时聊天应用等。

  • SSE 适用于单向服务器到客户端的实时数据推送,例如新闻更新、实时股票报价、天气预报等,特别是当你希望使用现有的 HTTP 基础设施时。

  • 在某些情况下,你甚至可以同时使用 WebSocket 和 SSE,根据不同的需求选择合适的技术。

无论选择哪种技术,都需要考虑你的应用程序的具体需求和复杂性。WebSocket 提供了更多的灵活性和功能,而 SSE 更加简单和易于部署。最终的选择取决于你的项目目标和资源。

Websocket 实现

使用原生 WebSocket API:

  1. 简单性

    • Spring Boot 提供了对原生 WebSocket API 的支持,使得创建 WebSocket 应用相对简单。
    • 开发人员可以直接使用 Java 标准库中的 WebSocket 相关类来处理 WebSocket 通信。
  2. 依赖

    • 原生 WebSocket 不需要额外的依赖,因为 WebSocket API 已经包含在 Java 标准库中。
  3. 性能

    • 原生 WebSocket API 在性能方面表现良好,适用于大多数中小型应用。
  4. 生态系统

    • 使用原生 WebSocket 可以更容易地集成到现有的 Spring Boot 生态系统中,例如 Spring Security 等。
  5. 简单应用

    • 当你需要创建相对简单的 WebSocket 应用时,原生 WebSocket 是一个不错的选择。

使用 Netty 创建 WebSocket:

  1. 灵活性

    • Netty 是一个高度可定制的异步事件驱动框架,它可以用于创建各种网络应用,包括 WebSocket。
    • Netty 提供了更多的灵活性和自定义选项,适用于复杂的 WebSocket 应用。
  2. 性能

    • Netty 以其高性能和低延迟而闻名,适用于需要处理大量并发连接的应用。
  3. 协议支持

    • Netty 支持多种协议,不仅限于 WebSocket。这意味着你可以在同一个应用程序中处理多种网络通信需求。
  4. 集成

    • 尽管 Netty 可以集成到 Spring Boot 中,但其集成可能需要更多的配置和代码。
  5. 复杂应用

    • 当你需要处理复杂的 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连接后,即使后端没有数据产生,之后也能接收到数据。这可以通过以下方式来实现:

  1. 保持持久连接: 确保前端建立的SSE连接是持久性连接,不会在第一次连接成功后关闭。这可以让连接一直保持打开状态,即使后端没有即时数据产生。你可以在前端代码中使用以下方式来确保连接持久:

    const eventSource = new EventSource('/environment/subscribe');
    

    默认情况下,EventSource对象会自动重连,以保持连接的持久性。

  2. 定期发送心跳数据: 在后端定期发送一些心跳数据,以确保连接保持活跃。这可以防止连接超时关闭。你可以在后端定期发送一个包含无用信息的SSE事件,例如:

    @Scheduled(fixedRate = 30000) // 每30秒发送一次心跳数据
    public void sendHeartbeat() {dataManager.pushData("heartbeat", "Heartbeat data");
    }
    

    前端可以忽略这些心跳事件,但它们会保持连接处于活跃状态。

  3. 前端自动重连: 在前端代码中添加自动重连逻辑,以处理连接断开的情况。这样,如果连接由于某种原因断开,前端会自动尝试重新建立连接。示例:

    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&#xff1a;Server-Sent Events (SSE)&#xff1a;选择 WebSocket 还是 SSE&#xff1a; Websocket 实现使用原生 WebSocket API&#xff1a;使用 Netty 创建 WebSocket&#xff1a;总结和选择…...

Redis从入门到精通(二:数据类型)

数据存储类型介绍 Redis 数据类型&#xff08;5种常用&#xff09; string hash list set sorted_set/zset&#xff08;应用性较低&#xff09; redis 数据存储格式 redis 自身是一个 Map&#xff0c;其中所有的数据都是采用 key : value 的形式存储 数据类型指的是存储的数据…...

基于SSM的珠宝首饰交易平台

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

4款视频号数据分析平台!

很多人在做视频号的时候就会有创作参考的需求&#xff0c;那么你们知道视频号中有哪些数据平台&#xff1f;今天就和大家来分享一下 接下来就总结一下视频号数据平台有哪些&#xff1f;排名不分前后。 1&#xff1a;视频号助手&#xff08;channels.weixin.qq.com&#xff09…...

【系统架构】什么是集群?为什么要使用集群架构?

什么是集群&#xff1f;为什么要使用集群架构&#xff1f; 1.什么是集群&#xff1f;2.为什么要使用集群&#xff1f;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显示试卷页面

试卷页面代码 在浏览器输入如下地址&#xff1a; http://localhost/examPageServlet 效果如下&#xff1a;...

视频监控系统/视频云存储EasyCVR接入国标GB28181设备无法播放设备录像,是什么原因?

安防视频监控平台EasyCVR支持将部署在监控现场的前端设备进行统一集中接入&#xff0c;可兼容多协议、多类型设备&#xff0c;管理员可选择任意一路或多路视频实时观看&#xff0c;视频画面支持单画面、多画面显示&#xff0c;视频窗口数量有1、4、9、16个可选&#xff0c;还能…...

四叶草clover配置工具:Clover Configurator for Mac

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

计算机网络第四章——网络层(中)

提示&#xff1a;待到山花烂漫时&#xff0c;她在丛中笑。 文章目录 需要加头加尾&#xff0c;其中头部最重要的就是加了IP地址和MAC地址&#xff08;也就是逻辑地址和物理地址&#xff09;集线器物理层设备&#xff0c;交换机是物理链路层的设备&#xff0c;如上图路由器左边就…...

时序分解 | MATLAB实现基于小波分解信号分解分量可视化

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

VMware虚拟化环境搭建

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

Jenkins :添加node权限获取凭据、执行命令

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

如何实现不同MongoDB实例间的数据复制?

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

微服务保护-隔离

个人名片&#xff1a; 博主&#xff1a;酒徒ᝰ. 个人简介&#xff1a;沉醉在酒中&#xff0c;借着一股酒劲&#xff0c;去拼搏一个未来。 本篇励志&#xff1a;三人行&#xff0c;必有我师焉。 本项目基于B站黑马程序员Java《SpringCloud微服务技术栈》&#xff0c;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)

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

(2596. 检查骑士巡视方案leetcode,经典深搜)-------------------Java实现

&#xff08;2596. 检查骑士巡视方案leetcode,经典深搜&#xff09;-------------------Java实现 题目表述 骑士在一张 n x n 的棋盘上巡视。在 有效 的巡视方案中&#xff0c;骑士会从棋盘的 左上角 出发&#xff0c;并且访问棋盘上的每个格子 恰好一次 。 给你一个 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…...

【网络】计算机网络基础

Linux网络 对网络的理解 在网络传输中存在的问题&#xff1a; 找到我们所需要传输的主机解决远距离数据传输丢失的问题怎么进行数据转发&#xff0c;路径选择的问题 有问题&#xff0c;就有解决方案&#xff1b; 我们把相同性质的问题放在一起&#xff0c;做出解决方案 解…...

(1)输入输出函数:cin和cout(2)数学函数:sqrt、pow、sin、cos、tan等

输入输出函数&#xff1a;cin 和 cout 在C编程语言中&#xff0c;为了与用户进行交互和显示程序的结果&#xff0c;我们使用了两个非常重要的函数&#xff1a;cin 和 cout。这两个函数分别用于输入和输出。 cin是C中的标准输入流对象&#xff0c;它用于从键盘接收用户的输入。…...

ArmSom-W3开发板之PCIE的开发指南(一)

1. 简介 RK3588从入门到精通本⽂介绍RK平台配置pcie的方法开发板&#xff1a;ArmSoM-W3 2、PCIE接口概述 PCIe&#xff08;Peripheral Component Interconnect Express&#xff09;是一种用于连接计算机内部组件的高速接口标准。以下是关于PCIe接口的简要介绍&#xff1a; …...

Android 13.0 framework修改AlertDialog对话框的button样式

1.概述 在13.0系统产品开发中 在AlertDialog 系统对话框原生的确定和取消 两个button 按钮中,由于产品觉得字体默认颜色的不太好看,由于产品的需求修改button字体的颜色,所以需要找到AlertDialog的字体样式然后修改就可以了 2.framework修改AlertDialog 对话框的button样式…...

如何使用ArcGIS Pro提取河网水系

DEM数据除了可以看三维地图和生成等高线之外&#xff0c;还可以用于水文分析&#xff0c;这里给大家介绍一下如何使用ArcGIS Pro通过水文分析提取河网水系&#xff0c;希望能对你有所帮助。 数据来源 本教程所使用的数据是从水经微图中下载的DEM数据&#xff0c;除了DEM数据&a…...

python pytesseract 中文文字批量识别

用pytesseract 来批量把图片转成文字 1、安装好 pytesseract 包 2、下载安装OCR https://download.csdn.net/download/m0_37622302/88348824https://download.csdn.net/download/m0_37622302/88348824 Index of /tesseracthttps://digi.bib.uni-mannheim.de/tesseract/ 我是…...

Python 之plt.plot()的介绍以及使用

文章目录 介绍代码实例 介绍 plt.plot() 是Matplotlib库中用于绘制线图&#xff08;折线图&#xff09;的主要函数之一。它的作用是将一组数据点连接起来&#xff0c;以可视化数据的趋势、关系或模式。以下是 plt.plot() 的详细介绍&#xff1a; plt.plot(x, y, fmt, **kwarg…...

自动化生成代码:MyBatis 的 Generator与MyBatis-Plus 的 AutoGenerator

文章目录 Mybatis Generator自动化生成代码MyBatis Generator概述使用Java代码形式1. 在 Maven 或 Gradle 中添加 MyBatis Generator 的依赖&#xff1a;2. 编写配置文件 GeneratorConfig.xml&#xff0c;配置需要生成的数据库表和对应的生成器&#xff1a;3. 在命令行中使用 M…...

达梦数据库-DW-国产化--九五小庞

武汉达梦数据库股份有限公司成立于2000年&#xff0c;是国内领先的数据库产品开发服务商&#xff0c;国内数据库基础软件产业发展的关键推动者。公司为客户提供各类数据库软件及集群软件、云计算与大数据等一系列数据库产品及相关技术服务&#xff0c;致力于成为国际顶尖的全栈…...

LeetCode 753. 破解保险箱【欧拉回路,DFS】困难

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…...

江门建设造价信息网站/枣庄网站建设制作

ldd命令用于判断某个可执行的 binary 档案含有什么动态函式库。 参数说明&#xff1a; --version 打印ldd的版本号 -v --verbose 打印所有信息&#xff0c;例如包括符号的版本信息 -d --data-relocs 执行符号重部署&#xff0c;并报告缺少的目标对象(只对ELF格式适用) -r --fun…...

做网站 人工智能/网络培训机构

一、准备工作 &#xff08;只做一次准备工作&#xff0c;以后都会很方便&#xff09; 1. 安装pip &#xff08;1&#xff09;下载pip到D:\download pip下载地址&#xff1a;https://pypi.python.org/pypi/pip#downloads &#xff08;2&#xff09;下载后解压到当前目录&#xf…...

自己会网站开发如何赚钱/重庆网站seo推广公司

理解 Java 的 GC 与 幽灵引用 Java 中一共有 4 种类型的引用 : StrongReference、 SoftReference、 WeakReference 以及 PhantomReference (传说中的幽灵引用 呵呵), 这 4 种类型的引用与 GC 有着密切的关系, 让我们逐一来看它们的定义和使用场景 : 1. St…...

网站做记录访客/百度手机应用市场

最近在做MIS管理系统中&#xff0c;对于数据列表展示前面要加上一个序号&#xff0c;全选等功能(本篇文章只解决在DataGrid前加序号问题)&#xff1b;从网上也看到有 朋友对这方面的功能做了一些讲解&#xff0c;其功能都是一样&#xff0c;也没有什么好说&#xff1b;关键在于…...

企业网站的推广方法/站内优化主要从哪些方面进行

如下图所示&#xff0c;我在任何时候按F1键&#xff0c;都会自动弹出Windows帮助和支持&#xff0c;事实上这个功能很鸡肋&#xff0c;从来没用过&#xff0c;但是玩魔兽的时候确实必须的&#xff0c;F1控制英雄的&#xff0c;呵呵。 方法还是有的&#xff0c;在任务管理器中找…...

做网站图片素材在线编辑/网络推广网站排名

操作系统实 验 报 告课程名称操作系统实验实验项目名称磁盘调度算法学号班级姓名专业计算机科学与技术学生所在学院计算机科学与技术学院指导教师初妍实验室名称地点21#428哈尔滨工程大学计算机科学与技术学院第六讲 磁盘调度算法一、实验概述1. 实验名称磁盘调度算法2. 实验目…...