SpringBoot2.x集成WebSocket实现多客户端共享点单

目录

  • 前言
  • 思路
  • 代码
  • 测试

前言

共享点单一般发生去餐厅共同扫桌面二维码,或者手机点单,发起人分享订单给别人,别人通过链接进入点单页面。
自己琢磨了下共享点单的一套流程,然后使用springboot+websocket+redis简单实现了一段拼单逻辑。

思路

  1. 在发起拼单时,首先会创建一个订单号。后面用户共享时,都进入该笔订单的服务连接。可以理解为一个聊天室。
  2. 使用websocket同步消息。
  3. 使用redis记录订单下对应的商品。
  4. 当所有用户关闭与服务器连接后,清除redis中的数据。

代码

以下为主要代码:

  • pom.xml 依赖
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-websocketartifactId>
dependency>
  • 配置类 WebSocketConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket 配置类
 * pom.xml,springboot 2.x 自带websocket依赖,不需要带版本号
 * 
 *    org.springframework.boot
 *    spring-boot-starter-websocket
 * 
 * add qy 
 */
@Configuration
public class WebSocketConfig {

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

}

  • 服务类 WebSocketServer.java
package com.qiuyu.demo.service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.qiuyu.demo.utils.redis.RedisUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;



@ServerEndpoint("/ws/shareBill/{orderId}/{userId}")
// @ServerEndpoint("/wsserver/{userId}")
@Component
public class WebSocketServer {

    private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    /**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
    private static int onlineTableCount = 0;

    private static ConcurrentHashMap<String, HashMap<String,WebSocketServer>> orderToUsersMap = new ConcurrentHashMap<>();
    /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
    private Session session;
    /**接收userId*/
    private String userId="";

    private String orderId="";

    private static final String SHARE_BILL_KEY  = "SHARE_BILL_KEY_";
    /**
     * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen(Session session,@PathParam("orderId") String orderId,@PathParam("userId") String userId) {
        this.session = session;
        this.userId=userId;
        this.orderId=orderId;
        // 获取拼单号是否存在
        HashMap<String,WebSocketServer> webSocketMap;
        if (orderToUsersMap.containsKey(orderId)){
            webSocketMap = orderToUsersMap.get(orderId);
            if (webSocketMap.containsKey(userId)){
                webSocketMap.remove(userId);
                webSocketMap.put(userId,this);
            } else {
                webSocketMap.put(userId,this);
            }
            orderToUsersMap.put(orderId,webSocketMap);
            // 将订单对应的JSON 发送给对应userId,可以从redis取出来
            try {
                sendMessage(getMessageFromRedisByOrder(orderId));
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }else{
            webSocketMap = new HashMap<>();
            webSocketMap.put(userId,this);
            orderToUsersMap.put(orderId,webSocketMap);
            //加入set中
            addOnlineCount();
            //在线数加1
        }
        log.info("用户ID:{},加入当前订单{},该订单拼单人数{},总拼单数为:{}" ,userId,orderId,webSocketMap.size(),getOnlineCount());

    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if(orderToUsersMap.containsKey(orderId)){
            HashMap<String,WebSocketServer> webSocketMap = orderToUsersMap.get(orderId);
            if (webSocketMap.size()==0){
                // 无参与人,移除订单
                orderToUsersMap.remove(orderId);
                subOnlineCount();
                log.info("订单{}移除,总拼单数为:{}" ,orderId, getOnlineCount());
            }else {
                if (webSocketMap.containsKey(userId)){
                    webSocketMap.remove(userId);
                    if (webSocketMap.size()==0){
                        // 无参与人,移除订单
                        orderToUsersMap.remove(orderId);
                        subOnlineCount();
                        String key = SHARE_BILL_KEY+orderId;
                        RedisUtils.remove(key);
                        log.info("订单{}移除,总拼单数为:{}" ,orderId, getOnlineCount());
                    }
                    log.info("用户ID:{},退出当前订单{},该订单拼单人数{},总拼单数为:{}" ,userId,orderId,webSocketMap.size(),getOnlineCount());
                }
            }
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用户消息ID:"+userId+",报文:"+message);
        //可以群发消息
        //消息保存到数据库、redis
        if(StringUtils.isNotBlank(message)){
            try {
                // 存储报文到redis
                sendMessageToRedis(orderId,message);
                //解析发送的报文
                JSONObject jsonObject = JSON.parseObject(message);
                // 已选商品同步至其他点单人
                HashMap<String,WebSocketServer> webSocketMap = orderToUsersMap.get(orderId);
                for (Map.Entry entry : webSocketMap.entrySet()){
                    WebSocketServer webSocketServer = (WebSocketServer)entry.getValue();
                    webSocketServer.sendMessage(jsonObject.toJSONString());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误ID:"+this.userId+",原因:"+error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    private void sendMessageToRedis(String orderId,String message){
        String key = SHARE_BILL_KEY+orderId;
        if (RedisUtils.exists(key)){
            RedisUtils.remove(key);
            RedisUtils.set(key,message,60*15L);
        }
        RedisUtils.set(key,message,60*15L);
    }


    private String getMessageFromRedisByOrder(String orderId){
        String key = SHARE_BILL_KEY+orderId;
        if (RedisUtils.exists(key)){
            return RedisUtils.get(key).toString();
        }
        return "";
    }

    /**
     * 发送自定义消息
     * */
    public static void sendInfo(String message,@PathParam("orderId") String orderId,@PathParam("userId") String userId) throws IOException {
        log.info("发送消息到ID:"+userId+",报文:"+message);
        if(StringUtils.isNotBlank(orderId) && orderToUsersMap.get(orderId).containsKey(userId)){
            orderToUsersMap.get(orderId).get(userId).sendMessage(message);
        }else{
            log.error("用户ID"+userId+",不在线!");
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineTableCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineTableCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineTableCount--;
    }
}

  • 利用freemarker 解析的一个测试页面
DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通讯title>
head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js">script>
<script>
    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else{
            console.log("您的浏览器支持WebSocket");
            // 实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
            // 等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
            // var socketUrl="${request.contextPath}/im/"+$("#userId").val();
            var socketUrl="http://localhost:8080/ws/shareBill/"+$("#orderId").val()+"/"+$("#userId").val();
            socketUrl=socketUrl.replace("https","ws").replace("http","ws");
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //打开事件
            socket.onopen = function() {
                console.log("websocket已打开");
                //socket.send("这是来自客户端的消息" + location.href + new Date());
            };
            //获得消息事件
            socket.onmessage = function(msg) {
                console.log(msg.data);
				var json = JSON.parse(msg.data);
				console.log("JSON.parse()",json);
				document.getElementById("contentText").value = json.contentText;
                //发现消息进入    开始处理前端触发逻辑
            };
            //关闭事件
            socket.onclose = function() {
                console.log("websocket已关闭");
            };
            //发生了错误事件
            socket.onerror = function() {
                console.log("websocket发生了错误");
            }
        }
    }

    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else {
            console.log("您的浏览器支持WebSocket");
            console.log('{"orderId":"'+$("#orderId").val()+'","contentText":"'+$("#contentText").val()+'"}');
            socket.send('{"orderId":"'+$("#orderId").val()+'","contentText":"'+$("#contentText").val()+'"}');
        }
    }
	function createOrderId() {
        document.getElementById("orderId").value = "20210323162129128WM6542";
    }
script>
<body>
<p>【用户ID】:<div><input id="userId" name="userId" type="text" value="wx123wdgr23icowe02c">div>
<p>【订单ID】:<div><input id="orderId" name="orderId" type="text" value=""><button onclick="createOrderId()">创建订单(默认写死的)button>div>
<p>【已选商品】:<div><input id="contentText" name="contentText" type="text" value="">div>
<p>【1、操作】:<div><button onclick="openSocket()">发起拼单/加入拼单button>div>
<p>【2、操作】:<div><button onclick="sendMessage()">点单操作(+ -)button>div>
body>

html>

  • 控制层 WebSocketController.java
package com.qiuyu.demo.resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class WebSocketController {

    @GetMapping("/index")
    public String re(){
        return "sharedOrder";
    }

}

测试

1、创建订单,这里默认一笔订单号了。
SpringBoot2.x集成WebSocket实现多客户端共享点单_第1张图片
2、发起人可以先选一部分商品,在发起拼单,也可以直接发起。
SpringBoot2.x集成WebSocket实现多客户端共享点单_第2张图片
在这里插入图片描述
3、添加商品
SpringBoot2.x集成WebSocket实现多客户端共享点单_第3张图片
这里是模拟,直接简单地将商品当成字符串处理保存到redis中。
SpringBoot2.x集成WebSocket实现多客户端共享点单_第4张图片
SpringBoot2.x集成WebSocket实现多客户端共享点单_第5张图片
4、其他用户加入拼单

SpringBoot2.x集成WebSocket实现多客户端共享点单_第6张图片
SpringBoot2.x集成WebSocket实现多客户端共享点单_第7张图片
SpringBoot2.x集成WebSocket实现多客户端共享点单_第8张图片

大家可以拉取项目启动尝试一下,去了解下运行过程。希望能给大家带来一些解决拼单问题的思路。有问题留言吧

你可能感兴趣的:(Java进阶之路,websocket,java,redis)