SpringBoot + Netty + Vue + WebSocket实现在线聊天
最近想学学WebSocket做一个实时通讯的练手项目
主要用到的技术栈是WebSocket Netty Vue Pinia MySQL SpringBoot,实现一个持久化数据,单一群聊,支持多用户的聊天界面
下面是实现的过程
后端
SpringBoot启动的时候会占用一个端口,而Netty也会占用一个端口,这两个端口不能重复,并且因为Netty启动后会阻塞当前线程,因此需要另开一个线程防止阻塞住SpringBoot
1. 编写Netty服务器
个人认为,Netty最关键的就是channel,可以代表一个客户端
我在这使用的是@PostConstruct注解,在Bean初始化后调用里面的方法,新开一个线程运行Netty,因为希望Netty受Spring管理,所以加上了spring的注解,也可以直接在启动类里注入Netty然后手动启动
@Service
public class NettyService {private EventLoopGroup bossGroup = new NioEventLoopGroup(1);private EventLoopGroup workGroup = new NioEventLoopGroup();@Autowiredprivate WebSocketHandler webSocketHandler;@Autowiredprivate HeartBeatHandler heartBeatHandler;@PostConstructpublic void initNetty() throws BaseException {new Thread(()->{try {start();} catch (Exception e) {throw new RuntimeException(e);}}).start();}@PreDestroypublic void destroy() throws BaseException {bossGroup.shutdownGracefully();workGroup.shutdownGracefully();}@Asyncpublic void start() throws BaseException {try {ChannelFuture channelFuture = new ServerBootstrap().group(bossGroup, workGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.DEBUG)).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {nioSocketChannel.pipeline()
// http解码编码器.addLast(new HttpServerCodec())
// 处理完整的 HTTP 消息.addLast(new HttpObjectAggregator(64 * 1024))
// 心跳检测时长.addLast(new IdleStateHandler(300, 0, 0, TimeUnit.SECONDS))
// 心跳检测处理器.addLast(heartBeatHandler)
// 支持ws协议(自定义).addLast(new WebSocketServerProtocolHandler("/ws",null,true,64*1024,true,true,10000))
// ws请求处理器(自定义).addLast(webSocketHandler);}}).bind(8081).sync();System.out.println("Netty启动成功");ChannelFuture future = channelFuture.channel().closeFuture().sync();}catch (InterruptedException e){throw new InterruptedException ();}finally {
//优雅关闭bossGroup.shutdownGracefully();workGroup.shutdownGracefully();}}
}
服务器类只是指明一些基本信息,包含处理器类,支持的协议等等,具体的处理逻辑需要再自定义类来实现
2. 心跳检测处理器
心跳检测是指 服务器无法主动确定客户端的状态(用户可能关闭了网页,但是服务端没办法知道),为了确定客户端是否在线,需要客户端定时发送一条消息,消息内容不重要,重要的是发送消息代表该客户端仍然在线,当客户端长时间没有发送数据时,代表客户端已经下线
package org.example.payroll_management.websocket.netty.handler;@Component
@ChannelHandler.Sharable
public class HeartBeatHandler extends ChannelDuplexHandler {@Autowiredprivate ChannelContext channelContext;private static final Logger logger = LoggerFactory.getLogger(HeartBeatHandler.class);@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent){// 心跳检测超时IdleStateEvent e = (IdleStateEvent) evt;logger.info("心跳检测超时");if (e.state() == IdleState.READER_IDLE){Attribute<Integer> attr = ctx.channel().attr(AttributeKey.valueOf(ctx.channel().id().toString()));Integer userId = attr.get();// 读超时,当前已经下线,主动断开连接ChannelContext.removeChannel(userId);ctx.close();} else if (e.state() == IdleState.WRITER_IDLE){ctx.writeAndFlush("心跳检测");}}super.userEventTriggered(ctx, evt);}
}
3. webSocket处理器
当客户端发送消息,消息的内容会发送当webSocket处理器中,可以对对应的方法进行处理,我这里偷懒了,就做了一个群组,全部用户只能在同一群中聊天,不过创建多个群组,或单对单聊天也不复杂,只需要将群组的ID进行保存就可以
这里就产生第一个问题了,就是SpringMVC的拦截器不会拦截其他端口的请求,解决方法是将token放置到请求参数中,在userEventTriggered方法中重新进行一次token检验
第二个问题,我是在拦截器中通过ThreadLocal保存用户ID,不走拦截器在其他地方拿不到用户ID,解决方法是,在userEventTriggered方法中重新保存,或者channel中可以保存附件(自身携带的数据),直接将id保存到附件中
第三个问题,消息的持久化,当用户重新打开界面时,肯定希望消息仍然存在,鉴于webSocket的实时性,数据持久化肯定不能在同一个线程中完成,我在这使用BlockingQueue+线程池完成对消息的异步保存,或者也可以用mq实现
不过用的Executors.newSingleThreadExecutor();可能会产生OOM的问题,后面可以自定义一个线程池,当任务满了之后,指定拒绝策略为抛出异常,再通过全局异常捕捉拿到对应的数据保存到数据库中,不过俺这种小项目应该不会产生这种问题
第四个问题,消息内容,这个需要前后端统一一下,确定一下传输格式就OK了,然后从JSON中取出数据处理
最后就是在线用户统计,这个没什么好说的,里面有对应的方法,当退出时,直接把channel踢出去就可以了
package org.example.payroll_management.websocket.netty.handler;@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {@Autowiredprivate ChannelContext channelContext;@Autowiredprivate MessageMapper messageMapper;@Autowiredprivate UserService userService;private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);private static final BlockingQueue<WebSocketMessageDto> blockingQueue = new ArrayBlockingQueue(1024 * 1024);private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();// 提交线程@PostConstructprivate void init(){EXECUTOR_SERVICE.submit(new MessageHandler());}private class MessageHandler implements Runnable{// 异步保存@Overridepublic void run() {while(true){WebSocketMessageDto message = null;try {message = blockingQueue.take();logger.info("消息持久化");} catch (InterruptedException e) {throw new RuntimeException(e);}Integer success = messageMapper.saveMessage(message);if (success < 1){try {throw new BaseException("保存信息失败");} catch (BaseException e) {throw new RuntimeException(e);}}}}}// 当读事件发生时(有客户端发送消息)@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {Channel channel = channelHandlerContext.channel();// 收到的消息String text = textWebSocketFrame.text();Attribute<Integer> attr = channelHandlerContext.channel().attr(AttributeKey.valueOf(channelHandlerContext.channel().id().toString()));Integer userId = attr.get();logger.info("接收到用户ID为 {} 的消息: {}",userId,text);// TODO 将text转成JSON,提取里面的数据WebSocketMessageDto webSocketMessage = JSONUtil.toBean(text, WebSocketMessageDto.class);if (webSocketMessage.getType().equals("心跳检测")){logger.info("{}发送心跳检测",userId);}else if (webSocketMessage.getType().equals("群发")){ChannelGroup channelGroup = ChannelContext.getChannelGroup(null);WebSocketMessageDto messageDto = JSONUtil.toBean(text, WebSocketMessageDto.class);WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();webSocketMessageDto.setType("群发");webSocketMessageDto.setText(messageDto.getText());webSocketMessageDto.setReceiver("all");webSocketMessageDto.setSender(String.valueOf(userId));webSocketMessageDto.setSendDate(TimeUtil.timeFormat("yyyy-MM-dd"));blockingQueue.add(webSocketMessageDto);channelGroup.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonPrettyStr(webSocketMessageDto)));}else{channel.writeAndFlush("请发送正确的格式");}}// 建立连接后触发(有客户端建立连接请求)@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {logger.info("建立连接");super.channelActive(ctx);}// 连接断开后触发(有客户端关闭连接请求)@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {Attribute<Integer> attr = ctx.channel().attr(AttributeKey.valueOf(ctx.channel().id().toString()));Integer userId = attr.get();logger.info("用户ID:{} 断开连接",userId);ChannelGroup channelGroup = ChannelContext.getChannelGroup(null);channelGroup.remove(ctx.channel());ChannelContext.removeChannel(userId);WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();webSocketMessageDto.setType("用户变更");List<OnLineUserVo> onlineUser = userService.getOnlineUser();webSocketMessageDto.setText(JSONUtil.toJsonStr(onlineUser));webSocketMessageDto.setReceiver("all");webSocketMessageDto.setSender("0");webSocketMessageDto.setSendDate(TimeUtil.timeFormat("yyyy-MM-dd"));channelGroup.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(webSocketMessageDto)));super.channelInactive(ctx);}// 建立连接后触发(客户端完成连接)@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete){WebSocketServerProtocolHandler.HandshakeComplete handshakeComplete = (WebSocketServerProtocolHandler.HandshakeComplete) evt;String uri = handshakeComplete.requestUri();logger.info("uri: {}",uri);String token = getToken(uri);if (token == null){logger.warn("Token校验失败");ctx.close();throw new BaseException("Token校验失败");}logger.info("token: {}",token);Integer userId = null;try{Claims claims = JwtUtil.extractClaims(token);userId = Integer.valueOf((String) claims.get("userId"));}catch (Exception e){logger.warn("Token校验失败");ctx.close();throw new BaseException("Token校验失败");}// 向channel中的附件中添加用户IDchannelContext.addContext(userId,ctx.channel());ChannelContext.setChannel(userId,ctx.channel());ChannelContext.setChannelGroup(null,ctx.channel());ChannelGroup channelGroup = ChannelContext.getChannelGroup(null);WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();webSocketMessageDto.setType("用户变更");List<OnLineUserVo> onlineUser = userService.getOnlineUser();webSocketMessageDto.setText(JSONUtil.toJsonStr(onlineUser));webSocketMessageDto.setReceiver("all");webSocketMessageDto.setSender("0");webSocketMessageDto.setSendDate(TimeUtil.timeFormat("yyyy-MM-dd"));channelGroup.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(webSocketMessageDto)));}super.userEventTriggered(ctx, evt);}private String getToken(String uri){if (uri.isEmpty()){return null;}if(!uri.contains("token")){return null;}String[] split = uri.split("\\?");if (split.length!=2){return null;}String[] split1 = split[1].split("=");if (split1.length!=2){return null;}return split1[1];}
}
4. 工具类
主要用来保存用户信息的
不要问我为什么又有static又有普通方法,问就是懒得改,这里我直接保存的同一个群组,如果需要多群组的话,就需要建立SQL数据了
package org.example.payroll_management.websocket;@Component
public class ChannelContext {private static final Map<Integer, Channel> USER_CHANNEL_MAP = new ConcurrentHashMap<>();private static final Map<Integer, ChannelGroup> USER_CHANNELGROUP_MAP = new ConcurrentHashMap<>();private static final Integer GROUP_ID = 10086;private static final Logger logger = LoggerFactory.getLogger(ChannelContext.class);public void addContext(Integer userId,Channel channel){String channelId = channel.id().toString();AttributeKey attributeKey = null;if (AttributeKey.exists(channelId)){attributeKey = AttributeKey.valueOf(channelId);} else{attributeKey = AttributeKey.newInstance(channelId);}channel.attr(attributeKey).set(userId);}public static List<Integer> getAllUserId(){return new ArrayList<>(USER_CHANNEL_MAP.keySet());}public static void setChannel(Integer userId,Channel channel){USER_CHANNEL_MAP.put(userId,channel);}public static Channel getChannel(Integer userId){return USER_CHANNEL_MAP.get(userId);}public static void removeChannel(Integer userId){USER_CHANNEL_MAP.remove(userId);}public static void setChannelGroup(Integer groupId,Channel channel){if(groupId == null){groupId = GROUP_ID;}ChannelGroup channelGroup = USER_CHANNELGROUP_MAP.get(groupId);if (channelGroup == null){channelGroup =new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);USER_CHANNELGROUP_MAP.put(GROUP_ID, channelGroup);}if (channel == null){return ;}channelGroup.add(channel);logger.info("向group中添加channel,ChannelGroup已有Channel数量:{}",channelGroup.size());}public static ChannelGroup getChannelGroup(Integer groupId){if (groupId == null){groupId = GROUP_ID;}return USER_CHANNELGROUP_MAP.get(groupId);}public static void removeChannelGroup(Integer groupId){if (groupId == null){groupId = GROUP_ID;}USER_CHANNELGROUP_MAP.remove(groupId);}
}
写到这里,Netty服务就搭建完成了,后面就可以等着前端的请求建立了
前端
前端我使用的vue,因为我希望当用户登录后自动建立ws连接,所以我在登录成功后添加上了ws建立请求,然后我发现,如果用户关闭网页后重新打开,因为跳过了登录界面,ws请求不会自动建立,所以需要一套全局的ws请求
不过我前端不是很好(其实后端也一般),所以很多地方肯定有更优的写法
1. pinia
使用pinia保存ws请求,方便在其他组件中调用
定义WebSocket实例(ws)和一个请求建立判断(wsConnect)
后面就可以通过ws接收服务的消息
import { defineStore } from 'pinia'export const useWebSocketStore = defineStore('webSocket', {state() {return {ws: null,wsConnect: false,}},actions: {wsInit() {if (this.ws === null) {const token = localStorage.getItem("token")if (token === null) return;this.ws = new WebSocket(`ws://localhost:8081/ws?token=${token}`)this.ws.onopen = () => {this.wsConnect = true;console.log("ws协议建立成功")// 发送心跳const intervalId = setInterval(() => {if (!this.wsConnect) {clearInterval(intervalId)}const webSocketMessageDto = {type: "心跳检测"}this.sendMessage(JSON.stringify(webSocketMessageDto));}, 1000 * 3 * 60);}this.ws.onclose = () => {this.ws = null;this.wsConnect = false;}}},sendMessage(message) {if (message == null || message == '') {return;}if (!this.wsConnect) {console.log("ws协议没有建立")this.wsInit();}this.ws.send(message);},wsClose() {if (this.wsConnect) {this.ws.close();this.wsConnect = false;}}}
})
然后再app.vue中循环建立连接(建立请求重试)
const wsConnect = function () {const token = localStorage.getItem("token")if (token === null) {return;}try {if (!webSocket.wsConnect) {console.log("尝试建立ws请求")webSocket.wsInit();} else {return;}} catch {wsConnect();}}
2. 聊天组件
界面相信大伙都会画,主要说一下我遇到的问题
第一个 上拉刷新,也就是加载历史记录的功能,我用的element-plus UI,也不知道是不是我的问题,UI里面的无限滚动不是重复发送请求就是无限发送请求,而且好像没有上拉加载的功能。于是我用了IntersectionObserver来解决,在页面底部加上一个div,当观察到这个div时,触发请求
第二个 滚动条到达顶部时,请求数据并放置数据,滚动条会自动滚动到顶部,并且由于观察的元素始终在顶端导致无限请求,这个其实也不是什么大问题,因为聊天的消息是有限的,没有数据之后我设置了停止观察,主要是用户体验不是很好。这是我是添加了display: flex; flex-direction: column-reverse;解决这个问题的(flex很神奇吧)。大致原理好像是垂直翻转了(例如上面我将观察元素放到div第一个子元素位置,添加flex后观察元素会到最后一个子元素位置上),也就是说当滚动条在最底部时,添加数据后,滚动条会自动滚动到最底部,不过这样体验感非常的不错
不要问我为什么数据要加 || 问就是数据懒得统一了
<style lang="scss" scoped>.chatBox {border-radius: 20px;box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;width: 1200px;height: 600px;background-color: white;display: flex;.chat {width: 1000px;height: inherit;.chatBackground {height: 500px;overflow: auto;display: flex;flex-direction: column-reverse;.loading {text-align: center;font-size: 12px;margin-top: 20px;color: gray;}.chatItem {width: 100%;padding-bottom: 20px;.avatar {margin-left: 20px;display: flex;align-items: center;.username {margin-left: 10px;color: rgb(153, 153, 153);font-size: 13px;}}.chatItemMessage {margin-left: 60px;padding: 10px;font-size: 14px;width: 200px;word-break: break-all;max-width: 400px;line-height: 25px;width: fit-content;border-radius: 10px;height: auto;/* background-color: skyblue; */box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;}.sendDate {font-size: 12px;margin-top: 10px;margin-left: 60px;color: rgb(187, 187, 187);}}}.chatBottom {height: 100px;background-color: #F3F3F3;border-radius: 20px;display: flex;box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;.messageInput {border-radius: 20px;width: 400px;height: 40px;}}}.userList {width: 200px;height: inherit;border-radius: 20px;box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;.user {width: inherit;height: 50px;line-height: 50px;text-indent: 2em;border-radius: 20px;transition: all 0.5s ease;}}}.user:hover {box-shadow: rgba(0, 0, 0, 0.05) -2px 0px 8px 0px;transform: translateX(-5px) translateY(-5px);}
</style><template>{{hasMessage}}<div class="chatBox"><div class="chat"><div class="chatBackground" ref="chatBackgroundRef"><div class="chatItem" v-for="i in messageList"><div class="avatar"><el-avatar :size="40" :src="imageUrl" /><div class="username">{{i.username || i.userId}}</div></div><div class="chatItemMessage">{{i.text || i.content}}</div><div class="sendDate">{{i.date || i.sendDate}}</div></div><div class="loading" ref="loading">显示更多内容</div></div><div class="chatBottom"><el-input class="messageInput" v-model="message" placeholder="消息内容"></el-input><el-button @click="sendMessage">发送消息</el-button></div></div><!-- 做成无限滚动 --><div class="userList"><div v-for="user in userList"><div class="user">{{user.userName}}</div></div></div></div>
</template><script setup>import { ref, onMounted, nextTick } from 'vue'import request from '@/utils/request.js'import { useWebSocketStore } from '@/stores/useWebSocketStore'import imageUrl from '@/assets/默认头像.jpg'const webSocketStore = useWebSocketStore();const chatBackgroundRef = ref(null)const userList = ref([])const message = ref('')const messageList = ref([])const loading = ref(null)const page = ref(1);const size = 10;const hasMessage = ref(true);const observer = new IntersectionObserver((entries, observer) => {entries.forEach(async entry => {if (entry.isIntersecting) {observer.unobserve(entry.target)await pageQueryMessage();}})})onMounted(() => {observer.observe(loading.value)getOnlineUserList();if (!webSocketStore.wsConnect) {webSocketStore.wsInit();}const ws = webSocketStore.ws;ws.onmessage = async (e) => {// console.log(e);const webSocketMessage = JSON.parse(e.data);const messageObj = {username: webSocketMessage.sender,text: webSocketMessage.text,date: webSocketMessage.sendDate,type: webSocketMessage.type}console.log("###")// console.log(JSON.parse(messageObj.text))if (messageObj.type === "群发") {messageList.value.unshift(messageObj)} else if (messageObj.type === "用户变更") {userList.value = JSON.parse(messageObj.text)}await nextTick();// 当发送新消息时,自动滚动到页面最底部,可以替换成消息提示的样式// chatBackgroundRef.value.scrollTop = chatBackgroundRef.value.scrollHeight;console.log(webSocketMessage)}})const pageQueryMessage = function () {request({url: '/api/message/pageQueryMessage',method: 'post',data: {page: page.value,size: size}}).then((res) => {console.log(res)if (res.data.data.length === 0) {hasMessage.value = false;}else {observer.observe(loading.value)page.value = page.value + 1;messageList.value.push(...res.data.data)}})}function getOnlineUserList() {request({url: '/api/user/getOnlineUser',method: 'get'}).then((res) => {console.log(res)userList.value = res.data.data;})}const sendMessage = function () {if (!webSocketStore.wsConnect) {webSocketStore.wsInit();}const webSocketMessageDto = {type: "群发",text: message.value}webSocketStore.sendMessage(JSON.stringify(webSocketMessageDto));}</script>
这样就实现了一个简易的聊天数据持久化,支持在线聊天的界面,总的来说WebSocket用起来还是十分方便的
后面我看看能不能做下上传图片,上传文件之类的功能
相关文章:
SpringBoot + Netty + Vue + WebSocket实现在线聊天
最近想学学WebSocket做一个实时通讯的练手项目 主要用到的技术栈是WebSocket Netty Vue Pinia MySQL SpringBoot,实现一个持久化数据,单一群聊,支持多用户的聊天界面 下面是实现的过程 后端 SpringBoot启动的时候会占用一个端口ÿ…...
配置mac mini M4 的一些软件
最近更换了 mac mini M4 ,想要重新下载配置软件 ,记录一下。 Homebrew是什么? homebrew是一款Mac OS平台下的软件包管理工具,拥有安装、卸载、更新、查看、搜索等功能。通过简单的指令可以实现包管理,而不用关心各种…...
Java——抽象方法抽象类 接口 详解及综合案例
1.抽象方法抽象类 介绍 抽象方法: 将共性的行为(方法)抽取到父类之后, 由于每一个子类执行的内容是不一样, 所以,在父类中不能确定具体的方法体。 该方法就可以定义为抽象方法。 抽象类: 如果一个类中存在抽象方法,那么该类就必须…...
【计网】一二章习题
1. (单选题, 3 分) 假设主机A和B之间的链路带宽为100Mbps,主机A的网卡速率为1Gbps,主机B的网卡速率为10Mbps,主机A给主机B发送数据的最高理论速率为( )。 A. 100Mbps B. 1Gbps C. 1Mbps D. 10Mbps 正确答案 D 发…...
苹果开发者账号推送证书配置详细指南
苹果开发者账号推送证书配置详细指南 一、准备工作 苹果开发者账号 确保拥有有效的苹果开发者账号(个人/公司账号),年费已缴纳。 App ID配置 登录 Apple开发者中心。进入 Certificates, Identifiers & Profiles → Identifiers。创建或…...
3. 列表操作
【问题描述】对于一个列表,在保持非零元素相对顺序的同时,将元素中所有的数字0移动到末尾。…...
【软考-高级】【信息系统项目管理师】【论文基础】进度管理过程输入输出及工具技术的使用方法
定义 项目进度管理是为了保证项目按时完成,对项目中所需的各个过程进行管理的过程,包括规划进度、定义活动、活动优先级排序、活动持续时间、制定进度计划和控制进度。 管理基础 制定进度计划的一般步骤 选择进度计划方法(如关键路径法&a…...
TOGAF之架构标准规范-技术架构
TOGAF是工业级的企业架构标准规范,本文主要描述技术架构阶段。 如上所示,技术架构(Technology Architecture)在TOGAF标准规范中处于D阶段 技术架构阶段 技术架构阶段的主要内容包括阶段目标、阶段输入、流程步骤、阶段输出、架构…...
为什么ChatGPT选择SSE而非WebSocket?
为什么ChatGPT选择SSE而非WebSocket? 一、ChatGPT回答问题的技术逻辑 ChatGPT的响应生成基于Transformer架构和自注意力机制,其核心是通过概率预测逐词生成文本。当用户输入问题后,模型会先解析上下文,再通过预训练的庞大语料库…...
Ansys Electronics 变压器 ACT
你好, 在本博客中,我将讨论如何使用 Ansys 电子变压器 ACT 自动快速地设计电力电子电感器或变压器。我将逐步介绍设计和创建电力电子变压器示例的步骤,该变压器为同心组件,双绕组,采用正弦电压激励,并应用…...
十三种物联网/通信模块综合对比——《数据手册--物联网/通信模块》
物联网/通信模块 名称 功能 应用场景 USB转换模块 用于将USB接口转换为其他类型的接口,如串口、并口等,实现不同设备之间的通信。 常用于计算机与外部设备(如打印机、扫描仪等)的连接,以及数据传输和设…...
Redis安装(Windows环境)
文章目录 Resid简介:下载Redis启动Redis服务设置Windows服务常用的Redis服务命令 Resid简介: Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。 Redis通常…...
FreeRTOS项目工程完善指南:STM32F103C8T6系列
FreeRTOS项目工程完善指南:STM32系列 本文是FreeRTOS STM32开发系列教程的一部分。我们将完善之前移植的FreeRTOS工程,添加串口功能并优化配置文件。 更多优质资源,请访问我的GitHub仓库:https://github.com/Despacito0o/FreeRTO…...
论坛系统(测试报告)
文章目录 一、项目介绍二、设计测试用例三、自动化测试用例的部分展示用户名或密码错误登录成功编辑自己的帖子成功修改个人信息成功回复帖子信息成功 四、性能测试总结 一、项目介绍 本平台是用Java开发,基于SpringBoot、SpringMVC、MyBatis框架搭建的小型论坛系统…...
【汽车产品开发项目管理——端到端的汽车产品诞生流程】
MPU:集成运算器、寄存器和控制器的中央处理器芯片 MCU:微控制单元,将中央处理器CPU、存储器ROM/RAM、计数器、IO接口及多种外设模块集成在单一芯片上的微型计算机系统。 汽车产品开发项目属性:临时性、独特性、渐进明细性、以目标…...
从零到有的游戏开发(visual studio 2022 + easyx.h)
引言 本文章适用于C语言初学者掌握基本的游戏开发, 我将用详细的步骤引领大家如何开发属于自己的游戏。 作者温馨提示:不要认为开发游戏很难,一些基本的游戏逻辑其实很简单, 关于游戏的开发环境也不用担心,我会详细…...
Open3d无法使用plt.get_cmap(“viridis“)着色pcd格式点云问题
在使用Open3D进行点云处理和可视化时,我们经常会遇到一个问题:直接加载PCD文件时,点云的颜色无法正确显示,但将其转换为PLY格式后再加载,颜色就能正常显示。本文将探讨这一问题的原因,并提供解决方案。 1.…...
网络故障排查实战指南:从准备到定位的全流程拆解
目录 第一章:排查前的准备工作 —— 别急着动手,先把底摸清 搞清楚故障现象:别被表象骗了 收集关键信息:把线索攒齐 做好心理准备:复杂问题不慌 第二章:排查工具箱 —— 你的 “武器” 得趁手 Wireshark:抓包界的 “显微镜” Ping:最基础但超实用的 “敲门员” …...
MCU的USB接口作为 USB CDC串口输出
前言: 如下内容是和Chatgpt的问答对话。询问了Chatgpt 关于 MCU微控制器内部的USB端口作为串口输出是怎么工作的,是否需要在上位机上安装串口驱动程序等,Chatgpt解答的很好。 正文: STM32 使用USB作为串行设备端口,需…...
【C++初阶】--- vector容器功能模拟实现
1.什么是vector? 在 C 里,std::vector 是标准模板库(STL)提供的一个非常实用的容器类,它可以看作是动态数组 2.成员变量 iterator _start;:指向 vector 中第一个元素的指针。 iterator _finish;&#x…...
函数式编程在 Java:Function、BiFunction、UnaryOperator 你真的会用?
大家好,我是你们的Java技术博主!今天我们要深入探讨Java函数式编程中的几个核心接口:Function、BiFunction和UnaryOperator。很多同学虽然知道它们的存在,但真正用起来却总是不得要领。这篇文章将带你彻底掌握它们!&am…...
Elasticsearch 学习规划
Elasticsearch 学习规划 明确学习目标与动机 场景化需求分析 - **S**:掌握Elasticsearch架构体系,熟练使用Elasticsearch 进行数据分析,Elasticsearch结合java 项目落地案例 - **M**:搜索和Elasticsearch相关GitHub项目 - **A**:每…...
【AI提示词】Emoji风格排版艺术与设计哲学
提示说明 Emoji风格排版艺术与设计哲学。 提示词 请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。使用案例(春日穿搭) 🌸 2025春季穿搭灵…...
LVM 扩容详解
目录 一、LVM扩容 1. 查看磁盘分区情况: 2. 查看pv、vg、lv 情况 3. 将新硬盘分区初始化 4. 将初始化后的分区添加到VG中 5. 查看逻辑卷的设备路径 6. VG分配给lv 二、扩展文件系统 1.确认文件系统类型 三、检验 一、LVM扩容 1. 查看磁盘分区情况: …...
STM32 低功耗模式下 RTC唤醒 和 PA0唤醒 的配合使用
STM32 低功耗模式不同唤醒源的配合使用 by 矜辰所致前言 关于 STM32 如何实现低功耗模式,我之前写过一篇文章: STM32 使用 STM32CubeMX HAL库实现低功耗模式 各种休眠模式如何实现文中已经讲得很清楚了,但是作为教学文章,文…...
QML 弹窗控件:Popup的基本用法与样式
目录 引言相关阅读Popup基本属性工程结构示例实现Main.qml - 主界面SimplePopup.qml - 简单弹窗ModalPopup.qml - 模态弹窗CustomPopup.qml - 自定义样式弹窗AnimatedPopup.qml - 带动画的弹窗 总结工程下载 引言 在现代图形用户界面(GUI)开发中,弹窗(Popup)是一种…...
MCP基础学习三:MCP客户端开发与工具集成
MCP客户端开发与工具集成 文章目录 MCP客户端开发与工具集成一, 学习目标二, 学习内容1. MCP客户端与服务端的通信方式1.1 通信原理1.2 通信实现分析 2. 如何开发MCP工具并集成到客户端2.1 工具开发流程2.2 工具实现示例2.3 客户端集成 3. 如何集成外部API到MCP客户端3.1 集成流…...
NSS#Round30 Web
小桃的PHP挑战 <?php include jeer.php; highlight_file(__FILE__); error_reporting(0); $A 0; $B 0; $C 0;//第一关 if (isset($_GET[one])){$str $_GET[str] ?? 0;$add substr($str, 0, 1); $add;if (strlen($add) > 1 ) {$A 1;} else {echo $one; } } else…...
POSIX线程(pthread)库:线程的终止与管理
在POSIX线程(pthread)库中,线程的终止和管理涉及多个关键函数。以下是关于线程终止的pthread系列函数的详细介绍: 1. pthread_exit:线程主动退出 ✨ 功能: 允许线程主动终止自身,并返回一个退出…...
解决 IntelliJ IDEA 中 Maven 项目左侧项目视图未显示顶层目录问题的详细步骤说明
以下是解决 IntelliJ IDEA 中 Maven 项目左侧项目视图未显示顶层目录问题的详细步骤说明: 1. 切换项目视图模式 默认情况下,IDEA 的项目视图可能处于 Packages 模式,仅显示代码包结构,而非物理目录。 操作步骤: 点击…...
