由浅入深逐步理解spring boot中如何实现websocket

实现websocket的方式

1.springboot中有两种方式实现websocket,一种是基于原生的基于注解的websocket,另一种是基于spring封装后的WebSocketHandler

基于原生注解实现websocket

1)先引入websocket的starter坐标

 	   <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>

2)编写websocket的Endpoint端点类

@ServerEndpoint(value = "/ws/{token}")
@Component
public class WebsocketHandler2 {private final static Logger log = LoggerFactory.getLogger(WebsocketHandler2.class);private static final Set<Session> SESSIONS = new ConcurrentSkipListSet<>(Comparator.comparing(Session::getId));private static final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(10);private static final Map<String, ScheduledFuture<?>> futures = new ConcurrentHashMap<>();@OnOpenpublic void onOpen(Session session, @PathParam("token") String token, EndpointConfig config) throws IOException {
//        session.addMessageHandler(new PongHandler());ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> sendPing(session), 5, 5, TimeUnit.SECONDS);String queryString = session.getQueryString();futures.put(session.getId(), future);session.setMaxIdleTimeout(6 * 1000);SESSIONS.add(session);log.info("open connect sessionId={}, token={}, queryParam={}", session.getId(), token, queryString);String s = String.format("ws client(id=%s) has connected", session.getId());session.getBasicRemote().sendText(s);}static class PongHandler implements MessageHandler.Whole<PongMessage> {@Overridepublic void onMessage(PongMessage message) {ByteBuffer data = message.getApplicationData();String s = new String(data.array(), StandardCharsets.UTF_8);log.info("receive pong msg=> {}", s);}}@OnClosepublic void onClose(Session session, CloseReason reason) {log.info("session(id={}) close ,closeCode={},closeParse={}", session.getId(), reason.getCloseCode(), reason.getReasonPhrase());SESSIONS.remove(session);ScheduledFuture<?> future = futures.get(session.getId());if (future != null) {future.cancel(true);}}@OnMessagepublic void onMessage(String message, Session session) throws IOException {log.info("receive client(id={}) msg=>{}", session.getId(), message);String s = String.format("reply your(id=%s) msg=>【%s】", session.getId(), message);session.getBasicRemote().sendText(s);}@OnMessagepublic void onPong(PongMessage message, Session session) throws IOException {ByteBuffer data = message.getApplicationData();String s = new String(data.array(), StandardCharsets.UTF_8);log.info("receive client(id={}) pong msg=> {}", session.getId(), s);}@OnErrorpublic void onError(Session session, Throwable error) {log.error("Session(id={}) error occur ", session.getId(), error);}private void sendPing(Session session) {if (session.isOpen()) {String replyContent = String.format("Hello,client(id=%s)", session.getId());try {session.getBasicRemote().sendPing(ByteBuffer.wrap(replyContent.getBytes(StandardCharsets.UTF_8)));} catch (IOException e) {log.error("ping client(id={}) error", session.getId(), e);}return;}SESSIONS.remove(session);ScheduledFuture<?> future = futures.remove(session.getId());if (future != null) {future.cancel(true);}}
}

注解说明

@ServerEndpoint标记这个是一个服务端的端点类
@OnOpen 标记此方法是建立websocket连接时的回调方法
@OnMessage 标记此方法是接收到客户端消息时的回调方法
@OnClose标记此方法是断开websocke连接时的回调方法
@OnError标记此方法是websocke发生异常时的回调方法
@PathParam可以获取@ServerEndpoint注解中绑定的路径模板参数

方法参数说明

1) onOpen方法参数

onOpen的可用参数在tomcat源码 org.apache.tomcat.websocket.pojo.PojoMethodMapping#getOnOpenArgs可以看到

public Object[] getOnOpenArgs(Map<String,String> pathParameters,Session session, EndpointConfig config) throws DecodeException {return buildArgs(onOpenParams, pathParameters, session, config, null,null);}

因此可以看出@OnOpen所标记方法的合法参数有
(1)@PathParam标记的路径参数
(2)当前会话Session参数
(3)当前endpoint的配置详情EndpointConfig参数

2) onClose方法参数

onClose的可用参数在tomcat源码 org.apache.tomcat.websocket.pojo.PojoMethodMapping#getOnCloseArgs可以看到

    public Object[] getOnCloseArgs(Map<String,String> pathParameters,Session session, CloseReason closeReason) throws DecodeException {return buildArgs(onCloseParams, pathParameters, session, null, null,closeReason);}

因此可以看出@OnClose所标记方法的合法参数有
(1)@PathParam标记的路径参数
(2)当前会话Session参数
(3)当前连接关闭的原因CloseReason参数

3) onError方法参数

onError的可用参数在tomcat源码 org.apache.tomcat.websocket.pojo.PojoMethodMapping#getOnErrorArgs可以看到

    public Object[] getOnErrorArgs(Map<String,String> pathParameters,Session session, Throwable throwable) throws DecodeException {return buildArgs(onErrorParams, pathParameters, session, null,throwable, null);}

因此可以看出@OnError所标记方法的合法参数有
(1)@PathParam标记的路径参数
(2)当前会话Session参数
(3)发生异常的异常对象Throwable参数

4) onMessage方法参数

onMessage的可用参数在tomcat源码 org.apache.tomcat.websocket.pojo.PojoMethodMapping.MessageHandlerInfo#getMessageHandlers可以看到

        public Set<MessageHandler> getMessageHandlers(Object pojo,Map<String,String> pathParameters, Session session,EndpointConfig config) {Object[] params = new Object[m.getParameterTypes().length];for (Map.Entry<Integer,PojoPathParam> entry :indexPathParams.entrySet()) {PojoPathParam pathParam = entry.getValue();String valueString = pathParameters.get(pathParam.getName());Object value = null;try {value = Util.coerceToType(pathParam.getType(), valueString);} catch (Exception e) {DecodeException de =  new DecodeException(valueString,sm.getString("pojoMethodMapping.decodePathParamFail",valueString, pathParam.getType()), e);params = new Object[] { de };break;}params[entry.getKey().intValue()] = value;}Set<MessageHandler> results = new HashSet<>(2);if (indexBoolean == -1) {// Basicif (indexString != -1 || indexPrimitive != -1) {MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m,session, config, null, params, indexPayload, false,indexSession, maxMessageSize);results.add(mh);} else if (indexReader != -1) {MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m,session, config, null, params, indexReader, true,indexSession, maxMessageSize);results.add(mh);} else if (indexByteArray != -1) {MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,m, session, config, null, params, indexByteArray,true, indexSession, false, maxMessageSize);results.add(mh);} else if (indexByteBuffer != -1) {MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,m, session, config, null, params, indexByteBuffer,false, indexSession, false, maxMessageSize);results.add(mh);} else if (indexInputStream != -1) {MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo,m, session, config, null, params, indexInputStream,true, indexSession, true, maxMessageSize);results.add(mh);} else if (decoderMatch != null && decoderMatch.hasMatches()) {if (decoderMatch.getBinaryDecoders().size() > 0) {MessageHandler mh = new PojoMessageHandlerWholeBinary(pojo, m, session, config,decoderMatch.getBinaryDecoders(), params,indexPayload, true, indexSession, true,maxMessageSize);results.add(mh);}if (decoderMatch.getTextDecoders().size() > 0) {MessageHandler mh = new PojoMessageHandlerWholeText(pojo, m, session, config,decoderMatch.getTextDecoders(), params,indexPayload, true, indexSession, maxMessageSize);results.add(mh);}} else {MessageHandler mh = new PojoMessageHandlerWholePong(pojo, m,session, params, indexPong, false, indexSession);results.add(mh);}} else {// ASyncif (indexString != -1) {MessageHandler mh = new PojoMessageHandlerPartialText(pojo,m, session, params, indexString, false,indexBoolean, indexSession, maxMessageSize);results.add(mh);} else if (indexByteArray != -1) {MessageHandler mh = new PojoMessageHandlerPartialBinary(pojo, m, session, params, indexByteArray, true,indexBoolean, indexSession, maxMessageSize);results.add(mh);} else {MessageHandler mh = new PojoMessageHandlerPartialBinary(pojo, m, session, params, indexByteBuffer, false,indexBoolean, indexSession, maxMessageSize);results.add(mh);}}return results;}}

因此可以看出@OnMessage所标记方法的合法参数有
(1)@PathParam标记的路径参数
(2)当前会话Session参数
(3)当数据是分块传输时,表示当前消息时是否是最后一块数据的boolean Boolean参数
(4)字符输入流Reader参数
(5)二进制输入流InputStream参数
(6)原始的ByteBuffer参数
(7)字节数组byte[]参数
(8)字符串string参数
(9) Pong响应PongMessage参数
注意:接收数据报文的参数(4)~(5),只能使用其中的一个,否则可能导致IO异常(IO流只能读取一次)

ping和pong

上面的代码中我额外给websocket会话增加了一个PongMessage的处理方法onPong,它的作用是接收客户端的pong回执消息。只有在服务端向客户端发送Ping请求时,服务端才能接收到Pong响应。这里的ping和pong就是类型于其他系统中的心跳机制,用来检测客户端、服务端双方是否还在线,如果超过了限定时间没有收到pingpong消息,服务端就会主动断开连接。

因此我在建立websocke连接的时候给当前回话设置了最大空闲时间(超过这个时间没有数据报文传输,此连接就会自动断开),同时绑定了一个定时任务,这个定时任务会定时发送ping消息来保活。
这里的onPong方法不是必须的,没有它能保活,onPong只是用来得到一个ping结果的通知。

3)注册暴露端点

@Configuration
@EnableWebSocket
public class WebsocketConfig  {@Beanpublic ServerEndpointExporter serverEndpointExporter(){ServerEndpointExporter serverEndpointExporter = new ServerEndpointExporter();//WebsocketHandler2如果是一个spring bean(即有@Component),则不需要调用setAnnotatedEndpointClasses方法,spring会自动探测有@ServerEndpoint注解的bean//WebsocketHandler2如果只是一个包含@ServerEndpoint注解的普通类(不是 spring bean),则需要在此调用setAnnotatedEndpointClasses方法,手动注册Endpoint类型
//        serverEndpointExporter.setAnnotatedEndpointClasses(WebsocketHandler2.class );return serverEndpointExporter;}
}    

配置类添加@EnableWebSocket,启用spring websocket功能.
另外还需配置一个Bean ServerEndpointExporter ;如果Endpoint类是一个spring bean(即有@Component),则不需要调用setAnnotatedEndpointClasses方法,spring会自动探测含有@ServerEndpoint注解的Bean;如果Endpoint类只是一个包含@ServerEndpoint注解的普通类(不是 spring bean),则需要在此调用setAnnotatedEndpointClasses方法,手动注册Endpoint类型。

注意:即使Endpoint类是spring bean ,WebsocketContainer也会再创建并使用这个类的一个新实例,也就是说这个Endpoint中不能使用spring相关的功能,典型的就是不能使用@Autowire等注解自动注入Bean。其原因是websocket的默认端点配置org.apache.tomcat.websocket.server.DefaultServerEndpointConfigurator获取endpoint实例的逻辑是反射调用构造方法去创建一个新对象

public class DefaultServerEndpointConfiguratorextends ServerEndpointConfig.Configurator {@Overridepublic <T> T getEndpointInstance(Class<T> clazz)throws InstantiationException {try {return clazz.getConstructor().newInstance();} catch (InstantiationException e) {throw e;} catch (ReflectiveOperationException e) {InstantiationException ie = new InstantiationException();ie.initCause(e);throw ie;}}

当然你可以通过注入静态属性的方式来绕过这个限制。
在这里插入图片描述

理论上说也可在@ServerEndpoint注解的configurator属性指定为spring的org.springframework.web.socket.server.standard.SpringConfigurator也可以自动注入Bean依赖.

 @ServerEndpoint(value = "/echo", configurator = SpringConfigurator.class)public class EchoEndpoint {// ...}

SpringConfigurator它重写了获取Endpoint实例的方法逻辑getEndpointInstance,它是直接到spring容器中去取这个bean,而不是创建一个新实例.
在这里插入图片描述
但实际在spring boot项目中,上面的getEndpointInstance方法获取到的WebApplicationContextnull,也就没法从spring容器中获取这个Endpoint bean

基于spring WebSocketHandler实现websocket

了解WebSocketHandler

提前引入前面提到的websocket的starter 依赖
WebSocketHandler接口定义了5个方法,
afterConnectionEstablished:建立连接后的回调方法
handleMessage:接收到客户端消息后的回调方法
handleTransportError: 数据传输异常时的回调方法
afterConnectionClosed: 连接关闭后的回调方法
supportsPartialMessages: 是否支持数据分块传输(最后一个分块传输,isLast是true)

它有两个主要的子类, 一个是处理纯文本数据的TextWebSocketHandler ,另一个是处理二进制数据的BinaryWebSocketHandler
我们实现websocket一般是继承这两个类,并重写相应的方法。一般都需要重写afterConnectionEstablished handleTransportError
handleTransportError afterConnectionClosed 这三个方法,除此之外,处理文本还要重写接收客户端消息后的回调方法handleTextMessage,处理二进制数据需要重写接收客户端消息后的回调方法handleBinaryMessage。如果有需要得到ping结果回调,还可以重写handlePongMessage方法

代码


@Component
public class WebsocketHandler1 extends TextWebSocketHandler {private final Logger log = LoggerFactory.getLogger(getClass());private static final Set<WebSocketSession> sessions = new ConcurrentSkipListSet<>(Comparator.comparing(WebSocketSession::getId));private static final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(10);private static final Map<String, ScheduledFuture<?>> futures = new ConcurrentHashMap<>();@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {@SuppressWarnings("unchecked")AbstractWebSocketSession<Session> standardSession = (AbstractWebSocketSession) session;Session nativeSession = standardSession.getNativeSession();nativeSession.setMaxIdleTimeout(1000*4);ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> sendPing(session), 5, 5, TimeUnit.SECONDS);futures.put(session.getId(), future);log.info("open connect sessionId={}", session.getId());sessions.add(session);TextMessage msg = new TextMessage(String.format("ws client(id=%s) has connected", session.getId()));session.sendMessage(msg);}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {log.info("receive client(id={}) msg=>{}", session.getId(), message.getPayload());TextMessage msg = new TextMessage(String.format("reply your(id=%s) msg=>%s", session.getId(), message.getPayload()));session.sendMessage(msg);}@Overrideprotected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {ByteBuffer payload = message.getPayload();String s = new String(payload.array());log.info("receive client(id={}) pong msg=>{}", session.getId(),s);}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {log.error("client(id={}) error occur ", session.getId(), exception);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {log.info("close ,status={}", status);sessions.remove(session);ScheduledFuture<?> future = futures.get(session.getId());if (future != null) {future.cancel(true);}}@Overridepublic boolean supportsPartialMessages() {return true;}private void sendPing(WebSocketSession session) {if (session.isOpen()) {String replyContent = String.format("Hello,client(id=%s)", session.getId());PingMessage msg = new PingMessage(ByteBuffer.wrap(replyContent.getBytes(StandardCharsets.UTF_8)));try {session.sendMessage(msg);} catch (IOException e) {log.error("ping client(id={}) error", session.getId(), e);}}}
}

springboot内置的WebSocketHandler在这里插入图片描述

前端html代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>WebSocket Chat</title><style>body {font-family: Arial, sans-serif;}#chat-box {width: 100%;height: 300px;border: 1px solid #ccc;overflow-y: auto;padding: 10px;margin-bottom: 10px;background-color: #f9f9f9;white-space: pre-wrap;}#input-box {width: calc(50% - 90px);padding: 10px;margin-right: 10px;display: flex;justify-content: center}.btn {padding: 10px;}#btn-container {margin: 1px;display: flex;justify-content: center;gap: 5px;}#input-container {margin: 1px;display: flex;justify-content: center;gap: 5px;}</style>
</head>
<body><div id="chat-box"></div>
<div id="input-container"><input type="text" id="input-box" placeholder="Enter your message"/>
</div><div id="btn-container"><button id="connect-button" class="btn">Connect</button><button id="close-button" class="btn">Close</button><button id="clear-button" class="btn">Clear</button><button id="send-button" class="btn">Send</button>
</div><script>const chatBox = document.getElementById('chat-box');const inputBox = document.getElementById('input-box');const sendButton = document.getElementById('send-button');const connectBtn = document.getElementById('connect-button');const closeBtn = document.getElementById('close-button');const clearBtn = document.getElementById('clear-button');let ws = null;sendButton.addEventListener('click', () => {if (ws === null) {alert("no connect")return;}const message = inputBox.value;if (message) {ws.send(message);chatBox.innerHTML += 'You: ' + message + '\n';chatBox.scrollTop = chatBox.scrollHeight;inputBox.value = '';}});clearBtn.addEventListener('click', () => {chatBox.innerHTML = '';});closeBtn.addEventListener('click', () => {if (ws === null) {alert("no connect")return;}console.log("prepare close ws");ws.close(1000, 'Normal closure');});connectBtn.addEventListener('click', () => {if (ws !== null) {alert("already connected!")return;}let curWs = new WebSocket('ws://localhost:7001/ws/Hews2df?id=323&color=red');curWs.onopen = event => {ws = curWs;console.log('Connected to WebSocket server, event=>%s', JSON.stringify(event));};curWs.onmessage = event => {const message = event.data;chatBox.innerHTML += 'Server: ' + message + '\n';chatBox.scrollTop = chatBox.scrollHeight;};curWs.onclose = event => {ws = null;console.log('Disconnected from WebSocket server, close code=%s,close reason=%s', event.code, event.reason);};curWs.onerror = event => {console.log("error occur, event=>%s", JSON.stringify(event))};});</script></body>
</html>

演示效果
在这里插入图片描述

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

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

相关文章

电信诈骗升级到了 FaceTime

最近&#xff0c;网上有消息称一些不法分子正在通过FaceTime来冒充微信、京东等平台的客服&#xff0c;骗取用户转移账号内的资金&#xff0c;或是申请贷款。 虽然从具体的诈骗方式来说还是老一套&#xff0c;但是却更加防不胜防&#xff0c;而且欺诈性更强&#xff0c;特别是…

‌5G SSB(同步信号块)位于物理层‌

‌5G SSB&#xff08;同步信号块&#xff09;位于物理层‌。在5G NR中&#xff0c;SSB由主同步信号&#xff08;PSS&#xff09;、辅同步信号&#xff08;SSS&#xff09;和物理广播信道&#xff08;PBCH&#xff09;组成&#xff0c;这些信号共同构成了SSB。SSB的主要功能是帮…

基于MATLAB驾驶行为的疲劳实时检测研究

[摘要]为了有效地检测出驾驶员的驾驶疲劳&#xff0c;本文设计了一种基于驾驶行为的实时疲劳驾驶检测模型。该算法选取与疲劳相关的驾驶行为指标&#xff0c;包括速度均值与标准差&#xff0c;加速度绝对均值与标准差&#xff0c;综合考虑报警设备的报警频率与驾驶时长来划分疲…

演练纪实丨 同创永益圆满完成10月份灾备切换演练支持

2024年10月&#xff0c;同创永益共支持5家客户圆满完成灾备切换演练&#xff0c;共涉及70多套核心系统总切换与回切步骤6000余个&#xff0c;成功率100%&#xff0c;RTO时长均达到客户要求。 其中耗时最短的一次演练仅花费约3个小时&#xff0c;共涉及32套系统的灾备切换演练&a…

Logback 常用配置详解

1. 配置文件解析 Logback 是 Spring Boot 默认使用的日志框架&#xff0c;Logback 配置主要包含 8 大元素 1.1 configuration Logback 配置文件的根元素&#xff0c;它包含所有的配置信息 1.2 appender 定义一个 Appender&#xff0c;即日志输出的目的地&#xff0c;如控制…

【AI日记】24.11.01 LangChain、openai api和github copilot

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 工作1 内容&#xff1a;学习deeplearning.ai的免费课程地址&#xff1a;LangChain Chat with Your DataB站地址&#xff1a;https://www.bilibili.com/video/BV148411D7d2时间&#xff1a;2小时评估&am…

#渗透测试#SRC漏洞挖掘# 操作系统-Linux系统

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…

利用ExcelJS封装一个excel表格的导出

ExcelJS 操作和写入Excel 文件。 直接上代码&#xff0c;js部分&#xff1a; exportFn.js import ExcelJS from exceljs; import { saveAs } from file-saver;export function exportExcleUtils(tHeader, filterVal, listData, fileName) {//设置工作簿属性const workbook ne…

Android App 技能在DuerOS的调试方法

温故知新&#xff0c;我们先回顾一下DuerOS的技能分类。根据不同的视角可以对DuerOS 目前支持的技能类型进行不同的分类&#xff0c;例如&#xff0c;从用户与技能的语音交互方式来看&#xff0c; 可以将技能分为这四种技能类型: L1技能&#xff1a;只支持语音的打开和关闭L2技…

Unity 2D寻路导航 NavMeshPlus解决方案

插件的github主页 h8man/NavMeshPlus: Unity NavMesh 2D Pathfinding 这个插件是基于新版3D寻路导航制作的&#xff0c;所以你可能需要看一下这篇文章 新旧Navmash 寻路导航组件对比 附使用案例与实用教程链接-CSDN博客 这行代码agent.updateUpAxis false 一定要为代理单位…

客户端与微服务之间的桥梁---网关

当我们创建好了N多个微服务或者微服务的实例之后&#xff0c;每个服务暴露出不同的端口地址&#xff0c;一般对于客户端请求&#xff0c;只需要请求一个端口&#xff0c;要隔离客户端和微服务的直接关系&#xff0c;保证微服务的安全性和灵活性&#xff0c;避免敏感信息的泄露。…

@Excel若依导出异常/解决BusinessBaseEntity里面的字段不支持导出

今天发现所有实体类继承BusinessBaseEntity里面的这些通用字段不支持导出&#xff0c;debug时发现是这样&#xff1a; 导出效果 这里我把能查到的方法都汇总了&#xff0c;如果你也遇到这个异常&#xff0c;可以去逐步排查 1.先看库里有没有数据 2.看字段名是否对齐 3.所需要…

Flink系列之:学习理解通过状态快照实现容错

Flink系列之&#xff1a;学习理解通过状态快照实现容错 状态后端检查点存储状态快照状态快照如何工作&#xff1f;确保精确一次&#xff08;exactly once&#xff09;端到端精确一次 状态后端 由 Flink 管理的 keyed state 是一种分片的键/值存储&#xff0c;每个 keyed state…

大数据之文件服务器方案

大数据文件服务器方案 一&#xff0c;文件服务器常用框架 二&#xff0c;文件服务器常用框架的实现技术 文件服务器常用框架 文件服务器是一种专门用于存储、管理和共享文件的服务器&#xff0c;其常用框架的实现技术涉及多个方面&#xff0c;以下是一些主要的实现技术及其详…

车载总线系列 --- CAN FD简介

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所有人的看法和评价都是暂时的,只有自己的经历是伴随一生的,几乎所有的担忧和畏惧,都是来源于自己的想象,只有你真的去做了,才会发现有多快乐。…

Oracle与SQL Server的语法区别

1&#xff09;日期和日期转换函数。 SQL: SELECT A.*, CASE WHEN NVL(PAA009,) OR PAA009 >Convert(Varchar(10), SYSDATE,120) THEN Y ELSE N END AS ActiveUser FROM POWPAA A WHERE PAA001admin or PAA002admin Oracle: SELECT A.*, CASE WHEN NVL(PAA009,) or PAA009&…

在培训班学网络安全有用吗

在当今数字化时代&#xff0c;网络安全问题日益凸显&#xff0c;成为了企业和个人关注的焦点。随着对网络安全人才需求的不断增长&#xff0c;各种网络安全培训班也如雨后春笋般涌现。然而&#xff0c;在培训班学网络安全真的有用吗? 一、网络安全的重要性与挑战 1. 信息时代的…

leaflet 地图基础应用篇

文章目录 leaflet 基础应用一、基础介绍二、功能总结1. 地图加载2. 打点3. 图层控制4. 绘制5. 聚合6. 特效实例应用1. 加载多个不同来源的地图图层并切换2. 加载自定义瓦片地图&#xff08;本地瓦片或私有瓦片服务器&#xff09;3. 加载有不同缩放级别限制的地图图层 图层加载控…

【前端】在Next.js中cors 库的使用及限制跨域请求的速度

cors 库是一个用于方便地启用跨域资源共享&#xff08;CORS&#xff09;的 Node.js 中间件。在 Next.js 中&#xff0c;你可以利用这个库来处理来自不同源的请求。下面是详细的步骤说明&#xff0c;展示如何在 Next.js 中使用 cors 库。 安装 cors 首先&#xff0c;你需要安装…

躺平成长-下一个更新的数据(躺平成长数据显示核心)

旭日图&#xff08;Sunburst Chart&#xff09;是一种用于展示具有层次结构数据的可视化图表。 它起源于饼图和环形图&#xff0c;并随着数据可视化需求的发展而演变。 旭日图通过将层次结构数据以由内向外的同心圆环形式展示&#xff0c;使数据的层次关系更加清晰直观。 以下…