全栈开发杂谈————关于websocket若干问题的大讨论

🍋‍🟩🍋‍🟩🍋‍🟩风起于青萍之末,浪成于微澜之间

小编为何会决定写一篇关于websocket的文章?

如下图所示是小编编写的一款外卖小程序,我们可以看到在商标的右侧有一个显示"休息中"的字样,而小编想要实现两种店铺状态即"休息中"和"营业中"两种状态的切换,并且当店铺处于休息中时用户是无法点餐的。

下图是管理端的店铺状态显示,我们可以发现右上角有一个营业状态设置的功能,点击后可以选择店铺状态为营业中或打烊中,营业中小程序就会显示营业中用户可以正常点餐,打烊中小程序就会显示休息中并且用户不能点餐。


通过上面的描述,我们不难发现,小程序的店铺状态取决于管理端的用户状态,而小程序想要获取到管理端的店铺状态就必须通过一种工具或者介质来实现或者说必须建立一种连接。

🍏🍏🍏纸上得来终觉浅,觉知此事要躬行

记小编一场酣畅淋漓的试错

说到获取店铺状态我们首先想到的就是查询店铺状态,就是小程序端通过查询的方式得到管理端的店铺状态,但是问题来了,管理端的店铺状态会切换,我们不可能保证每次查询到的都是店铺的最新状态,由此我们会想到一种更高级的查询方式---------轮流查询,即轮询的方式,实现原理就是每隔固定的时间间隔查询一次,时间间隔由我们自己指定,下面我们来看代码实现

对于轮询方式,由于是小程序查询管理端状态,所以我们只需要在小程序即用户端做出修改即可

用户端代码(app.js):

javascript 复制代码
require('./common/runtime.js')
require('./common/vendor.js')
require('./common/main.js')

// 定期轮询店铺状态
setInterval(function() {
  try {
    // 直接使用wx.request获取店铺状态
    wx.request({
      url: 'http://localhost:8080/user/shop/status', // 后端API地址
      method: 'GET',
      success: function(res) {
        // 将获取到的店铺状态存储到全局缓存中
        if (res && res.data) {
          wx.setStorageSync('SHOP_STATUS', res.data);
          console.log('店铺状态已更新:', res.data);
        }
      },
      fail: function(err) {
        console.error('获取店铺状态失败:', err);
      },
      complete: function() {
    
      }
    });
  } catch (error) {
    console.error('轮询机制异常:', error);
  }
}, 30000); // 每30秒轮询一次

核心属性:
url :我们请求店铺状态的接口路径
method :请求方法,由于是只查询所以我们用get
30000:轮询时间间隔,可以自己自定义,这里是每30秒查询一次

关于轮询方式优劣势的大讨论

优势 :
1.简单易实现 ,逻辑简单,技术难度低
2.主动检查 ,程序可以及时发现状态变化
劣势
1.资源浪费,由于需要频繁的查询,可能导致CPU和网络资源的浪费,即使店铺状态位于缓存中也会不断调用缓存,增加系统负担。

2.实时性差 ,轮询时间设置较长会导致我们无法捕获最新的状态,比如我们店铺状态已经切换但是我们周期过长就无法及时捕捉这种变化,但如果设置的过短比如1秒,那么前端就会不断发送http请求,加重服务器负载。
3.可扩展性差,当需要轮询多个条件时,代码复杂性会增加。

所以不论怎么说,大型项目开发轮询都不是一个好的处理方式

🪶🪶🪶撑一支长蒿,向青草更青处漫溯

上面我们说到轮询方式不太好,那么有没有一种更好的方式替代它呢,这就是我们今天要说的websocket(长连接),我们先来看一下代码应该怎么写,由于websocket是双向通信,需要双方建立连接,所以我们管理端和用户端的代码都要写

管理端代码(ShopController.java):

java 复制代码
  @PutMapping("/{status}")
    @ApiOperation("设置店铺的营业状态")
    public Result setStatus(@PathVariable Integer status){
        log.info("设置店铺营业状态为:{}",status==1?"营业中":"打烊中");
        redisTemplate.opsForValue().set(KEY,status);
        
        // 通过WebSocket向所有客户端推送店铺状态变更通知
        Map<String, Object> messageMap = new HashMap<>();
        messageMap.put("type", 3); // 3表示店铺状态变更
        messageMap.put("status", status);
        messageMap.put("content", status == 1 ? "店铺已营业" : "店铺已打烊");
        
        String jsonMessage = com.alibaba.fastjson.JSON.toJSONString(messageMap);
        webSocketServer.sendToAllClient(jsonMessage);
        log.info("已向所有客户端推送店铺状态变更通知");
        
        return Result.success();
    }

如下图我们采用websocket建立长连接后我们就可以看到控制台输出的已向所有客户端推送店铺状态变更通知的日志信息

小程序端代码(app.js):

javascript 复制代码
require('./common/runtime.js')
require('./common/vendor.js')
require('./common/main.js')

// WebSocket连接管理
let ws = null;
let wsReconnectTimer = null;
const RECONNECT_INTERVAL = 5000; // 重连间隔时间(毫秒)
const WS_URL = 'ws://localhost:8080/ws/weixin_client'; // WebSocket服务地址

// 初始化WebSocket连接
function initWebSocket() {
  // 关闭已存在的连接
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.close();
  }
  
  try {
    ws = wx.connectSocket({
      url: WS_URL,
      success() {
        console.log('WebSocket连接已发起');
      },
      fail(err) {
        console.error('WebSocket连接失败:', err);
        // 安排重连
        scheduleReconnect();
      }
    });
    
    // 连接成功回调
    ws.onOpen(() => {
      console.log('WebSocket连接已建立');
      // 连接成功后获取一次店铺状态
      getShopStatusOnce();
      // 清除重连计时器
      if (wsReconnectTimer) {
        clearTimeout(wsReconnectTimer);
        wsReconnectTimer = null;
      }
    });
    
    // 接收消息回调
    ws.onMessage((res) => {
      console.log('收到WebSocket消息:', res.data);
      handleWebSocketMessage(res.data);
    });
    
    // 连接关闭回调
    ws.onClose(() => {
      console.log('WebSocket连接已关闭');
      // 安排重连
      scheduleReconnect();
    });
    
    // 连接错误回调
    ws.onError((err) => {
      console.error('WebSocket连接错误:', err);
      // 安排重连
      scheduleReconnect();
    });
  } catch (error) {
    console.error('WebSocket初始化异常:', error);
    scheduleReconnect();
  }
}

// 安排重连
function scheduleReconnect() {
  if (!wsReconnectTimer) {
    console.log(`将在${RECONNECT_INTERVAL}ms后尝试重新连接WebSocket`);
    wsReconnectTimer = setTimeout(() => {
      wsReconnectTimer = null;
      initWebSocket();
    }, RECONNECT_INTERVAL);
  }
}

// 处理WebSocket消息
function handleWebSocketMessage(message) {
  try {
    const data = JSON.parse(message);
    
    // 处理店铺状态变更消息(type=3)
    if (data.type === 3 && data.status !== undefined) {
      console.log('收到店铺状态变更通知:', data.status === 1 ? '营业中' : '打烊中');
      // 更新本地缓存中的店铺状态
      wx.setStorageSync('SHOP_STATUS', data.status);
      // 发送全局事件通知页面刷新
      const app = getApp();
      if (app && app.globalData && app.globalData.eventBus) {
        app.globalData.eventBus.emit('shopStatusChange', data.status);
      }
    }
    
   
  } catch (error) {
    console.error('解析WebSocket消息失败:', error);
  }
}

// 一次性获取店铺状态
function getShopStatusOnce() {
  try {
    wx.request({
      url: 'http://localhost:8080/user/shop/status', // 后端API地址
      method: 'GET',
      success: function(res) {
        // 将获取到的店铺状态存储到全局缓存中
        if (res && res.data) {
          wx.setStorageSync('SHOP_STATUS', res.data);
          console.log('店铺状态已初始化:', res.data);
        }
      },
      fail: function(err) {
        console.error('获取店铺状态失败:', err);
      }
    });
  } catch (error) {
    console.error('获取店铺状态异常:', error);
  }
}

// 应用启动时初始化WebSocket连接
initWebSocket();

核心属性:

RECONNECT_INTERVAL :重连时间间隔,当websocket连接建立失败后,指定重新尝试的时间间隔,避免网络不稳定时websocket频繁发起请求
WS_URL:指定websocket服务地址

同时我们刷新小程序页面可以看到店铺又文章的开头的休息中变为营业中

2.
优势
1 .WebSocket 提供了全双工通信通道,服务器可以主动向客户端推送数据,无需客户端轮询,同时客户端也能向服务端回送数据。
2 .低延迟与高效性,相较于轮询方式,websocket建立了一种长连接,减少了重复建立连接的开销(三次握手),避免HTTP头部重复传输,持续连接状态下,数据包直接传输并且只在服务器有事件时才会通知客户端,持久化连接的同时减少了http请求数量,降低了网络负载和延迟。
3 .更少的带宽消耗,WebSocket 数据帧头部仅需2-10字节,而HTTP请求通常包含数百字节的头部信息。
劣势
1 .需要处理网络中断和重连机制

移动设备网络切换时连接可能中断

NAT超时可能导致连接断开(通常30-60分钟)
2 .些企业防火墙会阻止WebSocket连接

旧版浏览器(如IE9及以下)需要降级方案

可能需要使用Socket.io等库实现兼容
3 .需要维护连接状态

要处理消息顺序和重传

需设计自定义协议格式

调试工具不如HTTP完善
4 .每个连接需要保持状态

高并发时需要优化服务器架构

可能需要专门的WebSocket服务器(如Node.js)

🏍️🏍️🏍️坐好,现在开始加速

关于websocket的心跳机制

前面我们已经说过websocket是在TCP协议基础之上的一种需要双方建立连接的协议,那么读者可能会问如果连接断开怎么办呢,怎么就知道连接断开了呢,这就是我们下面要提到的websocket的"心跳机制"

心跳机制:

定义

是一种保持连接活跃的重要技术手段,主要用于检测连接是否正常,并防止因长时间无数据传输而被中间设备(如防火墙、代理服务器等)断开连接

作用

1.检测连接状态 :定期发送心跳包(Ping/Pong)确认连接是否存活。

2.防止超时断开 :避免因长时间无数据传输而被防火墙或代理服务器强制关闭连接。

3.资源清理:及时发现断开的连接并释放服务器资源。

实现方式

方式1:Ping/Pong 帧(WebSocket 协议原生支持)

Ping 帧:由一端(通常是服务端)主动发送,包含少量数据(如时间戳)。

Pong 帧:另一端收到 Ping 后必须回复 Pong,内容与 Ping 相同。

方式2: 应用层自定义心跳

实现步骤:

客户端和服务端约定一个固定格式的消息,定期(如每 30 秒)发送该消息。

接收方需在超时时间内(如 60 秒)回复确认,否则判定连接异常。

我们上面的小程序的websocket就可以采用自定义心跳的方式来加强连接,代码如下:

管理端代码(WebSocketServer.java添加心跳机制依赖)

java 复制代码
package com.sky.websocket;

import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * WebSocket服务
 */
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存储Session对象
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
        
        try {
            // 解析客户端消息
            JSONObject jsonMessage = JSON.parseObject(message);
            Integer type = jsonMessage.getInteger("type");
            
            // 处理心跳消息(type=0)
            if (type != null && type == 0) {
                String content = jsonMessage.getString("content");
                if ("ping".equals(content)) {
                    // 返回心跳响应
                    sendPongResponse(sid);
                }
            }
            
            // 这里可以根据type处理其他类型的消息
        } catch (Exception e) {
            System.out.println("解析消息失败: " + e.getMessage());
        }
    }

    /**
     * 发送心跳响应给客户端
     */
    private void sendPongResponse(String sid) {
        try {
            Map<String, Object> response = new HashMap<>();
            response.put("type", 0);
            response.put("content", "pong");
            String pongMessage = JSON.toJSONString(response);
            
            Session session = sessionMap.get(sid);
            if (session != null && session.isOpen()) {
                session.getBasicRemote().sendText(pongMessage);
                System.out.println("已发送心跳响应给客户端:" + sid);
            }
        } catch (IOException e) {
            System.out.println("发送心跳响应失败: " + e.getMessage());
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发消息
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送消息给指定客户端
     */
    public void sendToClient(String message, String sid) {
        Session session = sessionMap.get(sid);
        if (session != null) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

小程序端代码(加强版app.js)

核心是加入了心跳间隔时间和心跳超时时间并且加入了心跳检测成功机制,代码逻辑是:

1.客户端每30秒向服务器发送ping消息 服务器接收到ping消息后立即返回pong消息

2.客户端记录每次收到pong响应的时间

3.如果超过45秒未收到pong响应,客户端会主动断开连接并尝试重连

javascript 复制代码
require('./common/runtime.js')
require('./common/vendor.js')
require('./common/main.js')

// WebSocket连接管理
let ws = null;
let wsReconnectTimer = null;
let heartbeatTimer = null;
const RECONNECT_INTERVAL = 5000; // 重连间隔时间(毫秒)
const HEARTBEAT_INTERVAL = 30000; // 心跳间隔时间(毫秒)
const HEARTBEAT_TIMEOUT = 45000; // 心跳超时时间(毫秒)
const WS_URL = 'ws://localhost:8080/ws/weixin_client'; // WebSocket服务地址
let lastHeartbeatTime = 0; // 上次心跳时间
let isServerAlive = true; // 服务端是否存活

// 应用启动时初始化WebSocket连接
initWebSocket();

// 初始化WebSocket连接
function initWebSocket() {
  // 关闭已存在的连接
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.close();
  }
  
  try {
    ws = wx.connectSocket({
      url: WS_URL,
      success() {
        console.log('WebSocket连接已发起');
        lastHeartbeatTime = Date.now();
        isServerAlive = true;
      },
      fail(err) {
        console.error('WebSocket连接失败:', err);
        // 安排重连
        scheduleReconnect();
      }
    });
    
    // 连接成功回调
    ws.onOpen(() => {
      console.log('WebSocket连接已建立');
      // 连接成功后获取一次店铺状态
      getShopStatusOnce();
      // 清除重连计时器
      if (wsReconnectTimer) {
        clearTimeout(wsReconnectTimer);
        wsReconnectTimer = null;
      }
      // 启动心跳机制
      startHeartbeat();
    });
    
    // 接收消息回调
    ws.onMessage((res) => {
      console.log('收到WebSocket消息:', res.data);
      handleWebSocketMessage(res.data);
    });
    
    // 连接关闭回调
    ws.onClose(() => {
      console.log('WebSocket连接已关闭');
      // 停止心跳
      stopHeartbeat();
      // 安排重连
      scheduleReconnect();
    });
    
    // 连接错误回调
    ws.onError((err) => {
      console.error('WebSocket连接错误:', err);
      // 停止心跳
      stopHeartbeat();
      // 安排重连
      scheduleReconnect();
    });
  } catch (error) {
    console.error('WebSocket初始化异常:', error);
    stopHeartbeat();
    scheduleReconnect();
  }
}

// 安排重连
function scheduleReconnect() {
  if (!wsReconnectTimer) {
    console.log(`将在${RECONNECT_INTERVAL}ms后尝试重新连接WebSocket`);
    wsReconnectTimer = setTimeout(() => {
      wsReconnectTimer = null;
      initWebSocket();
    }, RECONNECT_INTERVAL);
  }
}

// 启动心跳机制
function startHeartbeat() {
  // 清除已有的心跳计时器
  stopHeartbeat();
  
  lastHeartbeatTime = Date.now();
  isServerAlive = true;
  
  // 发送心跳
  heartbeatTimer = setInterval(() => {
    // 检查是否心跳超时
    const now = Date.now();
    if (now - lastHeartbeatTime > HEARTBEAT_TIMEOUT) {
      console.error('WebSocket心跳超时,断开连接并尝试重连');
      isServerAlive = false;
      // 主动断开连接,触发重连
      if (ws) {
        ws.close();
      }
      return;
    }
    
    // 发送心跳消息
    if (ws && ws.readyState === WebSocket.OPEN) {
      const heartbeatMsg = JSON.stringify({ type: 0, content: 'ping' });
      ws.send({
        data: heartbeatMsg,
        success() {
          console.log('心跳消息发送成功');
        },
        fail(err) {
          console.error('心跳消息发送失败:', err);
        }
      });
    }
  }, HEARTBEAT_INTERVAL);
}

// 停止心跳机制
function stopHeartbeat() {
  if (heartbeatTimer) {
    clearInterval(heartbeatTimer);
    heartbeatTimer = null;
  }
}

// 处理WebSocket消息
function handleWebSocketMessage(message) {
  try {
    const data = JSON.parse(message);
    
    // 处理心跳响应
    if (data.type === 0 && data.content === 'pong') {
      console.log('收到心跳响应');
      lastHeartbeatTime = Date.now();
      isServerAlive = true;
      return;
    }
    
    // 处理店铺状态变更消息(type=3)
    if (data.type === 3 && data.status !== undefined) {
      console.log('收到店铺状态变更通知:', data.status === 1 ? '营业中' : '打烊中');
      // 更新本地缓存中的店铺状态
      wx.setStorageSync('SHOP_STATUS', data.status);
      // 发送全局事件通知页面刷新
      const app = getApp();
      if (app && app.globalData && app.globalData.eventBus) {
        app.globalData.eventBus.emit('shopStatusChange', data.status);
      }
    }
    
    // 可以在这里处理其他类型的消息
  } catch (error) {
    console.error('解析WebSocket消息失败:', error);
  }
}

// 一次性获取店铺状态
function getShopStatusOnce() {
  try {
    // 直接使用wx.request获取店铺状态
    wx.request({
      url: 'http://localhost:8080/user/shop/status', // 后端API地址
      method: 'GET',
      success: function(res) {
        // 将获取到的店铺状态存储到全局缓存中
        if (res && res.data) {
          wx.setStorageSync('SHOP_STATUS', res.data);
          console.log('店铺状态已初始化:', res.data);
        }
      },
      fail: function(err) {
        console.error('获取店铺状态失败:', err);
      }
    });
  } catch (error) {
    console.error('获取店铺状态异常:', error);
  }
}

如下图所示是我们加入心跳机制后小程序端的运行结果,显示连接断开,当我们再次在管理端更改店铺状态或者等待片刻然后刷新小程序页面时,连接又会重新建立

怎么样,是不是感觉很神奇?

🍕🍕🍕不识庐山真面目,只缘身在此山中

面对SSE的横空出世,到底谁更胜一筹

废话不多说我们直接看代码,如果把websocket改成SSE应该怎么改

管理端(user包下新建SseController类):

java 复制代码
package com.sky.controller.user;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * SSE控制器,用于服务器向客户端推送消息
 */
@RestController
@RequestMapping("/user/sse")
public class SseController {
    
    // 存储所有SSE连接
    private static final Map<String, SseEmitter> CLIENTS = new ConcurrentHashMap<>();
    
    /**
     * 建立SSE连接
     * @param clientId 客户端唯一标识
     * @return SSE连接对象
     */
    @GetMapping("/connect")
    public SseEmitter connect(String clientId) {
        // 设置超时时间,0表示永不超时
        SseEmitter emitter = new SseEmitter(0L);
        
        // 保存连接
        CLIENTS.put(clientId, emitter);
        
        // 连接关闭时移除
        emitter.onCompletion(() -> {
            CLIENTS.remove(clientId);
        });
        
        emitter.onTimeout(() -> {
            CLIENTS.remove(clientId);
            emitter.complete();
        });
        
        emitter.onError((e) -> {
            CLIENTS.remove(clientId);
            try {
                emitter.completeWithError(e);
            } catch (Exception ex) {
                // ignore
            }
        });
        
        return emitter;
    }
    
    /**
     * 向所有客户端发送消息
     * @param message 消息内容
     */
    public static void sendToAllClient(String message) {
        CLIENTS.forEach((clientId, emitter) -> {
            try {
                emitter.send(SseEmitter.event()
                        .name("message")
                        .data(message)
                        .reconnectTime(5000));
            } catch (IOException e) {
                // 发送失败,移除客户端
                CLIENTS.remove(clientId);
                try {
                    emitter.complete();
                } catch (Exception ex) {
                    // ignore
                }
            }
        });
    }
}

管理端(admin/ShopController)

新增:

java 复制代码
import com.sky.controller.user.SseController;
 SseController.sendToAllClient(jsonMessage);

删除:

java 复制代码
import com.sky.websocket.WebSocketServer;

@Autowired
    private WebSocketServer webSocketServer;

小程序端(app.js):

java 复制代码
require('./common/runtime.js')
require('./common/vendor.js')
require('./common/main.js')

// SSE连接管理
let sseRequestTask = null;
let sseReconnectTimer = null;
const RECONNECT_INTERVAL = 5000; // 重连间隔时间(毫秒)
const SSE_URL = 'http://localhost:8080/user/sse/connect'; // SSE服务地址
let clientId = null;

// 应用启动时初始化SSE连接
initSSE();

// 初始化SSE连接
function initSSE() {
  // 关闭已存在的连接
  if (sseRequestTask) {
    sseRequestTask.abort();
    sseRequestTask = null;
  }
  
  // 生成唯一客户端ID
  if (!clientId) {
    clientId = 'weixin_client_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
  }
  
  try {
    console.log('开始建立SSE连接');
    sseRequestTask = wx.request({
      url: SSE_URL,
      method: 'GET',
      header: {
        'Accept': 'text/event-stream',
        'Cache-Control': 'no-cache'
      },
      responseType: 'text',
      data: {
        clientId: clientId
      },
      success(res) {
        // 正常情况下不会进入这里,因为SSE连接是长连接
        console.log('SSE连接响应:', res);
      },
      fail(err) {
        console.error('SSE连接失败:', err);
        // 安排重连
        scheduleSSEReconnect();
      },
      complete() {
        console.log('SSE连接完成/中断');
        // 连接完成或中断时,安排重连
        scheduleSSEReconnect();
      }
    });
    
    // 监听数据流
    if (sseRequestTask && typeof sseRequestTask.onHeadersReceived === 'function') {
      sseRequestTask.onHeadersReceived((res) => {
        console.log('SSE连接头信息接收成功');
        // 连接成功后获取一次店铺状态
        getShopStatusOnce();
        // 清除重连计时器
        if (sseReconnectTimer) {
          clearTimeout(sseReconnectTimer);
          sseReconnectTimer = null;
        }
      });
    }
    
    // 监听数据接收(注意:小程序的wx.request不支持流式接收数据)
    // 因此我们需要在连接中断时重新获取最新状态
    
  } catch (error) {
    console.error('SSE初始化异常:', error);
    scheduleSSEReconnect();
  }
}

// 安排SSE重连
function scheduleSSEReconnect() {
  if (!sseReconnectTimer) {
    console.log(`将在${RECONNECT_INTERVAL}ms后尝试重新连接SSE`);
    sseReconnectTimer = setTimeout(() => {
      sseReconnectTimer = null;
      initSSE();
    }, RECONNECT_INTERVAL);
  }
}

// 处理SSE消息(在小程序中,我们通过轮询来弥补无法流式接收数据的不足)
function handleSSEMessage(message) {
  try {
    // 由于小程序限制,我们这里模拟处理消息格式
    // 实际项目中,需要根据服务端返回的SSE格式进行解析
    const eventData = message.replace(/^data:/, '').trim();
    const data = JSON.parse(eventData);
    
    // 处理店铺状态变更消息(type=3)
    if (data.type === 3 && data.status !== undefined) {
      console.log('收到店铺状态变更通知:', data.status === 1 ? '营业中' : '打烊中');
      // 更新本地缓存中的店铺状态
      wx.setStorageSync('SHOP_STATUS', data.status);
      // 发送全局事件通知页面刷新
      const app = getApp();
      if (app && app.globalData && app.globalData.eventBus) {
        app.globalData.eventBus.emit('shopStatusChange', data.status);
      }
    }
    
  } catch (error) {
    console.error('解析SSE消息失败:', error);
  }
}


// 当SSE连接中断时,可以通过轮询获取最新状态
setInterval(function() {
  try {
    // 直接使用wx.request获取店铺状态,作为SSE的补充
    wx.request({
      url: 'http://localhost:8080/user/shop/status', // 后端API地址
      method: 'GET',
      success: function(res) {
        // 将获取到的店铺状态存储到全局缓存中
        if (res && res.data) {
          // 获取本地已缓存的状态
          const cachedStatus = wx.getStorageSync('SHOP_STATUS');
          // 如果状态发生变化,才更新缓存并通知
          if (cachedStatus !== res.data) {
            wx.setStorageSync('SHOP_STATUS', res.data);
            console.log('店铺状态已更新:', res.data);
            // 发送全局事件通知页面刷新
            const app = getApp();
            if (app && app.globalData && app.globalData.eventBus) {
              app.globalData.eventBus.emit('shopStatusChange', res.data);
            }
          }
        }
      },
      fail: function(err) {
        console.error('获取店铺状态失败:', err);
      }
    });
  } catch (error) {
    console.error('轮询机制异常:', error);
  }
}, 30000); // 每30秒轮询一次

// 一次性获取店铺状态
function getShopStatusOnce() {
  try {
    // 直接使用wx.request获取店铺状态
    wx.request({
      url: 'http://localhost:8080/user/shop/status', // 后端API地址
      method: 'GET',
      success: function(res) {
        // 将获取到的店铺状态存储到全局缓存中
        if (res && res.data) {
          wx.setStorageSync('SHOP_STATUS', res.data);
          console.log('店铺状态已初始化:', res.data);
        }
      },
      fail: function(err) {
        console.error('获取店铺状态失败:', err);
      }
    });
  } catch (error) {
    console.error('获取店铺状态异常:', error);
  }
}

读者可以按照上述方法改写SSE版本,这里不再演示,但是由于微信小程序不支持流式接收数据,所以我们不建议采用这种方式

㊙️㊙️㊙️兼听则明,偏听则暗

八方比较才知websocket是魔是仙

🍬🍬🍬pk1:websocket VS 轮询

对比维度 WebSocket 轮询(Polling)
工作原理 全双工通信协议,建立一次连接后保持长连接,服务端可主动推送数据 客户端定时向服务端发送请求询问数据(如AJAX轮询)
连接方式 基于HTTP协议升级(101状态码),使用ws://wss://协议 标准HTTP/HTTPS短连接
实时性 毫秒级延迟(数据到达立即推送) 依赖轮询间隔(如5秒轮询则平均延迟2.5秒)
网络开销 仅需1次握手,后续无冗余Header传输 每次请求都携带完整HTTP Header(Cookie等)
服务端压力 维持长连接占用内存,但无频繁建连开销 高频短连接导致频繁TCP三次握手,QPS高时服务端压力剧增
适用场景 实时聊天、股票行情、在线游戏、协同编辑等高频双向通信场景 数据更新频率低(如每5分钟刷新天气)、兼容性要求极高的老系统
浏览器兼容性 IE10+、Chrome 4+、Firefox 11+(现代浏览器普遍支持) 所有支持HTTP的浏览器
数据格式 支持二进制帧和文本帧 仅文本(JSON/XML)
断线处理 自动心跳检测+重连机制 依赖下一次轮询恢复

🍬🍬🍬pk2:websocket VS SSE

对比维度 WebSocket SSE (Server-Sent Events)
协议类型 全双工通信协议 半双工通信协议(仅服务器向客户端推送)
连接方式 基于TCP的持久连接 基于HTTP的长连接
数据格式 支持二进制和文本数据 仅支持文本数据(通常是UTF-8编码的EventStream)
浏览器兼容性 IE10+、主流浏览器均支持 IE不支持,其他主流浏览器支持
心跳机制 需要手动实现心跳包 自动保持连接(通过HTTP长连接机制)
断线重连 需要手动实现 浏览器自动重连(默认3秒间隔)
应用场景 实时聊天、在线游戏、高频数据交互 股票行情推送、新闻实时更新、日志监控等
安全性 支持wss加密 支持https加密
头部开销 初始握手后仅需2字节帧头 每次消息都携带HTTP头部
实现复杂度 需要处理协议帧、心跳等复杂逻辑 基于标准HTTP协议,实现简单
跨域支持 支持CORS 支持CORS

🍬🍬🍬pk3: websocket VS webRTC

特性 WebSocket WebRTC
协议 基于 TCP 的独立协议 使用多种协议(SRTP, SCTP 等)
连接方式 客户端-服务器双向通信 点对点(P2P)通信
延迟 中等(依赖服务器转发) 低(直接端到端传输)
数据格式 文本或二进制数据 音视频流、任意数据
使用场景 实时聊天、通知推送、游戏状态同步 视频会议、实时流媒体、文件传输
NAT穿透 需要服务器中转 内置 ICE/STUN/TURN 穿透机制
建立连接 HTTP 升级握手 需要信令服务器交换 SDP 信息
浏览器支持 所有现代浏览器 主流浏览器(需处理兼容性)
典型应用 Slack, 股票行情推送 Zoom, Google Meet, 远程桌面
带宽效率 较高(无媒体编码开销) 取决于编解码器和网络条件
安全性 支持 WSS 加密 强制加密(DTLS-SRTP)

WebSocket 更适合需要服务器协调的场景

WebRTC 更适合媒体传输和高实时性要求的 P2P 应用

两者可以结合使用(如用 WebSocket 做信令通道)

💚💚💚大总结:

  1. 如果仅需要单向推送 :SSE是一个比WebSocket更轻量级的选择,实现简单且资源消耗更低

  2. 如果未来可能扩展双向通信 :WebSocket提供了更完整的功能,避免未来需要重构

    而且本项目有来单提醒和催单业务功能本身就有websocket依赖,所以websocket无疑是最合适的选择

  3. 实际应用考虑 :在实际应用中,还需要考虑服务器端的支持情况、客户端环境的限制以及团队的技术熟悉程度等因素,选择最适合的技术方案。

  4. 总结来说,SSE和WebSocket各有优势,选择哪种技术应该基于具体的业务需求、技术环境和未来的扩展性考虑。对于简单的单向推送场景,SSE可能是更合适的选择;而对于需要双向通信或更复杂交互的场景,WebSocket则更为全面。

相关推荐
weixin_419658314 小时前
Spring 的统一功能
java·后端·spring
小许学java4 小时前
Spring AI-流式编程
java·后端·spring·sse·spring ai
haogexiaole5 小时前
Java高并发常见架构、处理方式、api调优
java·开发语言·架构
EnCi Zheng5 小时前
@ResponseStatus 注解详解
java·spring boot·后端
wdfk_prog6 小时前
闹钟定时器(Alarm Timer)初始化:构建可挂起的定时器基础框架
java·linux·数据库
怎么没有名字注册了啊6 小时前
C++后台进程
java·c++·算法
z日火6 小时前
Java 泛型
java·开发语言
简色6 小时前
题库批量(文件)导入的全链路优化实践
java·数据库·mysql·mybatis·java-rabbitmq
程序员飞哥6 小时前
如何设计多级缓存架构并解决一致性问题?
java·后端·面试