為什么更推薦WebSocket
Server-Sent Events (SSE) 是一種服務器向客戶端推送數據的單向通信協議
,適合某些場景,在解決用戶同時登錄和實時獲取用戶信息的問題上,WebSocket 是更好的選擇。
1. SSE 的局限性
單向通信
- SSE 是單向的,只能從服務器向客戶端推送數據,客戶端無法通過 SSE 向服務器發送數據。
- 如果需要雙向通信(如用戶登錄后需要發送確認消息),SSE 無法滿足需求。
連接限制
- 瀏覽器對 SSE 的連接數有限制(通常
每個域名最多 6 個并發連接
)。 - 如果用戶同時打開多個頁面,可能會占用大量連接,導致新的連接無法建立。
協議支持
- SSE
基于 HTTP 協議
,不支持二進制數據傳輸
,只能傳輸文本數據
。 - 如果需要傳輸二進制數據(如圖片、文件),SSE 無法實現。
兼容性
- 雖然現代瀏覽器都支持 SSE,但在某些特殊環境(如舊版瀏覽器或移動端)可能
存在兼容性問題
。
2. WebSocket 的優勢
雙向通信
- WebSocket 是全雙工的,支持服務器和客戶端之間的雙向通信。
- 適合需要客戶端和服務器交互的場景(如用戶登錄后需要發送確認消息)。
高性能
- WebSocket 的
連接是持久的
,只有在有數據更新時才會傳輸數據,減少不必要的請求。 - 適合高頻更新的場景(如實時通知、聊天應用)。
支持二進制數據
- WebSocket 支持二進制數據傳輸,適合傳輸圖片、文件等數據。
多用戶并發
- WebSocket 可以為每個用戶維護獨立的連接,避免多個用戶之間的數據沖突。
- 適合多用戶并發的場景。
3. 場景對比
特性 | WebSocket | SSE |
---|---|---|
通信方式 | 雙向通信 | 單向通信(服務器 → 客戶端) |
性能 | 高性能,適合高頻更新 | 適合低頻更新 |
數據傳輸 | 支持文本和二進制數據 | 僅支持文本數據 |
連接限制 | 無連接限制 | 每個域名最多 6 個并發連接 |
兼容性 | 現代瀏覽器均支持 | 部分舊版瀏覽器不支持 |
適用場景 | 實時通知、聊天應用、多用戶并發 | 低頻通知、狀態更新 |
4. 為什么選擇 WebSocket?
用戶同時登錄時需要實時獲取用戶信息,且可能需要雙向通信(如用戶登錄后需要發送確認消息)。因此,WebSocket 是更好的選擇,因為它:
- 支持雙向通信,滿足復雜交互需求。
- 性能高,適合高頻更新。
- 支持多用戶并發,避免數據沖突。
5. SSE 的適用場景
SSE 適合以下場景:
- 低頻通知:如新聞更新、股票價格變動。
- 狀態更新:如任務進度、系統狀態。
- 單向通信:只需要服務器向客戶端推送數據。
如果你的場景是單向的、低頻的,且不需要客戶端向服務器發送數據,SSE 是一個簡單的選擇。
6. 總結
- 推薦使用 WebSocket,因為它
支持雙向通信
、性能高
、適合多用戶并發
。 - SSE 適合低頻、單向的場景,但在用戶同時登錄和實時獲取用戶信息的問題上,WebSocket 是更好的選擇。
實現步驟
解決用戶同時登錄時輪詢獲取用戶信息出錯的問題,推薦使用 WebSocket。以下是詳細的原因和實現方法:
1. 后端實現 WebSocket 服務器
安裝依賴
npm install ws
代碼實現
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 }); // WebSocket 服務器端口// 存儲所有連接的客戶端
const clients = new Map();wss.on('connection', (ws, req) => {console.log('客戶端已連接');// 從請求中獲取用戶標識(如 openid)const openid = new URL(req.url, 'http://localhost').searchParams.get('openid');if (openid) {clients.set(openid, ws); // 將用戶標識和 WebSocket 連接關聯}// 監聽客戶端消息ws.on('message', (message) => {console.log('收到客戶端消息:', message);});// 監聽連接關閉ws.on('close', () => {console.log('客戶端已斷開連接');if (openid) {clients.delete(openid); // 移除用戶標識}});
});// 推送登錄狀態
function pushLoginStatus(openid, userInfo) {const ws = clients.get(openid);if (ws) {ws.send(JSON.stringify({type: 'login_success',data: {openid,userInfo}}));}
}module.exports = { pushLoginStatus };
2. 在用戶登錄成功后推送消息
在用戶登錄成功的邏輯中,調用 pushLoginStatus
推送消息。
const { pushLoginStatus } = require('./websocket'); // 引入 WebSocket 模塊app.get('/auth/callback', async (req, res) => {const { code } = req.query;if (!code) {return res.status(400).send("Code 參數獲取失敗");}try {// 1. 用 code 換取 access_token 和 openidconst { data: tokenData } = await axios.get(`https://api.weixin.qq.com/sns/oauth2/access_token`, {params: { appid, secret: appsecret, code, grant_type: "authorization_code" }});if (!tokenData.access_token || !tokenData.openid) return res.status(400).send("獲取 access_token 失敗");if (tokenData.scope !== 'snsapi_userinfo') return res.status(400).send("用戶未授權");const { access_token, openid } = tokenData;// 2. 獲取用戶信息const { data: userInfo } = await axios.get(`https://api.weixin.qq.com/sns/userinfo`, {params: { access_token, openid, lang: "zh_CN" }});const { nickname, sex, province, city, headimgurl } = userInfo;// 3. 查詢數據庫,判斷用戶是否存在db.query(`SELECT * FROM users WHERE openid = ?`, [openid], (err, results) => {if (err) return res.status(500).send("數據庫查詢失敗");if (results.length > 0) {// 老用戶,更新 is_new 為 0db.query(`UPDATE users SET is_new = 0 WHERE openid = ?`,[openid],(updateErr, updateResults) => {if (updateErr) {console.error("更新 is_new 失敗:", updateErr);return res.status(500).send("更新 is_new 失敗");}// 推送登錄狀態pushLoginStatus(openid, results[0]);return res.status(200).send({status: 'success',message: "用戶已存在",is_new: 0,data: results[0]});});} else {// 新用戶,存入數據庫db.query(`INSERT INTO users (openid, nickname, sex, province, city, headimgurl, is_new)VALUES (?, ?, ?, ?, ?, ?, 1)`,[openid, nickname, sex, province, city, headimgurl],(insertErr, insertResults) => {if (insertErr) {console.error("數據庫操作失敗:", insertErr);return res.status(500).send("數據庫操作失敗");}// 推送登錄狀態pushLoginStatus(openid, { openid, nickname, sex, province, city, headimgurl });return res.status(200).send({status: 'success',message: "新用戶信息保存成功",is_new: 1,data: { openid, nickname, sex, province, city, headimgurl }});});}});} catch (err) {console.error("服務器錯誤:", err);res.status(500).send({status: 'error',message: "服務器錯誤",error: err.message || err.toString()});}
});
3. 前端實現 WebSocket 客戶端
代碼實現
export default {data() {return {isLoggedIn: false,userInfo: {},ws: null};},created() {this.connectWebSocket();},methods: {// 連接 WebSocketconnectWebSocket() {const openid = localStorage.getItem('userOpenID');if (!openid) return;this.ws = new WebSocket(`ws://localhost:8080?openid=${openid}`);this.ws.onopen = () => {console.log('已連接到 WebSocket 服務器');};this.ws.onmessage = (event) => {const data = JSON.parse(event.data);if (data.type === 'login_success') {localStorage.setItem('userOpenID', data.data.openid);this.isLoggedIn = true;this.userInfo = data.data.userInfo;this.$store.dispatch('saveUserInfo', this.userInfo);}};this.ws.onclose = () => {console.log('WebSocket 連接已關閉');};}},beforeDestroy() {// 組件銷毀時關閉 WebSocket 連接if (this.ws) {this.ws.close();}}
};
4. 總結
-
后端:
- 使用 WebSocket 服務器推送用戶登錄狀態。
- 在用戶登錄成功后,通過
pushLoginStatus
推送消息。
-
前端:
- 連接 WebSocket 服務器,并監聽登錄狀態的消息。
- 在收到消息后,更新用戶信息和登錄狀態。