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用起來還是十分方便的

后面我看看能不能做下上傳圖片,上傳文件之類的功能

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/74682.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/74682.shtml
英文地址,請注明出處:http://en.pswp.cn/web/74682.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

大數據Spark(五十七):Spark運行架構與MapReduce區別

文章目錄 Spark運行架構與MapReduce區別 一、Spark運行架構 二、Spark與MapReduce區別 Spark運行架構與MapReduce區別 一、Spark運行架構 Master:Spark集群中資源管理主節點&#xff0c;負責管理Worker節點。Worker:Spark集群中資源管理的從節點&#xff0c;負責任務的運行…

【爬蟲】網頁抓包工具--Fiddler

網頁抓包工具對比&#xff1a;Fiddler與Sniff Master Fiddler基礎知識 Fiddler是一款強大的抓包工具&#xff0c;它的工作原理是作為web代理服務器運行&#xff0c;默認代理地址是127.0.0.1&#xff0c;端口8888。代理服務器位于客戶端和服務器之間&#xff0c;攔截所有HTTP/…

Redis:集群

為什么要有集群&#xff1f; Redis 集群&#xff08;Redis Cluster&#xff09;是 Redis 官方提供的分布式解決方案&#xff0c;用于解決單機 Redis 在數據容量、并發處理能力和高可用性上的局限。通過 Redis 集群&#xff0c;可以實現數據分片、故障轉移和高可用性&#xff0…

【2012】【論文筆記】太赫茲波在非磁化等離子體——

前言 類型 太赫茲 + 等離子體 太赫茲 + 等離子體 太赫茲+等離子體 期刊 物理學報 物理學報 物理學報 作者

Linux字符驅動設備開發入門之框架搭建

聲明 本博客所記錄的關于正點原子i.MX6ULL開發板的學習筆記&#xff0c;&#xff08;內容參照正點原子I.MX6U嵌入式linux驅動開發指南&#xff0c;可在正點原子官方獲取正點原子Linux開發板 — 正點原子資料下載中心 1.0.0 文檔&#xff09;&#xff0c;旨在如實記錄我在學校學…

小剛說C語言刷題——第15講 多分支結構

1.多分支結構 所謂多分支結構是指在選擇的時候有多種選擇。根據條件滿足哪個分支&#xff0c;就走對應分支的語句。 2.語法格式 if(條件1) 語句1; else if(條件2) 語句2; else if(條件3) 語句3; ....... else 語句n; 3.示例代碼 從鍵盤輸入三條邊的長度&#xff0c;…

Apache httpclient okhttp(1)

學習鏈接 Apache httpclient & okhttp&#xff08;1&#xff09; Apache httpclient & okhttp&#xff08;2&#xff09; httpcomponents-client github apache httpclient文檔 apache httpclient文檔詳細使用 log4j日志官方文檔 【Java基礎】- HttpURLConnection…

洛谷題單3-P1420 最長連號-python-流程圖重構

題目描述 輸入長度為 n n n 的一個正整數序列&#xff0c;要求輸出序列中最長連號的長度。 連號指在序列中&#xff0c;從小到大的連續自然數。 輸入格式 第一行&#xff0c;一個整數 n n n。 第二行&#xff0c; n n n 個整數 a i a_i ai?&#xff0c;之間用空格隔開…

使用binance-connector庫獲取Binance全市場的幣種價格,然后選擇一個幣種進行下單

一個完整的示例,展示如何使用 api 獲取Binance全市場的幣種價格,然后選擇一個最便宜的幣種進行下單操作 代碼經過修改,親測可用,目前只可用于現貨,合約的待開發 獲取市場價格:使用client.ticker_price()獲取所有交易對的當前價格 賬戶檢查:獲取賬戶余額,確保有足夠的資…

算法設計學習10

實驗目的及要求&#xff1a; 本查找實驗旨在使學生深入了解不同查找算法的原理、性能特征和適用場景&#xff0c;培養其在實際問題中選擇和應用查找算法的能力。通過實驗&#xff0c;學生將具體實現多種查找算法&#xff0c;并通過性能測試驗證其在不同數據集上的表現&#xff…

5天速成ai agent智能體camel-ai之第1天:camel-ai安裝和智能體交流消息講解(附源碼,零基礎可學習運行)

嗨&#xff0c;朋友們&#xff01;&#x1f44b; 是不是感覺AI浪潮鋪天蓋地&#xff0c;身邊的人都在談論AI Agent、大模型&#xff0c;而你看著那些密密麻麻的代碼&#xff0c;感覺像在讀天書&#xff1f;&#x1f92f; 別焦慮&#xff01;你不是一個人。很多人都想抓住AI的風…

MySQL介紹及使用

1. 安裝、啟動、配置 MySQL 1. 安裝 MySQL 更新軟件包索引 sudo apt update 安裝 MySQL 服務器 sudo apt install mysql-server 安裝過程中可能會提示你設置 root 用戶密碼。如果沒有提示&#xff0c;可以跳過&#xff0c;后續可以手動設置。 2. 配置 MySQL 運行安全腳本…

九、重學C++—類和函數

上一章節&#xff1a; 八、重學C—動態多態&#xff08;運行期&#xff09;-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/147004745?spm1001.2014.3001.5502 本章節代碼&#xff1a; cpp/cppClassAndFunc.cpp CuiQingCheng/cppstudy - 碼云 - 開源中國…

lua和C的交互

1.C調用lua例子 #include <iostream> #include <lua.hpp>int main() {//用于創建一個新的lua虛擬機lua_State* L luaL_newstate();luaL_openlibs(L);//打開標準庫/*if (luaL_dofile(L, "test.lua") ! LUA_OK) {std::cerr << "Lua error: &…

java高并發------守護線程Daemon Thread

文章目錄 1.概念2.生命周期與行為2. 應用場景3. 示例代碼4. 注意事項 1.概念 Daemon &#xff1a; 滴門 在Java中&#xff0c;線程分為兩類&#xff1a;用戶線程(User Thread)和守護線程(Daemon Thread)。 守護線程是后臺線程&#xff0c;主要服務于用戶線程&#xff0c;當所…

Docker存儲策略深度解析:臨時文件 vs 持久化存儲選型指南

Docker存儲策略深度解析&#xff1a;臨時文件 vs 持久化存儲選型指南 一、存儲類型全景對比二、臨時存儲適用場景與風險2.1 最佳使用案例2.2 風險警示 三、持久化存儲技術選型3.1 Volume核心優勢Volume管理命令&#xff1a; 3.2 Bind Mount適用邊界掛載模式對比&#xff1a; 四…

【Linux網絡#18】:深入理解select多路轉接:傳統I/O復用的基石

&#x1f4c3;個人主頁&#xff1a;island1314 &#x1f525;個人專欄&#xff1a;Linux—登神長階 目錄 一、前言&#xff1a;&#x1f525; I/O 多路轉接 為什么需要I/O多路轉接&#xff1f; 二、I/O 多路轉接之 select 1. 初識 select2. select 函數原型2.1 關于 fd_set 結…

高級:微服務架構面試題全攻略

一、引言 在現代軟件開發中&#xff0c;微服務架構被廣泛應用于構建復雜、可擴展的應用程序。面試官通過相關問題&#xff0c;考察候選人對微服務架構的理解、拆分原則的掌握、服務治理的能力以及API網關的運用等。本文將深入剖析微服務架構相關的面試題&#xff0c;結合實際開…

使用MQTTX軟件連接阿里云

使用MQTTX軟件連接阿里云 MQTTX軟件阿里云配置MQTTX軟件設置 MQTTX軟件 阿里云配置 ESP8266連接阿里云這篇文章里有詳細的創建過程&#xff0c;這里就不再重復了&#xff0c;需要的可以點擊了解一下。 MQTTX軟件設置 打開軟件之后&#xff0c;首先點擊添加進行創建。 在阿…

【HFP】藍牙Hands-Free Profile(HFP)核心技術解析

藍牙 Hands-Free Profile&#xff08;HFP&#xff09;作為車載通信和藍牙耳機的核心協議&#xff0c;定義了設備間語音交互的標準化流程&#xff0c;并持續推動著無線語音交互體驗的革新。自2002年首次納入藍牙核心規范以來&#xff0c;HFP歷經多次版本迭代&#xff08;最新為v…