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

java每日精进 3.12 【WebSocket进阶】

基于 SpringWebSocket 进行二次封装,实现了更加简单的使用方式。例如说,WebSocket 的认证、Session 的管理、WebSocket 集群的消息广播等等。

1. 用户认证与登录用户信息传递

1.1 Token 过滤器 (TokenAuthenticationFilter)

① 在 WebSocket 连接建立时,通过 QueryString 的 token 参数,进行认证。例如说:ws://127.0.0.1:48080/ws?token=xxx

/*** Token 过滤器,验证 token 的有效性* 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文*/
@RequiredArgsConstructor
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {private final SecurityProperties securityProperties;private final GlobalExceptionHandler globalExceptionHandler;private final OAuth2TokenApi oauth2TokenApi;@Override@SuppressWarnings("NullableProblems")protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {// 情况一,基于 header[login-user] 获得用户,例如说来自 Gateway 或者其它服务透传LoginUser loginUser = buildLoginUserByHeader(request);// 情况二,基于 Token 获得用户// 注意,这里主要满足直接使用 Nginx 直接转发到 Spring Cloud 服务的场景。if (loginUser == null) {String token = SecurityFrameworkUtils.obtainAuthorization(request,securityProperties.getTokenHeader(), securityProperties.getTokenParameter());if (StrUtil.isNotEmpty(token)) {Integer userType = WebFrameworkUtils.getLoginUserType(request);try {// 1.1 基于 token 构建登录用户loginUser = buildLoginUserByToken(token, userType);// 1.2 模拟 Login 功能,方便日常开发调试if (loginUser == null) {loginUser = mockLoginUser(request, token, userType);}} catch (Throwable ex) {CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);ServletUtils.writeJSON(response, result);return;}}}// 设置当前用户if (loginUser != null) {SecurityFrameworkUtils.setLoginUser(loginUser, request);}// 继续过滤链chain.doFilter(request, response);}private LoginUser buildLoginUserByToken(String token, Integer userType) {try {// 校验访问令牌OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token).getCheckedData();if (accessToken == null) {return null;}// 用户类型不匹配,无权限// 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型// 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的if (userType != null&& ObjectUtil.notEqual(accessToken.getUserType(), userType)) {throw new AccessDeniedException("错误的用户类型");}// 构建登录用户return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()).setInfo(accessToken.getUserInfo()) // 额外的用户信息.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()).setExpiresTime(accessToken.getExpiresTime());} catch (ServiceException serviceException) {// 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可return null;}}/*** 模拟登录用户,方便日常开发调试* @param request 请求* @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号* @param userType 用户类型* @return 模拟的 LoginUser*/private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {if (!securityProperties.getMockEnable()) {return null;}// 必须以 mockSecret 开头if (!token.startsWith(securityProperties.getMockSecret())) {return null;}// 构建模拟用户Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));return new LoginUser().setId(userId).setUserType(userType).setTenantId(WebFrameworkUtils.getTenantId(request));}@SneakyThrowsprivate LoginUser buildLoginUserByHeader(HttpServletRequest request) {String loginUserStr = request.getHeader(SecurityFrameworkUtils.LOGIN_USER_HEADER);if (StrUtil.isEmpty(loginUserStr)) {return null;}try {loginUserStr = URLDecoder.decode(loginUserStr, StandardCharsets.UTF_8.name()); // 解码,解决中文乱码问题return JsonUtils.parseObject(loginUserStr, LoginUser.class);} catch (Exception ex) {log.error("[buildLoginUserByHeader][解析 LoginUser({}) 发生异常]", loginUserStr, ex);  ;throw ex;}}}
  • 作用:在 WebSocket 握手之前,验证用户的 Token 有效性,并将登录用户信息(LoginUser)设置到 Spring Security 上下文中。

  • 流程

    1. 从请求头或请求参数中获取 Token。

    2. 调用 OAuth2TokenApi 验证 Token 的有效性。

    3. 如果 Token 有效,构建 LoginUser 对象,并将其设置到 Spring Security 上下文中。

    4. 如果 Token 无效,返回错误响应。

  • 关键点

    • 支持从请求头或请求参数中获取 Token。

    • 支持模拟登录用户(用于开发环境)。

    • 支持用户类型(userType)的校验。

1.2 WebSocket 握手拦截器 (LoginUserHandshakeInterceptor)

/*** 登录用户的 {@link HandshakeInterceptor} 实现类** 流程如下:* 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过* 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中*/
public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Map<String, Object> attributes) {LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();if (loginUser != null) {WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);}return true;}@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Exception exception) {// do nothing}}
  • 作用:在 WebSocket 握手时,将 Spring Security 上下文中的 LoginUser 信息传递到 WebSocket 会话中。

  • 流程

    1. 在握手之前,从 Spring Security 上下文中获取 LoginUser

    2. 将 LoginUser 设置到 WebSocket 会话的 attributes 中。

  • 关键点

    • 通过 WebSocketFrameworkUtils.setLoginUser 将用户信息存储到会话中。

2. WebSocket 会话管理

每个前端和后端建立的 WebSocket 连接,对应后端的一个 WebSocketSession 会话对象。由于后续需要对 WebSocketSession 进行消息的发送,所以需要进行管理。

2.1 WebSocket 会话管理器 (WebSocketSessionManager)

/*** 默认的 {@link WebSocketSessionManager} 实现类*/
public class WebSocketSessionManagerImpl implements WebSocketSessionManager {/*** id 与 WebSocketSession 映射** key:Session 编号*/private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();/*** user 与 WebSocketSession 映射** key1:用户类型* key2:用户编号*/private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions= new ConcurrentHashMap<>();@Overridepublic void addSession(WebSocketSession session) {// 添加到 idSessions 中idSessions.put(session.getId(), session);// 添加到 userSessions 中LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);if (user == null) {return;}ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());if (userSessionsMap == null) {userSessionsMap = new ConcurrentHashMap<>();if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {userSessionsMap = userSessions.get(user.getUserType());}}CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());if (sessions == null) {sessions = new CopyOnWriteArrayList<>();if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {sessions = userSessionsMap.get(user.getId());}}sessions.add(session);}@Overridepublic void removeSession(WebSocketSession session) {// 移除从 idSessions 中idSessions.remove(session.getId());// 移除从 idSessions 中LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);if (user == null) {return;}ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());if (userSessionsMap == null) {return;}CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());sessions.removeIf(session0 -> session0.getId().equals(session.getId()));if (CollUtil.isEmpty(sessions)) {userSessionsMap.remove(user.getId(), sessions);}}@Overridepublic WebSocketSession getSession(String id) {return idSessions.get(id);}@Overridepublic Collection<WebSocketSession> getSessionList(Integer userType) {ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);if (CollUtil.isEmpty(userSessionsMap)) {return new ArrayList<>();}LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容Long contextTenantId = TenantContextHolder.getTenantId();for (List<WebSocketSession> sessions : userSessionsMap.values()) {if (CollUtil.isEmpty(sessions)) {continue;}// 特殊:如果租户不匹配,则直接排除if (contextTenantId != null) {Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));if (!contextTenantId.equals(userTenantId)) {continue;}}result.addAll(sessions);}return result;}@Overridepublic Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);if (CollUtil.isEmpty(userSessionsMap)) {return new ArrayList<>();}CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();}}
  • 存储 WebSocket 会话

    • 维护一个 idSessions 集合,用于通过 Session ID 存储和查找 WebSocketSession
    • 维护一个 userSessions 集合,用于根据 用户类型用户 ID 存储 WebSocketSession,支持多端会话管理。
  • 添加会话 (addSession)

    • session 存入 idSessions,以 Session ID 作为键。
    • 获取 session 关联的 LoginUser 信息(用户类型 & 用户 ID)。
    • userSessions 中,以 用户类型用户 ID 作为键,存储 WebSocketSession
  • 移除会话 (removeSession)

    • idSessions 移除 session
    • userSessions 移除 session
      • 先找到该 session 所属的用户类型 & 用户 ID 。
      • 在对应的 CopyOnWriteArrayList<WebSocketSession> 中删除该 session
      • 如果该用户的所有 session 都被移除,则删除该用户的映射。
  • 获取单个会话 (getSession)

    • 通过 idSessions.get(id) 直接获取 WebSocket 连接。
  • 获取某用户类型的所有会话 (getSessionList(Integer userType))

    • 获取 userSessions 中属于 userType 的所有 session
    • 如果存在租户 ID(多租户逻辑),则排除不同租户的 session
  • 获取某个用户的所有会话 (getSessionList(Integer userType, Long userId))

    • 获取特定用户 ID 关联的 session 集合。
  • 作用:管理所有 WebSocket 会话,支持按用户类型、用户编号等条件查询会话。

  • 实现类WebSocketSessionManagerImpl

  • 数据结构

    • idSessions:存储所有会话,键为会话 ID,值为 WebSocketSession

    • userSessions:按用户类型和用户编号存储会话,键为用户类型和用户编号,值为会话列表。

  • 功能

    • 添加会话:将新会话添加到 idSessions 和 userSessions 中。

    • 移除会话:从 idSessions 和 userSessions 中移除会话。

    • 查询会话:支持按会话 ID、用户类型、用户编号查询会话。

2.2 WebSocket 会话装饰器 (WebSocketSessionHandlerDecorator)

/*** {@link WebSocketHandler} 的装饰类,实现了以下功能:** 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理* 2. 封装 {@link WebSocketSession} 支持并发操作*/
public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {/*** 发送时间的限制,单位:毫秒*/private static final Integer SEND_TIME_LIMIT = 1000 * 5;/*** 发送消息缓冲上线,单位:bytes*/private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;private final WebSocketSessionManager sessionManager;public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,WebSocketSessionManager sessionManager) {super(delegate);this.sessionManager = sessionManager;}@Overridepublic void afterConnectionEstablished(WebSocketSession session) {// 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);// 添加到 WebSocketSessionManager 中sessionManager.addSession(session);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {sessionManager.removeSession(session);}}

  • 管理 WebSocket 连接

    • WebSocketSession 建立连接 时,使用 sessionManager.addSession(session) 进行管理。
    • WebSocketSession 关闭连接 时,使用 sessionManager.removeSession(session) 进行清理。
  • 增强 WebSocketSession,支持并发操作

    • 使用 ConcurrentWebSocketSessionDecorator 包装 session,确保在高并发环境下 避免阻塞或数据丢失
    • 限制消息发送时间(最大 5 秒)。
    • 限制消息缓冲区大小(最大 100 KB)。
  • 作用:在 WebSocket 连接建立和关闭时,调用 WebSocketSessionManager 管理会话。

  • 流程

    1. 在连接建立时,将 WebSocketSession 包装为 ConcurrentWebSocketSessionDecorator,支持并发操作。

    2. 将包装后的会话添加到 WebSocketSessionManager 中。

    3. 在连接关闭时,从 WebSocketSessionManager 中移除会话。

  • 关键点

    • 使用 ConcurrentWebSocketSessionDecorator 解决 WebSocket 并发问题。

3. WebSocket 消息处理

3.1 JSON 消息格式 (JsonWebSocketMessage)

/*** JSON 格式的 WebSocket 消息帧*/
@Data
public class JsonWebSocketMessage implements Serializable {/*** 消息类型** 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类*/private String type;/*** 消息内容** 要求 JSON 对象*/private String content;}
  • 作用:定义 WebSocket 消息的 JSON 格式。

  • 字段

    • type:消息类型,用于分发到对应的消息监听器。

    • content:消息内容,为 JSON 字符串。

3.2 JSON 消息处理器 (JsonWebSocketMessageHandler)

这个类 继承 TextWebSocketHandler,用于处理 WebSocket 文本消息(JSON 格式),并且 基于消息类型 type,将消息分发给对应的 WebSocketMessageListener 进行处理

/*** JSON 格式 {@link WebSocketHandler} 实现类** 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。*/
@Slf4j
public class JsonWebSocketMessageHandler extends TextWebSocketHandler {/*** type 与 WebSocketMessageListener 的映射*/private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();@SuppressWarnings({"rawtypes", "unchecked"})public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {listenersList.forEach((Consumer<WebSocketMessageListener>)listener -> listeners.put(listener.getType(), listener));}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 1.1 空消息,跳过if (message.getPayloadLength() == 0) {return;}// 1.2 ping 心跳消息,直接返回 pong 消息。if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {session.sendMessage(new TextMessage("pong"));return;}// 2.1 解析消息try {JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);if (jsonMessage == null) {log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());return;}if (StrUtil.isEmpty(jsonMessage.getType())) {log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());return;}// 2.2 获得对应的 WebSocketMessageListenerWebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());if (messageListener == null) {log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());return;}// 2.3 处理消息Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);Long tenantId = WebSocketFrameworkUtils.getTenantId(session);TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));} catch (Throwable ex) {log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());}}}
  • 处理客户端发送的 JSON 格式消息,并根据消息类型分发到对应的消息监听器。

  • 流程

    1. 解析客户端发送的消息为 JsonWebSocketMessage

    2. 根据 type 字段查找对应的消息监听器(WebSocketMessageListener)。

    3. 将消息内容反序列化为监听器所需的类型,并调用监听器的 onMessage 方法。

  • 关键点

    • 支持心跳消息(ping -> pong)。

    • 支持多租户(通过 TenantUtils.execute 设置当前租户上下文)。

4. WebSocket 全流程

4.1 连接建立流程

  1. 客户端发起 WebSocket 连接请求,携带 Token。

  2. TokenAuthenticationFilter 验证 Token,并将 LoginUser 设置到 Spring Security 上下文中。

  3. LoginUserHandshakeInterceptor 在握手时,将 LoginUser 设置到 WebSocket 会话的 attributes 中。

  4. WebSocketSessionHandlerDecorator 在连接建立时,将会话添加到 WebSocketSessionManager 中。

4.2 消息处理流程

  1. 客户端发送 JSON 格式的消息。

  2. JsonWebSocketMessageHandler 解析消息,并根据 type 字段查找对应的消息监听器。

  3. 消息监听器处理消息,并执行相应的业务逻辑。

4.3 连接关闭流程

  1. 客户端断开 WebSocket 连接。

  2. WebSocketSessionHandlerDecorator 在连接关闭时,从 WebSocketSessionManager 中移除会话。


5. 关键设计

  • 用户认证:通过 TokenAuthenticationFilter 和 LoginUserHandshakeInterceptor 实现用户信息的传递和验证。

  • 会话管理:通过 WebSocketSessionManager 统一管理所有 WebSocket 会话,支持按用户类型和用户编号查询。

  • 消息分发:通过 JsonWebSocketMessageHandler 实现消息的解析和分发,支持多租户和并发处理。

  • 扩展性:通过 WebSocketMessageListener 接口,支持灵活扩展消息处理逻辑。


6. 总结

上述WebSocket 应用,包括用户认证、会话管理、消息处理等功能。通过分层设计和模块化实现,代码具有良好的扩展性和可维护性。适用于需要 WebSocket 支持的实时通信场景,如在线聊天、实时通知等。

相关文章:

java每日精进 3.12 【WebSocket进阶】

基于 SpringWebSocket 进行二次封装&#xff0c;实现了更加简单的使用方式。例如说&#xff0c;WebSocket 的认证、Session 的管理、WebSocket 集群的消息广播等等。 1. 用户认证与登录用户信息传递 1.1 Token 过滤器 (TokenAuthenticationFilter) ① 在 WebSocket 连接建立…...

国家网络安全事件应急预案

目 录 1 总则 1.1 编制目的 1.2 编制依据 1.3 适用范围 1.4 事件分级 1.5 工作原则 2 组织机构与职责 2.1 领导机构与职责 2.2 办事机构与职责 2.3 各部门职责 2.4 各省&#xff08;区、市&#xff09;职责 3 监测与预警 3.1 预警分级 3.2 预警监测 3.3 预警研判…...

Markdown:Mermaid 画图

目录 安装基本语法流程图时序图甘特图总结 Mermaid 是一款用于生成流程图、时序图、甘特图等图表的 JavaScript 库。它可以将简单的文本描述转化为美观的图表&#xff0c;方便开发者进行可视化展示。 安装 Mermaid 可以直接在浏览器中使用&#xff0c;也可以在 Node.js 环境中…...

【视频】ffmpeg、Nginx搭建RTMP、HLS服务器

1、源码安装Nginx 1)源码下载 因为要使用Nginx的模块nginx-rtmp-module,所以要下载 nginx 和 nginx-rtmp-module 的源码。 下载地址: http://nginx.org/en/download.html https://github.com/arut/nginx-rtmp-module/tags2)解压、配置 在同一个目录中解压 nginx 和 nginx…...

时间有限,如何精确设计测试用例?5种关键方法

精确设计测试用例能够迅速识别并修复主要缺陷&#xff0c;确保产品质量&#xff0c;降低后期维护成本&#xff0c;并通过专注于核心功能来提升用户体验&#xff0c;为项目的成功奠定坚实基础。若未能精确设计测试用例&#xff0c;可能会导致关键功能测试不充分&#xff0c;使得…...

【算法】图论

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;Linux 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 持续更新中...1、DFS2、BFSN 叉树的层序遍历二叉树的锯齿形层序遍历二叉树最大宽度 3、多源BFS腐烂的苹果 4、拓扑排序 持续更新中…...

ADQ32 5G采集卡

ADQ32是一款高端12位双通道数据采集板&#xff0c;针对高通量科学应用进行了优化。ADQ32具有以下特性: 一个和两个模拟输入通道包括每通道5和2.5 GSPS7GB/s的持续数据传输速率至GPU7GB/秒的持续数据传输速率两个外部触发器通用输入/输出&#xff08;GPIO&#xff09;开放式FPG…...

机器人领域专业名词汇总

1. 电机与驱动 电机类型 DC Motor&#xff08;直流电机&#xff09;&#xff1a;通过直流电源驱动的电机。Stepper Motor&#xff08;步进电机&#xff09;&#xff1a;通过脉冲信号控制旋转角度的电机。Servo Motor&#xff08;伺服电机&#xff09;&#xff1a;带有反馈控制的…...

拆解 “ES 已死“ 伪命题:Agentic RAG 时代搜索引擎的终极形态

作者&#xff1a;来自 Elastic 李捷 xxx&#xff1a;“ES已死&#xff0c;#%#……” 我&#xff1a;&#xff1f;&#xff1f;&#xff1f; 最近&#xff0c;某厂商发了一堆公关文章&#xff0c;翻来覆去地炒作 “ES 已死”&#xff0c;“放弃 ES”。这哪是什么正经的技术文章&…...

eNSP中路由器的CON/AUX接口、GE Combo接口、Mini USB接口、USB接口、WAN侧uplink接口、FE接口、GE接口介绍

路由器常见接口的详细介绍及其应用示例&#xff1a; 1. CON/AUX 接口 全称&#xff1a;Console/Auxiliary&#xff08;控制台/辅助接口&#xff09;作用&#xff1a; CON&#xff08;Console&#xff09;&#xff1a;通过命令行界面&#xff08;CLI&#xff09;直接配置路由器…...

平面的四种方程及一些应用

平面的四种方程及一些应用 点法式方程一般式方程三点式方程截距式方程一些应用已知平面方程&#xff0c;找出平面上不共线的三个点 点法式方程 平面经过点 ( x 0 , y 0 , z 0 ) (x_0,y_0,z_0) (x0​,y0​,z0​)且法向量为 ( a , b , c ) (a,b,c) (a,b,c)&#xff0c;则平面的点…...

记录一个SQL自动执行的html页面

在实际工作场景中&#xff0c;需要运用到大量SQL语句更新业务逻辑&#xff0c;对程序员本身&#xff0c;写好的sql语句执行没有多大问题&#xff08;图1&#xff09;&#xff0c;但是对于普通用户来说还是有操作难度的。因此我们需要构建一个HTML页面&#xff08;图2&#xff0…...

SpringBoot——Maven篇

Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的工具。它具有许多特性&#xff0c;其中一些重要的特性包括&#xff1a; 1. 自动配置&#xff1a;Spring Boot 提供了自动配置的机制&#xff0c;可以根据应用程序的依赖和环境自动配置应用程序的各种组件&#xff…...

数据批处理(队列方式)

数据批处理&#xff08;队列方式&#xff09; public class DataProcessor {private static final int THREAD_COUNT 4;private static final int QUEUE_SIZE 10;private LinkedBlockingQueue<Data> queue new LinkedBlockingQueue<>(QUEUE_SIZE);public DataP…...

从零开始搭建搜索推荐系统(五十四)多路召回之万剑归宗

聊的不止技术。跟着小帅写代码&#xff0c;还原和技术大牛一对一真实对话&#xff0c;剖析真实项目筑成的一砖一瓦&#xff0c;了解最新最及时的资讯信息&#xff0c;还可以学到日常撩妹小技巧哦&#xff0c;让我们开始探索主人公小帅的职场生涯吧&#xff01; &#xff08;PS…...

c++介绍函数指针 十

指针代表内存中地址标识符&#xff0c;变量&#xff0c;数组都是存储内存中的数据。所以可以获得它们的地址&#xff0c;用指针来表示这块内存。 如图输出内存中的地址。 对于一个函数来说&#xff0c;也是内存中存储这段数据&#xff0c;所以我们也可以获取函数的地址。 函数…...

redis数据库

一、redis数据库介绍 NoSQL Not Only SQL 非关系型数据库 1、关系型数据库与非关系型数据库的区别 非关系型数据库性能高、速度快、支持高并发连接 1、非关系型数据库基于内存存储数据 2、摒弃了关系型数据的约束限制 3、采用o1算法进行设计开发 2、作用 关系型数…...

关于 NoC 中数据安全传输的设计与实现的详细介绍

片上网络&#xff08;Network-on-Chip&#xff0c;NoC&#xff09;作为一种新兴的片上通信架构&#xff0c;解决了传统总线架构在大规模集成电路设计中面临的诸多问题。然而&#xff0c;随着芯片系统的复杂性和应用场景的多样化&#xff0c;NoC 中数据安全传输变得至关重要。以…...

OpenGL(4)着色器

文章目录 一、着色器1、什么是着色器&#xff1f;2、着色器类型2.1、顶点着色器&#xff08;Vertex Shader&#xff09;2.2、片段着色器&#xff08;Fragment Shader&#xff09; 3、着色器属性3.1、layout 属性3.2、in 属性3.3、out 属性3.4、总结 4、示例 前言&#xff1a; 在…...

PHP批量去除Bom头的方法

检查的代码&#xff1a; <?php$dir __DIR__; $files new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));foreach ($files as $file) {if ($file->isFile() && pathinfo($file, PATHINFO_EXTENSION) php) {$content file_get_contents(…...

51单片机的keil c51软件安装教程

Keil&#xff08;C51&#xff09;介绍、下载、安装与注册_keil c51-CSDN博客 参考 安装 不一定是这个大小&#xff0c;也可以下载别的版本KEID C51 注册 加入芯片型号 …...

JavaScript基本知识

文章目录 一、JavaScript基础1.变量&#xff08;重点&#xff09;1-1 定义变量及赋值1-2 变量的命名规则和命名规范判断数据类型&#xff1a; 2.数据类型转换2-1 其他数据类型转成数值2-2 其他数据类型转成字符串2-3 其他数据类型转成布尔 3.函数3-1函数定义阶段3-2函数调用阶段…...

导数,积分及常用公式

导数定义&#xff1a; ​ 求导是数学计算中的一个计算方法&#xff0c;它的定义就是&#xff0c;当自变量的增量趋于零时&#xff0c;因变量的增量与自变量的增量之商的极限。在一个函数存在导数时&#xff0c;称这个函数可导或者可微分。可导的函数一定连续。不连续的函数一定…...

鸿蒙应用开发—ZDbUtil高效使用数据库

文章目录 介绍下载安装基本使用注解TableIdColumnOneToOne 使用方法定义实体类初始化数据库并根据被Table注解的类创建表创建表查数据插入数据删除数据清空数据 参考 介绍 ZDbUtil是一款基于SQLite的鸿蒙数据库框架&#xff0c;通过注解标注实体类与属性&#xff0c;让数据更能…...

强化学习(赵世钰版)-学习笔记(7.时序差分学习)

本章是课程算法与方法中的第四章&#xff0c;介绍的时序差分学习算法是基于随机近似方法设计的强化学习方法&#xff0c;也是model-free的方法。 时序差分算法是一种近似估计策略状态值的算法&#xff0c;具体的形式如下&#xff1a; 本质上是在当前t时刻&#xff0c;被访问到的…...

正则表达式入门及常用的正则表达式

正则表达式&#xff08;Regular Expression&#xff0c;简称 Regex&#xff09;是一种强大的文本处理工具&#xff0c;用于匹配、查找和替换字符串中的特定模式。以下是入门指南和常用正则表达式示例&#xff1a; 一、正则表达式入门 1. 基本语法 符号说明示例.匹配任意单个字…...

大白话如何在 Vue 项目中进行路由懒加载?

大白话如何在 Vue 项目中进行路由懒加载&#xff1f; 在 Vue 项目里&#xff0c;路由懒加载是种很实用的技术&#xff0c;它能让你在需要的时候再去加载对应的路由组件&#xff0c;而不是在项目启动时就把所有组件都加载进来&#xff0c;这样能加快项目的启动速度。下面就详细…...

手动实现一个RTTI系统

在 C 中&#xff0c;RTTI&#xff08;Runtime Type Information&#xff0c;运行时类型信息&#xff09;是一组允许程序在运行时获取对象类型信息的机制 。虽然C通过虚接口的方式提供了良好的抽象&#xff0c;但是对于一个复杂的系统&#xff0c;过于依赖抽象而忽略业务的复杂性…...

智能化水利监管:无人机视频在违章行为识别中的应用

随着我国经济社会的快速发展&#xff0c;水利工程建设规模不断扩大&#xff0c;但随之而来的违章建设行为也日益增多。传统的人工巡查方式效率低下&#xff0c;难以满足当前监管需求。无人机技术以其灵活性和高效性&#xff0c;为水利工程建设监管提供了新的解决方案。本文将探…...

力扣练习之确定两个字符串是否接近

目录 题目&#xff1a; 题解&#xff1a; 详细题解 题目&#xff1a; 如果可以使用以下操作从一个字符串得到另一个字符串&#xff0c;则认为两个字符串 接近 &#xff1a; 操作 1&#xff1a;交换任意两个 现有 字符。 例如&#xff0c;abcde -> aecdb 操作 2&#xff1…...

Word 小黑第21套

对应大猫22 设置表格为页面的80%&#xff1a;表布局 -属性 -表格 指定宽度80% 度量单位改成百分比 段落组 -中文版式 在表格上下方留一行空段&#xff08;如果表格太大改一下样式&#xff09;插入横线 边框线 &#xff08;右击横线 -图片 修改样式&#xff09; 段落 -取消对于…...

mingw32编译ffmpeg

ffmpeg https://gitee.com/mirrors/ffmpeg.git 使用msys2的mingw32 pacman -S mingw-w64-x86_64-toolchain compile ./confiure --enable-static --disable-shared --enable-gpl --target-oswin32 mingw32-make -j4 提示编译错误&#xff0c;msys2里面的路径是/d/tools/msys2…...

设计模式C++

针对一些经典的常见的场景, 给定了一些对应的解决方案&#xff0c;这个就叫设计模式。 设计模式的作用&#xff1a;使代码的可重用性高&#xff0c;可读性强&#xff0c;灵活性好&#xff0c;可维护性强。 设计原则&#xff1a; 单一职责原则&#xff1a;一个类只做一方面的…...

使用 Excel 实现绩效看板的自动化

引言 在日常工作中&#xff0c;团队的绩效监控和管理是确保项目顺利进行的重要环节。然而&#xff0c;面临着以下问题&#xff1a; ​数据分散&#xff1a;系统中的数据难以汇总&#xff0c;缺乏一个宏观的团队执行情况视图。​看板缺失&#xff1a;系统本身可能无法提供合适…...

ngx_openssl_conf_t

ngx_openssl_conf_t 定义在 src\event\ngx_event_openssl.c typedef struct {ngx_uint_t engine; /* unsigned engine:1; */ } ngx_openssl_conf_t; 1. 这个结构体的目的是存储与 OpenSSL 引擎相关的配置信息。 2. engine 字段用于标识是否启用 OpenSSL 的硬件加速引擎…...

深度学习环境配置指令大全

文章目录 环境配置官网/博客合集清华镜像站anaconda官网pytorch官网pytorch历史库官网pytorch与cuda对应版本下载博客torch与torchvision与python对应关系python与pytorch对应关系 环境相关创建环境激活环境退出环境删除环境检查环境冲突 安装相关安装requirementsconda安装con…...

Netty启动源码NioEventLoop剖析accept剖析read剖析write剖析

学习链接 NIO&Netty - 专栏 Netty核心技术十–Netty 核心源码剖析Netty核心技术九–TCP 粘包和拆包及解决方案Netty核心技术七–Google ProtobufNetty核心技术六–Netty核心模块组件Netty核心技术五–Netty高性能架构设计 聊聊Netty那些事儿 - 专栏 一文搞懂Netty发送数…...

<03.13>八股文补充知识

import java.lang.reflect.*; public class Main {public static void main(String[] args) throws Exception {// 获取 Class 对象//1. 通过类字面量Class<?> clazz Person.class;//2 通过对象实例化String str "Hello";Class<?> clazz_str str.ge…...

[HUBUCTF 2022 新生赛]messy_traffic

下载附件 看到文件类型直接用wireshark打开&#xff0c;对MySQL协议进行追踪流&#xff0c;并没有什么发现&#xff0c;后面对NO.437发现有用信息&#xff0c;http追踪流 发现**system(‘cat passwd.txt’);**这里是在打开查看passwd.txt&#xff0c;密码是"SignUpForHUBU…...

条款1:理解模版性别推导

目录 问题引出 情况1&#xff1a;ParamType是个指针或引用&#xff0c;但不是个万能引用。 情况2&#xff1a;ParamType是个万能引用 情况3&#xff1a;ParamType既非指针也非引用 问题引出 函数模板大致形如&#xff1a; template<typename T> void f(ParamType p…...

kafka连问

1&#xff0c;kafka多消费者指部署多个服务消费节点吗 2&#xff0c;多个消费节点自动组成消费组吗 3&#xff0c;消费者组与多消费节点关系 4&#xff0c;一个分区&#xff0c;多个消费者&#xff0c;可以保证有序消费吗 5&#xff0c;kafka如何实现顺序消费&#xff0c;一…...

Linux中基础开发工具详细介绍

目录 软件包管理器什么是软件包Linux软件生态 yum具体操作查看软件包安装软件卸载软件注意事项 编辑器VimLinux编辑器-vim使用vim的基本概念快速编辑的指令 编译器gcc/g背景知识gcc编译选项预处理(进行宏替换)编译&#xff08;生成汇编&#xff09;汇编&#xff08;生成机器可识…...

浅谈时钟启动和Systemlnit函数

时钟是STM32的关键&#xff0c;是整个系统的心脏&#xff0c;时钟如何启动&#xff0c;时钟源如何选择&#xff0c;各个参数如何设置&#xff0c;我们从源码来简单分析一下时钟的启动函数Systemlnit&#xff08;&#xff09;。 Systemlnit函数简介 我们先来看一下源程序的注释…...

社交软件频繁更新,UI 设计在其中扮演什么角色?

在当今数字化时代&#xff0c;社交软件已成为人们日常生活中不可或缺的一部分。随着科技的飞速发展和用户需求的不断变化&#xff0c;社交软件更新频率日益加快。在这频繁更新的背后&#xff0c;UI 设计扮演着至关重要的角色&#xff0c;它如同社交软件的 “门面担当” 与 “交…...

SQLMesh 系列教程:解锁SQLMesh的宏与变量魔法

在数据库流水线开发中&#xff0c;代码复用与动态配置是提升效率的核心诉求。SQLMesh以其独特的宏系统与用户定义变量机制&#xff0c;重新定义了SQL生成的灵活性。与传统模板引擎不同&#xff0c;SQLMesh的宏并非简单的字符串替换&#xff0c;而是基于语义理解的智能代码重构—…...

React篇之three渲染

需求&#xff1a;拖拽右侧面板&#xff0c;里面的three模型能够自适应 import { useEffect, useState, useRef } from react import ./App.css import * as THREE from three; import { GLTFLoader } from three/addons/loaders/GLTFLoader.js; import { debounce } from loda…...

PHP与前端框架的无缝集成:最佳实践与案例分析

PHP与前端框架的无缝集成&#xff1a;最佳实践与案例分析 在现代Web开发中&#xff0c;PHP作为后端语言与前端框架的集成已成为一种常见的开发模式。无论是传统的MVC架构&#xff0c;还是现代的SPA&#xff08;单页应用&#xff09;&#xff0c;PHP与前端框架的无缝集成能够显…...

Redis内存淘汰策略

Redis 是一种高性能的键值存储系统&#xff0c;广泛用于缓存、消息队列等场景。由于 Redis 数据存储在内存中&#xff0c;而内存资源有限&#xff0c;因此需要内存淘汰策略来管理内存的使用。Redis 提供了多种内存淘汰策略&#xff0c;可以根据不同的应用场景选择合适的策略。 …...

Facebook 的框架及技术栈

一、前端框架与技术 React.js 及其生态系统 核心原理与特点 React.js 是 Facebook 开源的用于构建用户界面的 JavaScript 库。它的核心概念是组件化&#xff0c;将用户界面拆分成一个个独立的、可复用的组件。每个组件都有自己的状态&#xff08;state&#xff09;和属性&#…...

QT中的布局管理

在 Qt 中&#xff0c;布局管理器&#xff08;如 QHBoxLayout 和 QVBoxLayout&#xff09;的构造函数可以接受一个 QWidget* 参数&#xff0c;用于指定该布局的父控件。如果指定了父控件&#xff0c;布局会自动将其管理的控件添加到父控件中。 在你的代码中&#xff0c;QHBoxLa…...