小柒味来 - WebSocket 来单提醒

一、WebSocket

1.1 介绍

WebSocket 是一种基于 TCP 的新型网络协议。它实现了浏览器与服务器的全双工通信 —— 只需完成一次握手,双方即可建立持久性连接,并支持双向数据传输。

1.1.1 与 HTTP 协议对比

  • 连接方式
    • HTTP:短连接
    • WebSocket:长连接
  • 通信模式
    • HTTP:单向,基于请求-响应模式(客户端请求,服务端响应)
    • WebSocket:支持双向通信
  • 底层协议
    • HTTP和WebSocket都基于TCP

1.1.2 小疑惑

Q:既然WebSocket支持双向通信,看似比HTTP更强大,那么是否可以基于WebSocket开发所有业务功能呢?

A:答案是不行,原因如下:

  • 服务器长期维护长连接,资源开销大
  • 浏览器兼容性存在差异
  • WebSocket 长连接受网络环境影响明显,需要处理好断线重连

1.1.3 应用场景

  1. 视频弹幕

  2. 网页在线聊天

  3. 体育赛事实况

  4. 股票基金实时行情


1.2 案例

3.2.1 案例分析

需求:实现浏览器与服务器全双工通信。

  • 浏览器可向服务端发送消息
  • 服务端可主动向浏览器推送消息

效果展示

实现步骤

  1. 使用websocket.html页面作为WebSocket客户端
  2. 导入WebSocket maven依赖
  3. 定义服务端组件WebSocketServer
  4. 注册WebSocket配置类WebSocketConfiguration
  5. 定义定时任务WebSocketTask,定时向客户端推送消息

3.2.2 案例代码

(1)前端页面:websocket.html

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    var websocket = null;
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点   注意现在是ws协议
        //这其实就是一个握手的请求,如果请求成功了客户端和服务端就建立了一个长连接
        //   之后就可以双向通信了。
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }
    else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息(客户端向服务端发送消息)
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
    
    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>

(2)Maven 依赖
qi-server/pom.xml 中引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

(3)服务端组件:WebSocketServer

@Component
//类似于controller方法中的路径,只不过用的注解不同,对应之前页面上的地址
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象(客户端和服务端要建立一个连接本质上就是一个会话,建立好会话之后双方之间就可以进行双向通信了)
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     *    这个过程不用我们管而是由websocket这个小框架自己来调,客户端可能有多个所以要区分不同的客户端,
     *    具体是通过sid来区分不同的客户端,sid是页面传递过来的clientId,clientId是页面生成的一个随机数
     *    @PathParam:路径参数获取
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法,类似于controller中的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

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

    /**
     * 群发:把map中的session都给遍历出来了,这就相当于是群发的一个效果,因为客户端可能
     *      有多个,都连接到了这个服务端,现在把这些session都遍历出来 然后都给这些
     *      客户端发送消息,这就是一个群发的效果。
     *  注意:这个是普通方法需要自己手动调用
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        //把sessionMap集合中的session都遍历出来
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

(4)配置类:WebSocketConfiguration

@Configuration
public class WebSocketConfiguration {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

(5)定时任务:WebSocketTask

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        //调用WebSocketServer类中群发的方法
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

3.2.3 功能测试

  1. 启动服务,打开 websocket.html 页面
  2. 浏览器向服务端发送消息
  3. 服务端每隔 5 秒向浏览器推送数据

二、来单提醒(WebSocket实践)

2.1 需求分析

当用户下单并支付成功后,需要及时通知商家。通知方式包括:

  • 语音播报
  • 弹出提示框

2.1.1 设计思路

  1. 通过WebSocket保持管理端页面与服务端的长连接
  2. 用户支付成功后,服务端调用WebSocket API主动推送消息
  3. 客户端浏览器解析消息,根据类型区分是来单提醒还是催单,并触发对应的提示和语音播报
  4. 消息格式约定为 JSON,包含以下字段:
    • type:消息类型(1=来单提醒,2=客户催单)
    • orderId:订单 ID
    • content:提示框显示的内容

无论是来单提醒还是催单,本质都关联一个订单,因此需要传递订单 ID


2.2 实现代码

2.2.1 WebSocketServer配置

复用案例中的WebSocketServer 组件及配置类

2.2.2 前端代码

前端已实现WebSocket 客户端。

  1. 登录页面打开开发者工具,点击登录
  2. 控制台显示服务端与客户端成功建立长连接:

2.2.3 支付功能处理(有商户号或其他支付途径方案)

OrderServiceImpl 中注入WebSocketServer

@Autowired
private WebSocketServer webSocketServer;

@Override
public void paySuccess(String outTradeNo) {
    Orders ordersDB = orderMapper.getByNumber(outTradeNo);

    Orders orders = Orders.builder()
            .id(ordersDB.getId())
            .status(Orders.TO_BE_CONFIRMED)
            .payStatus(Orders.PAID)
            .checkoutTime(LocalDateTime.now())
            .build();
    orderMapper.update(orders);

    Map map = new HashMap();
    map.put("type", 1); // 来单提醒
    map.put("orderId", orders.getId());
    map.put("content", "订单号:" + outTradeNo);

    String json = JSON.toJSONString(map);
    webSocketServer.sendToAllClient(json);
}

2.2.4 支付功能处理(无支付途径方案)

@Autowired
private WebSocketServer webSocketServer;

@Override
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
    Long userId = BaseContext.getCurrentId();
    User user = userMapper.getById(userId);

    JSONObject jsonObject = new JSONObject();
    jsonObject.put("code","ORDERPAID");
    OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
    vo.setPackageStr(jsonObject.getString("package"));

    Integer orderPaidStatus = Orders.PAID;
    Integer orderStatus = Orders.TO_BE_CONFIRMED;
    LocalDateTime checkoutTime = LocalDateTime.now();

    Long orderId = Long.parseLong(ordersPaymentDTO.getOrderNumber());
    orderMapper.updateStatus(orderStatus, orderPaidStatus, checkoutTime, orderId);

    Orders ordersDB = orderMapper.getByNumber(ordersPaymentDTO.getOrderNumber());

    Map map = new HashMap();
    map.put("type", 1);
    map.put("orderId", ordersDB.getId());
    map.put("content", "订单号:" + orderId);

    String json = JSON.toJSONString(map);
    webSocketServer.sendToAllClient(json);

    return vo;
}

提示:若提示音一直循环播放,原因是设置了 5 秒定时重复推送。


三、客户催单

3.1 需求分析

上面已经说明,不再赘述

3.1.1 接口设计

客户催单

接口地址:/user/order/reminder/{id}

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
id id path true integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}

3.2 实现代码

3.2.1 Controller 层

UserOrderController.java

@GetMapping("/reminder/{id}")
    @ApiOperation("客户催单")
    public Result reminder(@PathVariable Long id){
        log.info("订单id{}客户催单", id);
        orderService.reminder(id);

        return Result.success();
    }

3.2.2 Service 层

OrderService.java

/**
     * 用户催单
     * @param id
     */
    void reminder(Long id);

3.2.3 Service 实现类

OrderServiceImpl.java

public void reminder(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在
        if (ordersDB == null ) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        //基于WebSocket实现催单
        Map map = new HashMap();
        map.put("type", 2);//1表示来电提醒 2代表用户催单
        map.put("orderId", id);//订单的id
        map.put("content", "订单号:" + ordersDB.getNumber());//订单号

        //参数需要json类型,所以需要进行转化(调用WebSocketServer组件中群发的方法)
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    }

3.2.4 Mapper 层

/**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id=#{id}")
    Orders getById(Long id);

四、联调测试

  1. 登录后台
    浏览器与服务端建立长连接

  2. 小程序端下单支付
    下单并支付

  3. 查看来单提醒
    支付成功后,商家后台收到提醒,并触发语音播报

  4. 点击催单

  1. 后台弹框提醒

关于WebSocket的学习和实践就完成了,下一章小柒味来 - 项目开发总结与经验归纳


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub