iis搭建網(wǎng)站seo學(xué)校培訓(xùn)課程
目錄
- 后端
- pom.xml
- Config配置類
- Controller類
- DTO
- 前端
- 安裝相關(guān)依賴
- websocketService.js接口
- javascript
- html
- CSS
- 效果展示
- 簡(jiǎn)單測(cè)試連接:
- 報(bào)錯(cuò)解決方法
- 1、vue3 使用SockJS報(bào)錯(cuò) ReferenceError: global is not defined
- 功能補(bǔ)充拓展
- 1. 安全性和身份驗(yàn)證
- 2. 異常處理
- 3. 消息廣播的功能
- 4. 配置 WebSocket 消息緩存和負(fù)載均衡
- 5. 客戶端連接管理
- 6. WebSocket 消息格式和編碼
- 總結(jié)
- 后面將繼續(xù)完善,待更新...
后端
pom.xml
<!-- Spring Boot WebSocket--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Spring Boot 數(shù)據(jù)庫支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- MySQL 驅(qū)動(dòng) --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- Thymeleaf(如果你使用了模板) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- Spring Boot Web 支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
Config配置類
注意:允許源根據(jù)自己項(xiàng)目修改
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;@Configuration
@EnableWebSocketMessageBroker
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/queue", "/topic","/user");config.setApplicationDestinationPrefixes("/app");}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws")
// 在 WebSocket 握手時(shí),我們可以通過 URL 參數(shù)或者 HTTP headers 傳遞用戶身份信息。
// .addInterceptors(new MyHandshakeInterceptor())// 添加攔截器.setAllowedOrigins("http://127.0.0.1:8889", "http://localhost:8889", "http://localhost:8888", "http://127.0.0.1:8888", "http://localhost:8000","http://localhost:8890","http://127.0.0.1:8890").withSockJS(); // 添加 SockJS 支持}
}
Controller類
import com.tianwen.mapper.UserMessagesMapper;
import com.tianwen.user.dtos.MessageDTO;
import com.tianwen.user.pojos.Messages;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.time.LocalDateTime;@Controller
public class ChatController {@Autowiredprivate UserMessagesMapper userMessagesMapper;@Autowiredprivate SimpMessagingTemplate messagingTemplate; // 注入消息模板,用于發(fā)送消息到指定目的地@MessageMapping("/chat.sendMessage")//這個(gè)注解用來監(jiān)聽來自前端的 WebSocket 消息,路徑是 /app/chat.sendMessage,當(dāng)前端發(fā)送消息到這個(gè)路徑時(shí),sendMessage 方法會(huì)被觸發(fā)。
// @SendTo("/topic/messages")//這個(gè)注解表明處理完消息后,返回的消息將廣播給訂閱了 /topic/messages 路徑的所有客戶端。public MessageDTO sendMessage(MessageDTO messageDTO) throws Exception {System.out.println("接收到的message:"+messageDTO);// 1. 可以在這里進(jìn)行私信存儲(chǔ)到數(shù)據(jù)庫操作Messages messages = new Messages();messages.setSenderId(messageDTO.getSenderId());messages.setReceiverId(messageDTO.getReceiverId());messages.setContent(messageDTO.getContent());messages.setCreateTime(LocalDateTime.now());// 2. 保存私信消息(插入操作)if (userMessagesMapper.insert(messages) <= 0) {// 如果插入失敗,可以返回錯(cuò)誤或做其他處理return null;}// 3. 實(shí)時(shí)將消息轉(zhuǎn)發(fā)給接收者String receiverIdStr = String.valueOf(messageDTO.getReceiverId()); // 將 receiverId 轉(zhuǎn)換為 StringString receiverDestination = "/user/" + receiverIdStr + "/queue/messages";//通過 SimpMessagingTemplate 的 convertAndSendToUser 方法,將消息實(shí)時(shí)推送給接收者。//推送的目標(biāo)是 /user/{receiverId}/queue/messages,該路徑是給特定用戶的私有消息隊(duì)列。messagingTemplate.convertAndSendToUser(receiverIdStr, receiverDestination, messageDTO);return messageDTO;}
}
DTO
import lombok.Data;
@Data
public class MessageDTO {private Integer senderId;private Integer receiverId;private String content;
}
前端
安裝相關(guān)依賴
npm install sockjs-client@latest
npm install @stomp/stompjs sockjs-client
npm install global
npm i --save-dev @types/sockjs-client
websocketService.js接口
注意:服務(wù)器地址根據(jù)自己的修改(application.yml)
// websocketService.js
// Stomp.js:用于處理 STOMP 協(xié)議,它在 WebSocket 基礎(chǔ)上實(shí)現(xiàn)了消息訂閱、發(fā)送等功能。
import { Stomp } from "@stomp/stompjs";
//SockJS:是一個(gè)用于實(shí)現(xiàn) WebSocket 的庫,它為 WebSocket 提供了回退機(jī)制(例如 HTTP 長(zhǎng)輪詢等),確保在不同瀏覽器和網(wǎng)絡(luò)環(huán)境下的兼容性。
import SockJS from "sockjs-client/dist/sockjs.min.js";
export default {connect(onMessageReceived) {//使用 SockJS 和 Stomp 創(chuàng)建一個(gè) WebSocket 客戶端,連接到后端的 WebSocket 服務(wù) http://localhost:8000/ws。const socket = new SockJS("http://localhost:8000/ws"); // 使用 SockJS 連接const stompClient = Stomp.over(socket);const userId = JSON.parse(localStorage.getItem("userId"));stompClient.connect({}, () => {console.log("本人消息隊(duì)列ID:", userId);//在連接成功后,通過 stompClient.subscribe 訂閱 /topic/messages,接收從后端廣播過來的消息。// stompClient.subscribe("/topic/messages", (messageOutput) => {// onMessageReceived(JSON.parse(messageOutput.body));// });// 訂閱當(dāng)前用戶的私有消息隊(duì)列stompClient.subscribe("/user/" + userId + "/queue/messages",(messageOutput) => {onMessageReceived(JSON.parse(messageOutput.body)); // 處理接收到的私聊消息});// 訂閱當(dāng)前用戶的私有消息隊(duì)列;// stompClient.subscribe(// "/user/" + userId + "/queue/messages",// function (messageOutput) {// const message = JSON.parse(messageOutput.body);// console.log("接收到私信:", message);// }// );});// 連接到 WebSocket 后,訂閱用戶消息// stompClient.connect({}, function (frame) {// // 獲取當(dāng)前用戶ID// const userId = getCurrentUserId(); // 假設(shè)這個(gè)方法能夠獲取當(dāng)前用戶的ID// // 訂閱接收者的消息隊(duì)列// stompClient.subscribe(// "/user/" + userId + "/queue/messages",// function (messageOutput) {// const message = JSON.parse(messageOutput.body);// console.log("接收到私信:", message);// }// );// });},sendMessage(message) {console.log("發(fā)送消息接口1:", message);const socket = new SockJS("http://localhost:8000/ws"); // 使用 SockJS 連接const stompClient = Stomp.over(socket);stompClient.connect({}, () => {//當(dāng)用戶輸入消息時(shí),通過 stompClient.send 方法將消息發(fā)送到 /app/chat.sendMessage,這個(gè)路徑會(huì)將消息推送到后端進(jìn)行處理。stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(message));});},
};
javascript
<script setup lang="ts">
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import type { IToolbarConfig, IEditorConfig } from "@wangeditor/editor";
const editorRef = shallowRef();
import websocketService from "@/api/websocketService.js";
import {ref,onMounted,
} from "vue";
import websocketService from "@/api/websocketService.js";const receiverIdAnswer = ref();
const sendPrivateMessage = () => {dialogVisible.value = true;
};
const sendPrivateMessage = async (userId) => {receiverIdAnswer.value = userId;dialogVisible.value = true;const response = await getAuthorDetailsByUserId(userId);console.log("response", userId);privateMessagesUser.value = response.data;console.log("privateMessagesUser", privateMessagesUser.value);
};const dialogVisible = ref(false);interface Message {id: string;senderId: string;receiverId: string;content: string;
}const messages = ref<Message[]>([]); // 明確指定消息數(shù)組的類型const newMessage = ref("");onMounted(() => {websocketService.connect((message: Message) => {// 明確指定回調(diào)函數(shù)的參數(shù)類型messages.value.push(message);});
});const sendMessage = () => {if (newMessage.value.trim()) {const message: Message = {// 明確聲明消息類型id: Date.now().toString(), // 使用當(dāng)前時(shí)間戳作為唯一 IDsenderId: userInfo.value.id, // Example senderreceiverId: receiverIdAnswer.value, // Example receivercontent: newMessage.value,};websocketService.sendMessage(message);newMessage.value = "";}
};
const editorConfig: Partial<IEditorConfig> = {placeholder: "請(qǐng)輸入...",MENU_CONF: {},
};
const handleCreated = (editor) => {editorRef.value = editor;
};
// 排除富文本的菜單項(xiàng)
const toolbarConfigPrivateMessages: Partial<IToolbarConfig> = {// toolbar 配置excludeKeys: ["headerSelect","blockquote","|","bold","underline","italic","group-more-style", // 排除菜單組,寫菜單組 key 的值即可"color","bgColor","|","fontSize","fontFamily","lineHeight","bulletedList","numberedList","todo","group-justify","group-indent","insertLink","group-video","insertTable","codeBlock","divider","undo","redo","fullScreen",],
};
</script>
html
<!-- 私信聊天框 --><el-dialog v-model="dialogVisible"><template #title><div style="text-align: center; font-weight: bold">{{ privateMessagesUser.username }}</div><hr class="line" /></template><div class="chat-container"><div class="messages" ref="messagesContainer"><divv-for="message in messages":key="message.id":class="{'my-message': message.senderId === userInfo.id,'other-message': message.senderId !== userInfo.id,}"><div style="display: flex; flex-direction: row"><div><el-image:src="userInfo.avatarUrl"style="width: 45px; border-radius: 50%"></el-image></div><div style="margin-top: 5px; margin-left: 10px"><div><strong>{{ userInfo.username }}</strong></div><divclass="message-bubble message-green"v-html="message.content"></div></div></div></div></div></div><div style="border: 1px solid #ccc"><Toolbarstyle="border-bottom: 1px solid #ccc":editor="editorRef":defaultConfig="toolbarConfigPrivateMessages"mode="default"/><Editorstyle="height: 200px; overflow-y: hidden"v-model="newMessage"@keyup.enter="sendMessage":defaultConfig="editorConfig"mode="default"@onCreated="handleCreated"/></div><template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="sendMessage">發(fā)送</el-button></template></el-dialog>
CSS
<style scoped>
/* 私信樣式 */
/* 標(biāo)題居中 */
/* .private-message-dialog {
} */
.line {border-top: 1px solid #ccc; /* 直線的樣式,可以修改顏色 */
}/* 聊天框滾動(dòng) */
.chat-container {display: flex;flex-direction: column;height: 300px;overflow-y: auto;box-sizing: border-box; /* 讓 padding 和 border 包含在寬度和高度內(nèi) */
}.chat-container > * {width: 100%; /* 確保所有子元素不會(huì)超出容器寬度 */box-sizing: border-box; /* 確保子元素的寬度計(jì)算不受 padding 和 border 影響 */
}/* 消息容器 */
.messages {display: flex;flex-direction: column;gap: 10px;padding: 10px;max-height: 250px;overflow-y: auto;
}/* 發(fā)送方和接收方的消息樣式 */
.my-message {/* text-align: right; */border-radius: 10px;height: auto;/* padding: 5px 10px; */
}.other-message {/* text-align: left; */border-radius: 10px;/* padding: 5px 10px; */
}.message-bubble {/* max-width: 70%; */padding: 0px 10px;border-radius: 10px;/* height: 30px; *//* margin: 0px 0px 0px 0px; *//* word-wrap: break-word; *//* line-height: 1.4; */font-size: 14px;display: flex;align-items: center;justify-content: center;
}.message-green {background-color: #57c457; /* 微信消息綠色 */color: white;align-self: flex-end; /* 讓氣泡靠右顯示 */
}
</style>
效果展示
簡(jiǎn)單測(cè)試連接:
報(bào)錯(cuò)解決方法
1、vue3 使用SockJS報(bào)錯(cuò) ReferenceError: global is not defined
解:
import SockJS from “sockjs-client”;
修改為:
import SockJS from “sockjs-client/dist/sockjs.min.js”;
并安裝依賴
npm i --save-dev @types/sockjs-client
功能補(bǔ)充拓展
以下是一些可能的補(bǔ)充和優(yōu)化,確保 WebSocket 能夠順利運(yùn)行并且高效處理消息。
1. 安全性和身份驗(yàn)證
如果你的 WebSocket 服務(wù)需要進(jìn)行身份驗(yàn)證(如用戶登錄),你可以考慮在 WebSocket 握手時(shí)驗(yàn)證用戶身份。你可以在 WebSocketConfig
中添加一個(gè) HandshakeInterceptor
來攔截握手請(qǐng)求,獲取 HTTP header 或 URL 參數(shù)中的用戶信息,確保只有經(jīng)過身份驗(yàn)證的用戶能夠連接。
例如:
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws").addInterceptors(new MyHandshakeInterceptor()) // 添加攔截器.setAllowedOrigins("http://localhost:8889").withSockJS();
}
其中 MyHandshakeInterceptor
可以用來在 WebSocket 握手期間傳遞用戶信息。
2. 異常處理
在 WebSocket 消息處理過程中,你可能會(huì)遇到一些異常,如數(shù)據(jù)庫操作失敗或消息傳輸失敗等。在控制器中,你可以捕獲這些異常并返回相應(yīng)的錯(cuò)誤消息,確保系統(tǒng)更加健壯。
例如:
@MessageMapping("/chat.sendMessage")
public MessageDTO sendMessage(MessageDTO messageDTO) {try {// 消息處理邏輯} catch (Exception e) {log.error("發(fā)送消息失敗", e);return new MessageDTO("error", "消息發(fā)送失敗");}
}
3. 消息廣播的功能
目前,你的代碼實(shí)現(xiàn)了將消息發(fā)送到指定用戶的功能(私信)。如果你希望實(shí)現(xiàn)群聊功能或全局廣播,可以進(jìn)一步擴(kuò)展 @SendTo
注解。這個(gè)注解的使用使得你可以將處理后的消息發(fā)送給所有訂閱某個(gè)特定主題的客戶端。
例如,廣播消息給所有訂閱 /topic/messages
的客戶端:
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/messages")
public MessageDTO sendMessage(MessageDTO messageDTO) {// 處理消息邏輯return messageDTO; // 返回的消息會(huì)廣播給所有訂閱 /topic/messages 的客戶端
}
4. 配置 WebSocket 消息緩存和負(fù)載均衡
如果你的系統(tǒng)需要處理大量的 WebSocket 連接,可能會(huì)面臨性能和可擴(kuò)展性的問題。在這種情況下,考慮使用 Redis 等消息隊(duì)列作為消息代理,可以通過 @EnableWebSocketMessageBroker
配置遠(yuǎn)程消息代理。
例如,通過 Redis 實(shí)現(xiàn)消息廣播:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/queue", "/topic", "/user");config.setApplicationDestinationPrefixes("/app");// 使用 Redis 消息代理config.setBrokerRegistry().setApplicationDestinationPrefixes("/app").enableStompBrokerRelay("/topic", "/queue").setRelayHost("localhost").setRelayPort(61613); // 配置Redis(如ActiveMQ)等消息中間件}
}
5. 客戶端連接管理
如果你希望在某個(gè)用戶斷開連接時(shí)進(jìn)行一些清理工作(例如清理會(huì)話、推送通知等),你可以使用 @OnDisconnect
注解來捕獲斷開連接事件。
@MessageMapping("/chat.disconnect")
public void handleDisconnect(SessionDisconnectEvent event) {// 用戶斷開連接時(shí)執(zhí)行的邏輯log.info("用戶斷開連接,sessionId: " + event.getSessionId());
}
6. WebSocket 消息格式和編碼
確保你的客戶端和服務(wù)端使用相同的消息格式。你可能需要為 WebSocket 消息提供適當(dāng)?shù)男蛄谢头葱蛄谢?#xff0c;以確保消息能夠正確地從客戶端傳輸?shù)椒?wù)端,以及從服務(wù)端傳輸回客戶端。
例如,使用 Jackson 或其他庫將 Java 對(duì)象序列化為 JSON 格式:
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/messages")
public MessageDTO sendMessage(MessageDTO messageDTO) throws Exception {// 消息傳輸過程中確保使用適當(dāng)?shù)?JSON 格式return messageDTO;
}
總結(jié)
大體上,已經(jīng)完成了 WebSocket 的配置和處理消息的核心部分,剩下的步驟主要是根據(jù)具體業(yè)務(wù)需求做擴(kuò)展,如身份驗(yàn)證、消息緩存、廣播支持等。如果你的應(yīng)用規(guī)模較大,可能還需要考慮負(fù)載均衡和消息隊(duì)列等高可用性的設(shè)計(jì)。